ADR 0012 – Fichiers : un fichier par nœud, contenu adressé par SHA-256 sur disque, service en même origine durci
Accepté (2026-10-15, mode autonome) ; « un fichier au plus par nœud » remplacé par l’ADR 0032 (plusieurs fichiers ordonnés)
ADR 0012 – Fichiers : un fichier par nœud, contenu adressé par SHA-256 sur disque, service en même origine durci
Statut : Accepté (2026-10-15, mode autonome) ; « un fichier au plus par nœud » remplacé par l’ADR 0032 (plusieurs fichiers ordonnés)
Décisions prises en mode autonome (Aurélien, 2026-10-15 : options les plus simples, sûres et réversibles) : un fichier au plus par nœud ; contenu adressé par SHA-256 (table file_blob, colonne node.file_ref déjà prévue par V1, nom d’origine et type détecté sur node) ; stockage disque sous app.files.path (/data/files/ab/cd/<sha256>, sans extension), écriture dans un fichier temporaire puis renommage atomique, déduplication ; taille maximale 50 Mo (multipart Spring, inchangée), fichier vide refusé ; type détecté par le contenu avec Apache Tika tika-core 4.1.0 (le nom ne sert que d’indice), aucun type refusé à l’upload ; un nœud par fichier (POST /api/nodes/files, sans type ni aspect), rattachement (PUT /api/nodes/{id}/file), retrait, lecture en flux par le nœud (GET /api/nodes/{id}/file) ; liste blanche des types servis inline, tout le reste en attachment, nosniff, CSP sandbox sauf PDF ; événements file.attached / file.replaced / file.removed ; aucune suppression physique avant la purge de la corbeille (jalon 3) ; aperçu PDF par le lecteur natif du navigateur (<iframe>), images par <img>, texte par <pre> ; Office, miniatures, pdf.js et ![[fichier]] reportés ; aucune dépendance npm.
Contexte
Le cahier (jalon 1) demande un « fichier optionnel » par nœud (« Nœud générique »), le dépôt par glisser-déposer avec « création d’un nœud par fichier », l’aperçu des PDF, images, documents Office et textes, le stockage sur disque avec déduplication par hash, l’original jamais modifié, l’export des fichiers d’origine et leur sauvegarde. Le backlog réduit la tranche à : stockage disque (/data), déduplication par hash, file_ref, aperçu PDF et images.
État du code :
node.file_ref textexiste depuis V1 (« référence du fichier stocké (jalon 1 : upload) ») mais n’est ni lu ni écrit (absent deDb).spring.servlet.multipartest déjà réglé à 50 Mo par fichier et 60 Mo par requête.- L’image Docker crée
/data(propriétaireapp) etdocker-compose.ymly monte le volumeapp-data; rien ne l’utilise encore. - Sécurité (ADR 0004) : même origine pour le front et l’API, cookie de session, CSRF par en-tête
X-XSRF-TOKEN. Spring Security envoie par défautX-Frame-Options: DENYetX-Content-Type-Options: nosniff, sur les deux chaînes (devetoidc). - Rendu (ADR 0008) : les images du Markdown n’acceptent que
https:et les URL relatives ; le seulbypassSecurityTrust*autorisé est celui deapp-markdown-view. ![[…]]est préservé mais n’est ni lien ni transclusion (ADR 0009, V2).- Le client HTTP Angular utilise
withFetch().
Le risque principal est la sécurité. Servir depuis l’origine de l’application un fichier fourni par l’utilisateur (HTML, SVG, XHTML) revient à lui faire exécuter du script avec la session : c’est un XSS stocké. Les autres choix difficiles à défaire sont la forme des données (un ou plusieurs fichiers par nœud, nom d’origine porté par le nœud ou par le contenu) et la disposition sur disque, que reprendront sauvegardes, export, purge et versions (V2).
Sonde de détection (Tika tika-core 4.1.0, Tika#detect(InputStream, nom), 2026-10-15) :
| Contenu | Nom fourni | Type détecté |
|---|---|---|
SVG avec <script> |
a.png |
image/svg+xml |
HTML avec <script> |
a.png, a.txt, a.pdf |
text/html |
| XHTML | a.txt |
application/xhtml+xml |
texte avec <script> au milieu |
a.txt |
text/plain |
| PNG | a.html |
image/png |
| octets aléatoires | a.pdf / a.png / aucun |
application/pdf / image/png / application/octet-stream |
| texte | a.pdf |
text/plain |
| ZIP | a.docx / a.png |
type Word (OOXML) / application/zip |
# Titre… |
note.md |
text/markdown |
Les signatures (magic) l’emportent sur l’extension. L’extension n’est retenue que si elle précise le type reconnu par les signatures, ou si aucune signature ne correspond. Un contenu actif ne se fait donc pas passer pour une image ou un PDF. À l’inverse, un contenu illisible peut être étiqueté PDF ou PNG d’après son nom ; avec nosniff, le navigateur échoue simplement à l’afficher. La même sonde montre que tika-core 4.1.0 échoue au chargement avec commons-io 2.21 (NoClassDefFoundError: TaggedInputStream) : il exige la version 2.22.
Décision
Modèle : un fichier par nœud, contenu adressé par son SHA-256
-
Un fichier au plus par nœud au jalon 1, conformément au cahier (« fichier optionnel », « un nœud par fichier »). Pour plusieurs pièces jointes, on crée plusieurs nœuds, enfants (ADR 0007) ou liés. Les versions d’un même fichier relèvent de la V2.
-
Migration
V6__fichiers.sql:-- Fichiers (ADR 0012) : contenus stockés sur disque sous app.files.path, adressés par leur SHA-256. create table file_blob ( sha256 text primary key check (sha256 ~ '^[0-9a-f]{64}$'), size bigint not null check (size >= 0), created_at timestamptz not null default now() ); -- node.file_ref (V1, jamais écrit jusqu'ici) devient la référence au contenu ; restrict : un contenu -- référencé ne peut pas être supprimé (garde-fou de la purge du jalon 3). alter table node add constraint node_file_ref_fk foreign key (file_ref) references file_blob (sha256); alter table node add column file_name text; -- nom d'origine, assaini alter table node add column file_media_type text; -- type détecté par le contenu (Tika) alter table node add constraint node_file_chk check ( (file_ref is null and file_name is null and file_media_type is null) or (file_ref is not null and file_name is not null and file_name <> '' and file_media_type is not null and file_media_type <> '')); create index node_file_ref_idx on node (file_ref) where file_ref is not null;file_blobdécrit le contenu (identique pour tous les nœuds qui le partagent). Le nœud porte ce qui dépend du dépôt : le nom d’origine, et le type détecté, qui dépend aussi du nom (un ZIP nommé.docxest un document Word). La taille est lue par jointure. -
Aucun lien avec le domaine. Le moteur ne connaît ni l’aspect
documentni aucun type. Un nœud créé par dépôt n’a ni type ni aspect : c’est un élément « capturé, à classer », candidat naturel de l’inbox (tranche suivante). L’utilisateur lui ajoute ensuitedocument,facture… comme à tout nœud. Les métadonnées du fichier (nom, type, taille) ne sont pas un aspect : ce sont des colonnes du moteur, commetitle. Le format YAML et les méta-schémas ne changent pas.
Stockage sur disque
- Racine configurable :
app.files.path, valeur par défaut${APP_FILES_PATH:../data/files}(racine du dépôt en développement, comme../config). Valeur/data/filesdans l’image (ENVduDockerfileetdocker-compose.yml)./data/est ajouté au.gitignore. - Chemin d’un contenu :
<racine>/<2 premiers hex>/<2 suivants>/<sha256>, sans extension. Le chemin est calculé uniquement à partir d’un SHA-256 validé (^[0-9a-f]{64}$) : aucune donnée fournie par l’utilisateur, nom compris, n’entre dans un chemin. - Écriture en deux temps :
- mise en attente (hors transaction) : le flux du multipart est copié dans
<racine>/.tmp/<uuid>à travers unDigestInputStreamSHA-256, puisFileChannel.force(true). On obtient le hash et la taille en une seule lecture ; - validation, dans la transaction de la base, après le verrou consultatif partagé
file.gc:insert into file_blob … on conflict do nothing, puisFiles.move(tmp, cible, ATOMIC_MOVE). Si la cible existe déjà avec la même taille, c’est un doublon : le fichier temporaire est supprimé. Si sa taille diffère (fichier abîmé), elle est remplacée. Sous Windows, un renommage refusé alors que la cible existe avec la bonne taille vaut aussi doublon. - le fichier temporaire est supprimé dans un
finallys’il existe encore.
- mise en attente (hors transaction) : le flux du multipart est copié dans
- Les répertoires sont créés à la première écriture, pas au démarrage. Les tests qui n’écrivent pas de fichier ne créent donc rien, et les autres pointent
app.files.pathvers un@TempDir. Au démarrage, les fichiers de.tmpde plus de 24 h sont supprimés et le chemin absolu est journalisé. - Une transaction annulée après le renommage laisse un contenu sans ligne
file_blob. C’est sans danger : un orphelin de plus pour la purge. L’inverse, une ligne sans contenu, est impossible tant que la purge (jalon 3) prend le même verroufile.gcen mode exclusif. - Une erreur d’entrée-sortie du stockage produit une
FileStorageException, traduite en 503 ProblemDetail « Stockage des fichiers indisponible ».
Taille, nom et type
- Taille :
spring.servlet.multipart.max-file-sizereste à 50 Mo (surcharge parSPRING_SERVLET_MULTIPART_MAX_FILE_SIZE). Un dépassement (MaxUploadSizeExceededException) donne 413 avecerrors.file. Un fichier vide donne 400errors.file, car un dossier déposé dans certains navigateurs arrive comme un fichier vide. Une partiefileabsente donne 400errors.file.GET /api/files/limitsrenvoie{ maxFileSize }(octets, lu dansMultipartProperties) pour que le front refuse avant l’envoi : au-delà deserver.tomcat.max-swallow-size(2 Mo), Tomcat coupe la connexion et le navigateur ne voit qu’une erreur réseau, pas le 413. - Nom d’origine (
FileNames.sanitize) : dernier segment après/ou\, NFC, suppression des caractères de contrôle C0 et C1, de U+2028/U+2029, des marques bidirectionnelles (U+200E/F, U+202A–U+202E, U+2066–U+2069) et de U+FEFF,strip(), 255 points de code au plus en conservant l’extension. Vide après assainissement :fichier. - Titre du nœud créé (
FileNames.title) : le nom sans sa dernière extension (point en position > 0), 500 caractères au plus ; vide : le nom entier. - Type :
MediaTypes.detect(Path, String nom)appelle une instance unique deorg.apache.tika.Tikasur les premiers octets du fichier mis en attente, avec le nom assaini en indice. LeContent-Typeenvoyé par le client est ignoré. Aucun type n’est refusé à l’upload : un deuxième cerveau stocke aussi des ZIP, des mails ou des fichiers inconnus. La sécurité se joue à la lecture (ci-dessous).
API
Contrôleur NodeFileController, tag OpenAPI files :
| Méthode | Chemin | Effet | Réponse |
|---|---|---|---|
POST |
/api/nodes/files (multipart : file ; requête : parentId?) |
crée un nœud (titre tiré du nom, sans type) et lui rattache le fichier | 201 NodeView, Location: /api/nodes/{id} |
PUT |
/api/nodes/{id}/file (multipart : file) |
rattache ou remplace ; même contenu et même nom : rien | 200 NodeView |
DELETE |
/api/nodes/{id}/file |
détache (le contenu reste sur disque) | 200 NodeView, 404 sans fichier (comme removeAspect) |
GET |
/api/nodes/{id}/file |
contenu en flux | 200, 206 (Range), 304 (If-None-Match), 404 si nœud absent, en corbeille ou sans fichier |
GET |
/api/files/limits |
{ maxFileSize } |
200 |
- Un fichier par requête de création. Le front envoie les fichiers d’un dépôt en parallèle limité et rend compte de chacun.
POST /api/nodes/filespasse parNodeService.create(même transaction). Ainsi la création rattrape les liens wiki non résolus de son titre (ADR 0009) et vérifie le parent. UnparentIdintrouvable donne 400errors.parentId.NodeViewetNodeSummarygagnentfile(FileView(String name, String mediaType, long size, String sha256)), absent du JSON quand le nœud n’a pas de fichier (@JsonInclude(NON_NULL),requiredMode = NOT_REQUIRED, commeBacklinkView.linkId).GET …/fileest documentéapplication/octet-stream,format: binary(le client généré renvoie unBlob). Il renvoieResponseEntity<Resource>(FileSystemResource), ce qui donne à Spring MVC la prise en charge des requêtesRange, utile au lecteur PDF et aux médias. L’en-têteETag: "<sha256>"est vérifié parWebRequest.checkNotModified.- Le multipart passe par
PUTcomme parPOST(Tomcat lit les parties quelle que soit la méthode). Un test d’intégration le vérifie. Repli si ce n’est pas le cas :POST /api/nodes/{id}/file.
Lecture : en-têtes contre le XSS en même origine
Politique pure FileServingPolicy.of(String mediaType) → Served(MediaType contentType, boolean inline, String csp), testée unitairement :
inlineseulement pour la liste blanche :application/pdf,image/png,image/jpeg,image/gif,image/webp,image/avif,text/plain. Tout le reste est servi enattachment(SVG, HTML, XML, Office, inconnus).Content-Dispositionest construit parContentDispositionde Spring (filename*=UTF-8''…) à partir du nom assaini.Content-Type: le type détecté s’il appartient àimage/*,audio/*,video/*,application/pdfoutext/plain, sinonapplication/octet-stream. On n’envoie donc jamaistext/html,application/xhtml+xml,*/xmlni*/javascript.image/svg+xmlest conservé pour que<img>l’affiche ; une balise<img>n’exécute aucun script et ignoreContent-Disposition.- Sur toutes les réponses :
X-Content-Type-Options: nosniff,Cross-Origin-Resource-Policy: same-originetCache-Control: private, no-cache(revalidation par ETag). Content-Security-Policy:- PDF :
default-src 'none'; object-src 'self'; frame-ancestors 'self'. Pas desandbox, etobject-srcest nécessaire : Chromium n’affiche pas un PDF servi avecsandboxni avecdefault-src 'none'seul ; - autres types :
default-src 'none'; img-src 'self'; media-src 'self'; style-src 'unsafe-inline'; frame-ancestors 'self'; sandbox.
- PDF :
X-Frame-Optionspasse deDENYàSAMEORIGINdans les deux chaînes deSecurityConfig(headers.frameOptions(sameOrigin)), sinon l’aperçu PDF en<iframe>est bloqué. Le clickjacking par une autre origine reste impossible. Amende l’ADR 0004.- CSRF : inchangé. Le jeton est lu dans l’en-tête
X-XSRF-TOKEN, qu’Angular ajoute aux requêtesPOST/PUT/DELETErelatives, multipart compris. Spring Security n’a donc pas à lire le corps : aucunMultipartFilterà placer avant la chaîne. LeGETn’a pas d’effet de bord.
Événements
Dans la transaction d’écriture, sur le modèle des aspects. Charge utile d’un fichier : {sha256, name, size, mediaType}.
file.attached{after}: premier fichier du nœud. Un nœud créé par dépôt produitnode.createdpuisfile.attached.file.replaced{before, after}: contenu ou nom différent.file.removed{before}.- Rattacher le même contenu sous le même nom : ni événement ni mise à jour. Toute autre écriture fait
touchsurupdated_at.
Le déclencheur « fichier ajouté » des règles (jalon 3) correspondra à file.attached et file.replaced. Comme les événements gardent le sha256 des contenus remplacés et que rien n’est supprimé physiquement, l’historique des versions (V2) pourra être reconstitué.
Corbeille, purge, sauvegarde, export
- Mettre un nœud à la corbeille ne touche pas au fichier : la restauration le retrouve.
GET …/filed’un nœud en corbeille donne 404, commeGET /api/nodes/{id}. - Aucune suppression physique au jalon 1. La purge (jalon 3, avec la purge de la corbeille) prendra le verrou
file.gcen mode exclusif. Elle supprimera les lignesfile_blobque plus aucun nœud ne référence (la clé étrangèrerestrictprotège les autres), puis leurs fichiers, puis les contenus présents sur disque sans ligne. Elle devra garder ce que l’historique ou les versions (V2) référencent encore, selon la politique qui sera arrêtée. - Sauvegardes (tranche dédiée) :
pg_dumpd’abord, puis copie de<racine>(sans.tmp/). Les contenus sont immuables et jamais supprimés avant la purge : une copie prise après le dump contient tout ce que le dump référence. La purge devra être exclue de la plage de sauvegarde. La restauration testée vérifiesha256sumsur un échantillon. - Export (tranche dédiée) : le fichier d’origine sous son nom d’origine, à côté du Markdown du nœud. Le
sha256va dans le frontmatter. Un même contenu partagé est copié une fois par nœud.
Front
- Pas de nouvelle dépendance npm. Rien n’est chargé dans le bundle
initialen dehors de la petite directive de dépôt (aucune bibliothèque). core/files/:nodeFileUrl(id)(vérifie le format UUID, renvoie/api/nodes/${id}/file),previewKind(mediaType): 'pdf' | 'image' | 'text' | 'none',formatSize(octets)(fr-FR), etFileUploader(limites mises en cache, contrôle de taille et de fichier vide avant l’envoi, envois en parallèle limités à 3, résultat par fichier).withFetch()ne donne pas la progression de l’envoi : l’indicateur est indéterminé, par fichier.- Directive
appFileDrop: compteurdragenter/dragleave, réagit seulement sidataTransfer.typescontientFiles, ignore les dossiers (webkitGetAsEntry()?.isDirectory). Elle s’accompagne d’un bouton « Importer des fichiers » (<input type="file" multiple>) : sur mobile, le glisser-déposer n’existe pas. - Points de dépôt, un nœud par fichier : accueil et liste des nœuds (à la racine ; la liste se recharge ensuite) ; panneau des enfants de l’éditeur (
parentId= nœud courant). Au plus 100 fichiers par dépôt. Avec un seul fichier déposé depuis l’accueil, on ouvre le nœud créé. - Panneau « Fichier » de l’éditeur (
app-node-file-panel), sur un nœud existant (masqué sur/nodes/new) :- sans fichier : zone de dépôt (un seul fichier) et bouton « Joindre un fichier » ;
- avec fichier : nom, type, taille, boutons « Ouvrir » (nouvel onglet), « Télécharger » (
<a download>), « Remplacer » et « Retirer ». - Comme les panneaux de liens, il écrit immédiatement. Il ne recharge pas le formulaire de l’éditeur, pour ne pas perdre la saisie en cours, et ses boutons sont désactivés pendant la requête.
- Aperçu selon
previewKind:- PDF :
<iframe>sinavigator.pdfViewerEnabled, sinon les seuls boutons (Chrome Android n’a pas de lecteur intégré). L’<iframe>n’a pas d’attributsandbox, qui bloquerait le lecteur ; - image :
<img loading="lazy">, y compris SVG ; - texte :
text/*,application/json, au plus 512 Kio, lu par le client généré (Blob, puisTextDecoder('utf-8')) et affiché dans un<pre>par interpolation, jamais eninnerHTML; - autres types : rien.
- PDF :
- Amende l’ADR 0008 : un second contournement est autorisé,
bypassSecurityTrustResourceUrl, limité ausrcde l’<iframe>PDF. Il ne s’applique qu’à la valeur renvoyée parnodeFileUrl(id), construite à partir d’un UUID validé, jamais d’une chaîne libre. - Une image d’un nœud peut déjà s’insérer dans un corps par URL relative,
, que la politique de l’ADR 0008 accepte. Aucune syntaxe n’est ajoutée :![[fichier]]reste la transclusion de la V2.
Dépendances
org.apache.tika:tika-core4.1.0 (Apache-2.0, publiée le 2026-09-26, Java 17+, environ 770 Ko). Exclusion deorg.commonmark:*: Tika 4 l’utilise pour la sortie Markdown, qui ne sert pas à la détection, et le projet a déjàcommonmark0.30.1. Dépendances restantes :slf4j-api(fourni par Spring Boot) etcommons-io.commons-io:commons-io2.22.0 (Apache-2.0) fixé dansdependencyManagement: Tika 4.1.0 ne fonctionne pas avec 2.21 (voir la sonde), et une dépendance transitive plus proche pourrait imposer une version antérieure. Contrôle :./mvnw -pl backend dependency:tree -Dincludes=commons-io,org.commonmark.- Choisir Tika aligne la détection sur l’extraction du texte des PDF (cahier, jalon 3), qui ajoutera les parseurs de la même version.
Alternatives écartées
- Plusieurs fichiers par nœud dès le jalon 1 (table
node_file, ordre, pièces jointes) : le cahier parle d’« un » fichier optionnel et d’« un nœud par fichier ». Les pièces multiples s’expriment par la hiérarchie et les liens, qui existent déjà. Ajouter une table plus tard coûte peu ; en retirer une, après usage, coûte cher. - Nom d’origine et type dans
file_blob: deux dépôts du même contenu sous deux noms se marcheraient dessus, et le type dépend du nom (ZIP ou docx). - Pas de table
file_blob(tout surnode) : plus de clé étrangère pour protéger un contenu référencé, et la purge devrait parcourir le disque pour savoir ce qui existe. - Identifiant UUID pour le contenu au lieu du hash : la déduplication demanderait un index unique de plus, et le chemin sur disque ne permettrait plus de vérifier l’intégrité.
- Chemin avec extension ou nom d’origine : il faudrait se protéger de la traversée de répertoires et des noms Windows réservés, et le serveur de fichiers d’un reverse proxy pourrait se mettre à « deviner » le type.
- Stockage en base (
bytea, large objects) : dumps volumineux et lents, mémoire, et le cahier prévoit S3/MinIO. Le disque est l’option la plus simple ; une interfaceFileStorepermettra d’ajouter S3 plus tard. - S3/MinIO dès le jalon 1 : un service de plus à déployer pour un usage personnel.
- Liste blanche de types à l’upload : refuser un ZIP ou un
.emlcontredit la capture sans friction, et la sécurité se règle mieux à la lecture. - Détection maison (une dizaine de signatures) : plus légère, mais il faudrait maintenir à la main ce que Tika fait déjà et qu’il faudra de toute façon ajouter au jalon 3.
- Tika 3.3.3 (dépendances plus légères, sans commonmark) : branche de maintenance ; le jalon 3 aurait dû migrer vers la version 4.
- Confiance dans le
Content-Typedu client : ce n’est qu’une déclaration, choisie par l’expéditeur. - Origine séparée pour les fichiers (
files.example.com, comme leusercontentde Google) : c’est la défense la plus robuste contre le XSS par fichier. Elle demande un second nom de domaine, des cookies ou jetons distincts et du CORS, contre le modèle même origine de l’ADR 0004. On la garde en réserve si un type serviinlinepose un jour problème. GET /api/files/{sha256}(lecture par contenu) : le nom d’origine appartient au nœud, et passer par le nœud règle l’accès (corbeille) et l’autorisation future (multi-utilisateur) en un seul endroit.- pdf.js (
pdfjs-dist, Apache-2.0) : rendu identique partout, mobile compris. Mais plusieurs mégaoctets, avec le worker, qu’il faut charger et mettre à jour, alors que les lecteurs natifs couvrent les postes de travail. On le réévaluera avec la PWA (jalon 2) si l’aperçu sur mobile manque. - Miniatures et aperçus générés à l’upload (PDFBox, Thumbnailator) : inutiles tant qu’il n’y a ni galerie (« Plus tard ») ni vignette dans la liste. Les générer plus tard, en lot, ne coûte qu’un traitement.
- Conversion Office (LibreOffice headless, Gotenberg) : un conteneur de plusieurs centaines de mégaoctets et une surface d’attaque (fichiers non fiables) importante, pour un besoin secondaire.
- Taille de fichier ou nom ajoutés à
node.search: changer l’expression de la colonne générée impose de la recréer avec son index. Le titre reprend déjà le nom ; le contenu des PDF viendra avec Tika au jalon 3.
Conséquences
- Un fichier déposé devient un nœud complet : titre, liens wiki, tags, hiérarchie, corbeille, historique. Le moteur reste générique : aucun aspect ni type n’est implicite.
node.file_ref(V1) prend son sens. Une valeur non nulle déjà présente ferait échouerV6(clé étrangère) ; aucune n’a jamais été écrite.- Le disque grossit sans jamais rétrécir avant le jalon 3. Rien ne se perd, mais il n’y a pas de quota : à surveiller dans la tranche déploiement (alerte d’espace libre).
- Le reverse proxy (Caddy, tranche déploiement) ne doit pas limiter le corps des requêtes en dessous de 60 Mo. La future CSP de l’application devra autoriser
frame-src 'self'etimg-src 'self'. - Aperçus non uniformes : un PDF sur Chrome Android ou une photo HEIC d’iPhone (non affichable hors Safari) n’ont que les boutons Ouvrir et Télécharger.
- Le texte s’affiche toujours en UTF-8 dans l’application. Ouvert dans un onglet, un
text/plainsanscharsetest décodé selon la supposition du navigateur. - Un PDF est servi
inlinesanssandbox. Le JavaScript d’un PDF s’exécute dans le lecteur du navigateur, isolé de la page (PDFium, pdf.js), mais à la même origine. C’est un risque résiduel accepté pour un usage personnel ; l’origine séparée en est le remède. - Les métadonnées des fichiers (EXIF, géolocalisation des photos) sont conservées : l’original est intact. À traiter au partage public (« Plus tard »).
- Les fichiers ne sont pas chiffrés au repos par l’application : chiffrer le disque de l’hôte (cahier : chiffrement au repos hors MVP).
- Le contrat OpenAPI gagne du multipart et du binaire. Le client généré envoie un
Blob: il faut passer leFilelui-même, sans le recopier, pour garder son nom dans leFormData, et un test le vérifie.
Reporté
- Miniatures et aperçus générés (cahier « Rendus », MVP sans jalon explicite) : avec la galerie ou une colonne vignette.
- Aperçu Office par conversion : V2, ou au jalon 3 avec Tika si l’extraction de texte suffit.
- pdf.js pour le mobile : avec la PWA (jalon 2), si le besoin est confirmé.
- Extraction du texte des PDF pour la recherche (jalon 3, Tika), OCR (V2).
- Purge des contenus orphelins (jalon 3, avec la corbeille), versions de fichier (V2), dossier surveillé et partage mobile (V2).
![[fichier]](transclusion, V2) ; collage d’une image dans l’éditeur qui crée un nœud et insère son URL relative.- Détection et signalement des doublons au dépôt (même
sha256déjà présent sur un autre nœud). - Stockage S3/MinIO.
Questions ouvertes tranchées (mode autonome)
- Un ou plusieurs fichiers par nœud : un.
- Rattachement automatique de l’aspect
document: non, le moteur reste générique. Le classement viendra des règles (jalon 3) ou du classement automatique (V2). - Taille maximale : 50 Mo (inchangée), configurable par variable d’environnement.
- Types refusés à l’upload : aucun ; liste blanche à la lecture.
- Création en lot : un fichier par requête, en parallèle limité côté front.
- Aperçu PDF : natif ; images :
<img>; texte : inclus (peu coûteux) ; Office : reporté. - Suppression physique : aucune avant le jalon 3.
Questions ouvertes tranchées (mode autonome)
- Miniatures générées et aperçu Office par conversion : reportés après le jalon 1 (lignes ajoutées au backlog).
- Aperçu PDF sur mobile (pdf.js) : non au jalon 1 ; boutons Ouvrir / Télécharger en repli.
- Doublons au dépôt : pas de signalement au jalon 1 (le contenu est dédupliqué sur disque, un second nœud est créé).
- Aucun type ni aspect posé par défaut sur un nœud déposé (pas de changement du méta-schéma).
- Taille maximale : 50 Mo, surchargeable par variable d’environnement.