Skip to main content
Le drive est la surface du draft JMAP File Storage (des objets FileNode au-dessus de la mécanique de blobs standard) : un arbre de fichiers et de répertoires par compte, partageable par nœud, avec des liens de téléchargement anonymes comme objet distinct.

Objets et méthodes

Le contenu passe par les points d’accès de blobs JMAP standard (/jmap/upload, /jmap/download) : une création de FileNode référence un blobId téléversé. FileNode déclare son vocabulaire d’écriture au répartiteur : une propriété qu’un client ne peut pas écrire est refusée nommément, jamais abandonnée en silence.

Le téléchargement

Un blob est immuable par construction (son identifiant est le SHA-256 du contenu), et la porte de téléchargement le dit. /jmap/download répond ETag: "<blobId>" et Cache-Control: private, max-age=31536000, immutable (privé : la ressource est authentifiée, jamais pour un cache partagé), et un If-None-Match qui porte le bon tag reçoit 304 sans corps. Une requête Range: bytes= unique est honorée, 206 avec Content-Range, 416 si elle n’est pas satisfiable ; une spécification malformée ou multi-plages est ignorée et le 200 complet servi, comme la RFC 9110 le permet. Depuis ADR-145 le corps est lu en flux depuis le fichier lui-même, une fenêtre en mémoire à la fois, si bien qu’un gros fichier ne coûte au processus pas plus qu’un petit. La porte de partage publique (/share/{token}) honore le même Range mais garde Cache-Control: no-store : sa réponse compte dans un quota de téléchargements et ne doit jamais être resservie par un cache sans passer par le compteur.

L’historique de versions

Chaque mise à jour du contenu d’un nœud photographie le blob précédent dans son historique de versions, dans la même transaction que la mise à jour. Le contenu courant vit toujours sur le nœud lui-même ; FileVersion n’est que de l’histoire, et chacune de ses propriétés est immuable : La capacité urn:ietf:params:jmap:files annonce maxVersionsPerNode : la valeur effective du locataire, jamais une constante. C’est à la fois la fenêtre de conservation et la borne d’élagage : les versions au-delà des N plus récentes sont élaguées dans la transaction même qui a créé la plus récente, et leurs blobs sont libérés par le chemin de destruction à compteur de références, jamais par une suppression directe, puisqu’un blob peut être partagé par déduplication. Un client doit conditionner son entrée de menu « historique de versions » à la présence de cette clé, plutôt que de supposer que la surface existe. La rétention est un réglage du locataire, appliqué à chaud : oximail tenant update --max-file-versions N (ou --clear-max-file-versions), 10 par défaut, plafond d’instance 100, et 0 désactive l’historique, ce qui court-circuite l’instantané au lieu de l’insérer puis de l’élaguer dans la même transaction, et libère les versions existantes au prochain enregistrement. Tenant/get sert maxFileVersionsPerNode comme valeur stockée (null pour le défaut, pour qu’une console distingue « défaut » de « réglé à 10 »), Tenant/set l’accepte, et un changement fait bouger sessionState, si bien qu’un client ouvert relit la session au lieu de garder l’ancienne annonce jusqu’à sa reconnexion. L’effet tombe au prochain enregistrement de chaque fichier ; il n’y a pas de verbe d’élagage immédiat.
  • FileVersion/query exige un filtre fileNodeId. Les versions n’ont pas de sens en liste à l’échelle du compte, et exiger ce point d’ancrage garde la surface bornée par le plafond de conservation. Les plus récentes d’abord par défaut. Son queryState est l’état de la collection FileNode : les versions ne changent que quand le contenu d’un nœud change, ce qui fait avancer cet état, donc un seul curseur est cohérent pour les deux. canCalculateChanges vaut false : il n’y a pas de journal de changements distinct pour les versions, donc re-interrogez sur un changement de FileNode.
  • FileVersion/restore ne détruit jamais l’histoire. L’opération passe par le même point de passage de stockage qu’une mise à jour de contenu FileNode/set { blobId } : le contenu sur lequel vous êtes est donc photographié comme nouvelle version avant d’être remplacé. La version d’où vous venez reste atteignable, et l’élagage s’applique comme d’habitude. size et contentType sont restaurés depuis l’enregistrement de version, qui fait foi puisqu’il les a capturés au moment de la photographie. Restaurer la version dont le blob est déjà le courant ne change rien au contenu, mais répond quand même en succès. La méthode accepte un ifInState optionnel, pour le même contrôle de concurrence optimiste que FileNode/set, vérifié atomiquement avec l’écriture.
  • Les droits suivent le nœud. Lire la liste des versions demande mayRead sur le nœud propriétaire, restore demande mayWrite (un montage partagé en lecture seule répond forbidden). Un nœud qui n’existe pas et un nœud qu’un appelant partagé n’a pas le droit de lire répondent avec un libellé identique : la surface des versions n’est pas un oracle d’existence.

