Modèle de données
Les onze tables fraza_drive_*, la table unique des dossiers et fichiers, et les choix de schéma.
Onze tables, toutes préfixées fraza_drive_. Elles sont créées par npx fraza-db init et supprimées par npx fraza-db drop.
fraza_drive_space
Les espaces. Un seul de type shared, un par utilisateur de type personal.
| Colonne | Type | Note |
|---|---|---|
id | text | Clé primaire. |
type | text | shared ou personal. |
name | text | |
owner_id | text | Vers fraza_auth_users(id). NULL pour l'espace partagé, obligatoire pour un espace personnel. |
used_bytes | bigint | Compteur incrémental, mis à jour dans la même transaction que l'écriture. |
Aucune colonne de quota : le plafond d'un espace vient de l'environnement, et il est posé sur l'espace à la lecture. Une valeur écrite ici survivrait au changement de variable.
L'unicité passe par deux index partiels : un seul espace shared, un seul personal par propriétaire. Une contrainte UNIQUE classique ne sait pas exprimer une condition sur une autre colonne.
fraza_drive_node
Dossiers et fichiers dans la même table, discriminés par kind. C'est le choix structurant du module : une seule hiérarchie, une seule cible de partage, une seule corbeille, un seul journal.
| Colonne | Type | Note |
|---|---|---|
id | text | Clé primaire. |
space_id | text | Vers fraza_drive_space, en cascade. |
parent_id | text | Auto-référence, en cascade. NULL à la racine. |
kind | text | folder ou file. |
name | text | Affiché tel quel : accents, espaces et majuscules conservés. |
mime_type, size_bytes | Fichiers seulement. | |
storage_key | text | Clé de la version courante. Unique, jamais exposée. |
thumbnail_key | text | Miniature webp, quand elle existe. |
version_no | integer | Numéro de la version courante. |
created_by, updated_by, deleted_by | text | Vers fraza_auth_users(id), mis à NULL si le compte disparaît. |
deleted_at | timestamptz | Non nul quand l'élément est à la corbeille. |
trashed_root | boolean | Voir plus bas. |
Trois contraintes de forme : un nœud n'est pas son propre parent, un fichier a toujours une clé de stockage, un dossier n'en a jamais.
Unicité du nom
CREATE UNIQUE INDEX fraza_drive_node_name_unique
ON fraza_drive_node (space_id, COALESCE(parent_id, ''), lower(name))
WHERE deleted_at IS NULL;Deux détails comptent. Le COALESCE est indispensable : en Postgres, NULL est distinct de lui-même dans une contrainte UNIQUE, donc la racine y échapperait. Et la clause WHERE laisse coexister un fichier en corbeille et un nouveau fichier du même nom, ce qui évite de refuser un dépôt à cause d'un élément que l'utilisateur croit supprimé.
Ordre des noms
CREATE COLLATION fraza_drive_natural (provider = icu, locale = 'und-u-kn-true');Tout tri par nom passe par cette collation : ORDER BY name COLLATE fraza_drive_natural. Elle compare une suite de chiffres comme un nombre, donc « 2 » se range avant « 10 », et elle classe les lettres sans égard à la casse ni aux accents.
Elle ne sert qu'à l'ordre d'affichage. L'unicité et la recherche restent sur lower(name), et la colonne name garde la collation de la base.
trashed_root
Marque le seul nœud sur lequel l'utilisateur a réellement cliqué pour supprimer. Tout son sous-arbre porte la même deleted_at, mais lui seul porte ce drapeau.
Sans lui, un dossier supprimé apparaîtrait dans la corbeille avec chacun de ses cent fichiers, et les restaurer un par un serait le seul recours. Avec lui, la corbeille n'affiche que les racines, la restauration remonte le sous-arbre d'un bloc, et la rétention se compte une seule fois.
fraza_drive_version
Une ligne par dépôt, y compris le premier. fraza_drive_node.storage_key reflète la version courante, qui a toujours sa ligne ici.
La rétention des N dernières versions se réduit donc à supprimer les lignes au delà de N en excluant la courante. Une restauration ne recopie pas l'objet : deux versions peuvent partager une clé, ce dont la purge tient compte en dédoublonnant avant de supprimer.
fraza_drive_share
Les liens publics. Porte le jeton, le mode, les quatre droits, le condensat du mot de passe, l'expiration, le plafond de téléchargements, les adresses de notification et la cadence.
revoked_at marque une révocation plutôt que de supprimer la ligne : l'historique continue de nommer le lien.
fraza_drive_share_session
Une session ouverte après saisie du mot de passe. Une table plutôt qu'un témoin signé : cela ne dépend d'aucun secret d'environnement, et cela se révoque. Modifier le mot de passe d'un lien supprime ses sessions.
fraza_drive_share_user
Les partages internes : un élément, un utilisateur, quatre droits. Unique sur le couple.
fraza_drive_activity
Le journal. node_id et share_id sont sans clé étrangère, et le nom, le chemin et le libellé de l'acteur sont dénormalisés au moment de l'événement.
C'est délibéré : l'historique doit rester lisible après la purge du fichier ou la suppression du partage. Une clé étrangère effacerait ou bloquerait exactement ce qu'on veut conserver.
ip_hash porte un condensat de l'adresse d'un visiteur, jamais l'adresse.
Six index couvrent les filtres de la page d'historique, un par critère.
fraza_drive_notification
La file des avis de dépôt. Une ligne par fichier reçu sur un partage qui notifie, marquée envoyée seulement après un envoi réussi. Un échec la laisse en attente, donc la tâche suivante réessaie.
fraza_drive_upload
Les sessions d'envoi par tranches. Une ligne est écrite à l'ouverture de la session, avant même que le stockage n'ouvre le téléversement en plusieurs parties, et passe à finalized ou abandoned selon l'issue.
Cet ordre d'écriture est délibéré : un incident entre les deux laisse une ligne sans rien dans le seau, ce que le balayage résout sans frais. L'ordre inverse laisserait des parts facturées que plus rien ne référence.
chunk_size et part_count sont figés à l'ouverture : le stockage exige que toutes les parts sauf la dernière fassent la même taille, donc une reprise doit reprendre le découpage d'origine. replace_existing retient si le dépôt doit créer une version plutôt qu'un doublon ; les partages ne le posent jamais.
expires_at est repoussé à chaque tranche acceptée. Il borne donc une inactivité, pas la durée totale d'un envoi : un fichier de deux gigaoctets sur une ligne lente ne doit pas expirer en plein vol. Une ligne restée pending après expiration signale un téléversement laissé ouvert, dont le balayage horaire libère les parts.
fraza_drive_upload_part
Une ligne par tranche reçue, avec son etag, celui que le stockage exige pour l'assemblage.
Une table plutôt qu'une colonne jsonb sur la session, et la raison vaut d'être dite : le navigateur envoie plusieurs tranches en parallèle. Un tableau jsonb imposerait une lecture puis une écriture, donc un verrou de ligne, donc la sérialisation de ce qu'on venait de paralléliser. Une ligne par tranche avec ON CONFLICT s'écrit sans se gêner, et SUM(size_bytes) donne le total reçu sans avoir à faire confiance à un compteur.
Les lignes disparaissent à l'assemblage : dix mille tranches par fichier terminé ne méritent pas d'être conservées.
fraza_drive_job_lock
Le verrou des tâches planifiées, à durée de vie plutôt qu'à libération obligatoire : une instance qui tombe en plein traitement ne bloque pas la tâche indéfiniment. Porte aussi le résultat de la dernière exécution, affiché dans l'écran de configuration.
Migrations
| Identifiant | Effet |
|---|---|
001-multipart-uploads | Colonnes de session sur fraza_drive_upload, table fraza_drive_upload_part, et expiration des lignes héritées du protocole d'URL signée. |
002-space-quota-from-env | Suppression de la colonne quota_bytes de fraza_drive_space : le quota d'un espace vient de l'environnement. |
003-natural-name-collation | Création de la collation fraza_drive_natural. S'arrête avec un message explicite si PostgreSQL a été compilé sans ICU. |
Lancez npx fraza-db migrate après chaque mise à jour du paquet. Voir Écrire une migration.