Fichiers joints aux specs et raffinages (stockage local/S3) #8

Closed
opened 2026-10-25 08:24:20 +00:00 by Gilles · 0 comments
Owner

Associer des fichiers joints aux versions de specs et aux raffinages.

Stockage (configuration par variables d'env)

  • ATTACHMENTS_STORAGE=local|s3 (défaut local, MEDIA_ROOT explicite).
  • Mode s3 : S3_ENDPOINT_URL, S3_BUCKET_NAME, S3_ACCESS_KEY_ID(_FILE), S3_SECRET_ACCESS_KEY(_FILE), adressage path-style, default_acl=None (django-storages[s3]).
  • Notre instance : Garage (https://s3.stationmyr.net, bucket thierryade-attachments).
  • Au passage : migrer STATICFILES_STORAGE (supprimé depuis Django 5.1) vers un dict STORAGES complet.

Modèle

  • StoredFile : project, sha256 (unique par projet), file, size, content_type, uploaded_by/at. Stocké une seule fois, immuable.
  • Attachment : lien spec_version XOR refinement → StoredFile (PROTECT), name, added_by/at. Uniques : (cible, stored_file) et (cible, name).
  • Ajout/remplacement/suppression/restauration uniquement sur un brouillon (version ou version parente du raffinage), droit CanWriteProject.
  • Spec.reopen() recopie les liens (pas les fichiers) vers le nouveau brouillon.
  • Un StoredFile sans aucun lien est supprimé physiquement (on_commit).

Statut calculé (rien de stocké)

Comparaison avec la version N-1 de la même spec, par nom :

  • même nom, même fichier → hérité ; même nom, fichier différent → remplacé ; absent en N-1 → nouveau ; présent en N-1 mais pas en N → supprimé (entrée fantôme, restaurable).
  • Sans correspondance de nom mais même StoredFile en N-1 → hérité (renommé).
  • Raffinages : comparaison avec le raffinage de la version N-1 de la spec parente vers la même place enfant.

Usages calculés

  • exists_in_specs, exists_in_refinements : specs/raffinages utilisant le fichier.
  • exists_in_versions(spec) : versions d'une spec utilisant le fichier.
  • Filtrés selon les droits de lecture.

API

  • GET/POST/DELETE /api/attachments/ (filtre spec_version / refinement), POST .../{id}/replace/, restauration d'un supprimé.
  • GET /api/files/?project=&sha256= (détection de doublon avant envoi), GET /api/files/{id}/usages/, GET /api/files/{id}/versions/?spec=, GET /api/files/{id}/download/.
  • Déduplication côté serveur (hash recalculé, deduplicated: true).
  • Téléchargement via Django (droits vérifiés), Content-Disposition: attachment, nosniff, contournement HeadObject de Garage, taille max ATTACHMENTS_MAX_SIZE_MB (20).

Déploiement

  • Bucket Garage thierryade-attachments, clé thierryade, secrets thierryadeS3AccessKey / thierryadeS3SecretKey, deploy/docker-compose.yml + deploy/README.md.
Associer des fichiers joints aux versions de specs et aux raffinages. ## Stockage (configuration par variables d'env) - `ATTACHMENTS_STORAGE=local|s3` (défaut `local`, `MEDIA_ROOT` explicite). - Mode `s3` : `S3_ENDPOINT_URL`, `S3_BUCKET_NAME`, `S3_ACCESS_KEY_ID(_FILE)`, `S3_SECRET_ACCESS_KEY(_FILE)`, adressage path-style, `default_acl=None` (`django-storages[s3]`). - Notre instance : Garage (`https://s3.stationmyr.net`, bucket `thierryade-attachments`). - Au passage : migrer `STATICFILES_STORAGE` (supprimé depuis Django 5.1) vers un dict `STORAGES` complet. ## Modèle - `StoredFile` : `project`, `sha256` (unique par projet), `file`, `size`, `content_type`, `uploaded_by/at`. Stocké une seule fois, immuable. - `Attachment` : lien `spec_version` XOR `refinement` → `StoredFile` (PROTECT), `name`, `added_by/at`. Uniques : `(cible, stored_file)` et `(cible, name)`. - Ajout/remplacement/suppression/restauration uniquement sur un brouillon (version ou version parente du raffinage), droit `CanWriteProject`. - `Spec.reopen()` recopie les liens (pas les fichiers) vers le nouveau brouillon. - Un `StoredFile` sans aucun lien est supprimé physiquement (`on_commit`). ## Statut calculé (rien de stocké) Comparaison avec la version N-1 de la même spec, par nom : - même nom, même fichier → **hérité** ; même nom, fichier différent → **remplacé** ; absent en N-1 → **nouveau** ; présent en N-1 mais pas en N → **supprimé** (entrée fantôme, restaurable). - Sans correspondance de nom mais même `StoredFile` en N-1 → **hérité (renommé)**. - Raffinages : comparaison avec le raffinage de la version N-1 de la spec parente vers la même place enfant. ## Usages calculés - `exists_in_specs`, `exists_in_refinements` : specs/raffinages utilisant le fichier. - `exists_in_versions(spec)` : versions d'une spec utilisant le fichier. - Filtrés selon les droits de lecture. ## API - `GET/POST/DELETE /api/attachments/` (filtre `spec_version` / `refinement`), `POST .../{id}/replace/`, restauration d'un supprimé. - `GET /api/files/?project=&sha256=` (détection de doublon avant envoi), `GET /api/files/{id}/usages/`, `GET /api/files/{id}/versions/?spec=`, `GET /api/files/{id}/download/`. - Déduplication côté serveur (hash recalculé, `deduplicated: true`). - Téléchargement via Django (droits vérifiés), `Content-Disposition: attachment`, `nosniff`, contournement HeadObject de Garage, taille max `ATTACHMENTS_MAX_SIZE_MB` (20). ## Déploiement - Bucket Garage `thierryade-attachments`, clé `thierryade`, secrets `thierryadeS3AccessKey` / `thierryadeS3SecretKey`, `deploy/docker-compose.yml` + `deploy/README.md`.
Sign in to join this conversation.
No description provided.