Les verrous : qui tient un fichier ouvert

Un fichier peut être tenu ouvert par un protocole qui a un « open », branché dans le même binaire par la couture du serveur (un moteur SMB, par exemple). La règle : le protocole qui a un « open » tient ; celui qui n’en a pas consulte. Le registre vit en mémoire, dans le processus, donc un verrou meurt avec la connexion ou le binaire, sans bail ni balayage. JMAP ne tient jamais rien : FileNode/set refuse forbidden toute mise à jour, renommage, mise à la corbeille ou destruction d’un nœud tenu, et FileVersion/restore refuse d’en remplacer le contenu, en nommant le porteur et l’âge du verrou (locked by alice@x.test via SMB (12 min), and N other open(s) s’ils sont plusieurs). FileNode/get sert lockedBy (String | null, l’étiquette et le protocole du porteur le plus ancien), lu en direct et hors de l’état /changes : un indicateur que le client affiche, la garantie étant le refus à l’écriture. Le grain est le nœud, pas la plage d’octets, puisqu’un rédacteur JMAP remplace le blob entier. Sans un tel protocole le registre est vide et la propriété vaut toujours null.

Les comptes miroirs

Un compte miroir est un compte partagé dont le Drive montre, en lecture seule, les dossiers qu’une organisation appairée partage avec ce serveur (fédération). Un client voit un compte partagé ordinaire et les méthodes FileNode ordinaires ; ce qui change, c’est où vit l’arborescence et qui peut l’écrire.
  • L’arborescence est tenue en mémoire, copiée depuis le serveur du propriétaire et maintenue à [federation] mirror_staleness_secs d’âge au plus (120 s par défaut) ; rien du propriétaire n’est stocké sur ce serveur. L’état FileNode/changes d’un miroir vaut <époque>.<séquence> : un redémarrage crée une nouvelle époque, si bien qu’un état antérieur répond cannotCalculateChanges et que le client se resynchronise. FileNode/queryChanges répond toujours cannotCalculateChanges : un miroir ne garde pas de journal de requêtes, le client relance sa requête.
  • Lecture seule pour tout appelant. FileNode/set sur un compte miroir est refusé avec forbidden, dont la description commence par mirror-read-only, avant toute lecture ; myRights n’accorde que la lecture.
  • Un état nommé, jamais un dossier vide. Tant que la copie est en cours, le propriétaire injoignable, l’accès révoqué ou l’appairage inactif, la liste porte un fichier dont le nom le dit ((copy in progress from partner.example), …) à la place de l’arborescence.
  • Le téléchargement lit chez le propriétaire. /jmap/download sert un fichier miroir en lisant son contenu sur le serveur du propriétaire, derrière un court cache en mémoire. Un propriétaire injoignable répond 503 avec type: "ownerUnreachable", un refus du propriétaire 502 avec type: "ownerRefused" ; Range et If-None-Match se comportent comme plus haut. Un fichier miroir n’a pas de vignette (404, type: "noRendition").

Comportements à connaître

  • type est respecté. type: "directory" crée un répertoire ; le champ est validé, jamais deviné d’après la présence d’un blob.
  • Le nom implique le type. Un fichier créé sans contentType reçoit le type MIME que son extension implique, par la même table que les autres portes ; un type explicite gagne toujours, et un nom qui ne dit rien laisse null plutôt qu’un application/octet-stream deviné.
  • ids: null est borné. FileNode/get sans identifiants rend chaque nœud tant que leur nombre, compté sur ce que l’appelant recevrait, ne dépasse pas maxObjectsInGet ; au-delà, l’appel répond requestTooLarge (RFC 8620 §5.1), comme Email/get, et un client pagine avec FileNode/query (un filtre parentId, par exemple) puis FileNode/get sur ses identifiants. Les lectures qu’il y a derrière procèdent par lots de 500, si bien qu’un Drive de dizaines de milliers de nœuds ne tombe plus sur une limite de paramètres de la base.
  • Un nom que Windows réserve à un périphérique est refusé. CON, PRN, AUX, NUL, COM0 à COM9 et LPT0 à LPT9, quelle que soit la casse et avec ou sans extension (aux.txt, nul.tar.gz), sont refusés en invalidProperties à la création et au renommage : servi tel quel sur un partage de fichiers, un tel nœud ne pouvait être ni ouvert, ni renommé, ni supprimé depuis l’Explorateur Windows. Les autres caractères que Windows refuse (< > : " | ? *, un point ou une espace final) restent permis ; les traduire revient à la face de partage de fichiers.
  • Les attributs Windows sont conservés. Un protocole de partage de fichiers branché dans le binaire (un moteur SMB, par exemple) peut déclarer lecture seule, caché, système et archive sur un nœud ; le serveur les garde tels que déclarés, hors du fil JMAP (une notion Windows dont aucun client JMAP n’a besoin), et n’en applique aucun lui-même. Tout changement du contenu d’un fichier, par n’importe quelle porte (FileNode/set, une version restaurée, un partage de fichiers), repose l’attribut archive, pour que la prochaine sauvegarde incrémentale d’un outil Windows reprenne le fichier.
  • L’arbre a une racine ; le fil utilise null. Chaque compte a une racine de stockage ; parentId: null dans une requête signifie « premier niveau ». Les déplacements de répertoires sont vérifiés contre les cycles : un déplacement qui créerait un cycle de parenté est rejeté.
  • La corbeille voyage dans la même transaction. Une mise à jour d’isTrashed est atomique avec le reste de la mise à jour (la mise à la corbeille récursive d’un répertoire est une seule transaction), et les nœuds à la corbeille se restaurent depuis la corbeille visible (ADR-035). Le balayage des corbeilles détruit ce qui est à la corbeille depuis plus longtemps que l’horizon du locataire, sa surcharge drive.trash_retention_days ou à défaut [drive] trash_retention_days (30 par défaut, plancher 1 ; une surcharge à 0 ne purge rien, voir les horizons par locataire), par la même sortie qu’oximail drive purge-trash : un compte dont un message est sous séquestre garde aussi ses fichiers, et chaque nœud meurt dans une transaction (blob libéré par l’outbox de destruction, ligne, versions, liens de partage) avec un changement FileNode Destroyed annoncé, si bien que la corbeille d’un client synchronisé ne garde jamais ce que le serveur a retiré (stockage).
  • Les favoris sont par lecteur. Mettre en favori un fichier partagé marque votre vue ; l’objet du propriétaire n’est pas touché.
  • Un droit absent vaut « non ». Un nœud partagé {mayRead, mayWrite} ne donne aucun mayDelete : une clé absente d’un grant stocké se lit fausse, pour chaque bit, dans chaque vocabulaire. La règle de lecture qui remontait les partages en écriture sans la clé laissait un bénéficiaire mettre à la corbeille la racine même du partage, dans le drive du propriétaire. Les droits sur un nœud s’accordent aussi depuis la CLI, oximail drive share --path Partages/Scan --grantee-email … --rights mayRead,mayWrite, par la même chaîne de validation que FileNode/set et avec la même ShareNotification au bénéficiaire ; cette notification porte objectSubtype (directory ou file), si bien qu’un partage retiré, dont le bénéficiaire ne peut plus résoudre le nœud, est encore nommé pour ce qu’il était (CLI).
  • Le quota est débité de la croissance nette exacte. Une mise à jour de contenu est débitée de la nouvelle taille, plus la version stockée de l’ancien contenu, moins ce que la rétention élague : mesuré avant et après les écritures, et une croissance qui franchit le quota du Drive est refusée overQuota avec l’objet entier annulé. Un patch qui ne fait pas grossir le Drive (un fichier plus petit, un renommage) n’est jamais refusé ; un nouveau fichier est refusé avant que sa ligne existe (ADR-151, quotas).
  • Les vignettes sont dérivées côté serveur pour les types prévisualisables ; les clients n’envoient pas les leurs.
  • Les liens de partage sont bornés et comptés. Un FileShareLink donne accès à exactement le nœud qu’il nomme, applique maxDownloads au téléchargement près, et un échec après que le compteur a bougé (un blob manquant, une plage non satisfiable) rend l’unité, si bien qu’une sonde ne brûle jamais le quota du destinataire. Un lien sur la racine interne du compte répond comme un nœud inexistant, et un jeton malformé alimente fail2ban : le générateur n’a jamais frappé que 32 caractères alphanumériques, tout le reste est une signature de sondage. Le blob servi est celui que le nœud référence, résolu dans une seule jointure avec le lien ; un lecteur anonyme n’atteint rien d’autre.
La déduplication des blobs est par organisation et adressée par contenu : téléverser deux fois le même fichier ne le stocke qu’une fois. Les règles de rigueur sont identiques à la surface courrier.