gestion_table_tournante_libre/src/stockage/systeme_fichiers.js

177 lines
9.9 KiB
JavaScript
Raw Normal View History

// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
// Le système de fichiers, frontière de plateforme du stockage (§ 13.4). Ce
// module en déclare l'interface, que trois implémentations portent : celle de
// la coquille electron, livrée ; celle de la plateforme web, sur l'OPFS ; celle
// d'épreuve, en mémoire, qui sait tomber en panne (test/fichiers_simules.js).
// Toute fonction du stockage qui touche un fichier reçoit un SystemeFichiers
// en paramètre (§ 14.10) ; la suite de contrat test/contrat_fichiers.js
// éprouve les trois implémentations par les mêmes épreuves.
//
// Une primitive ne reçoit jamais de chemin absolu : une racine, connue par son
// identifiant, et un chemin relatif à elle, aux segments séparés par « / »,
// sans « .. » ni chemin absolu ; '' désigne la racine elle-même. Le chemin
// d'une racine sert à l'affichage, jamais à résoudre un fichier. Toutes les
// primitives sont asynchrones. Un échec lève une ErreurStockage :
//
// ABSENT {chemin} lecture d'un fichier, ou liste
// d'un dossier, qui n'existe pas
// ECRITURE {chemin, dossier, cause} une écriture, un ajout, un
// renommage refusés ; la cible est
// intacte. chemin est le chemin
// relatif reçu, dossier le chemin
// affichable du dossier qui le
// porte — la racine elle-même pour
// '' —, cause celle que rapporte
// la plateforme (EBUSY, EROFS…)
// EXISTE {chemin} deplacer vers une cible existante
// CHEMIN_REFUSE {chemin} chemin absolu ou remontant, racine
// inconnue
[FIX] storage, session: strict UTF-8, lost journal, lock, exclusions The whole-iteration review found ways to lose or corrupt data. A state saved in another encoding opened with its accents turned to U+FFFD and was then overwritten; decoding is now strict, the state opens read-only, and a gesture from its secours first moves it to the trash. A journal decodes line by line, so one bad byte drops only the lines after it. A vanished journal is rewritten whole, never appended to. Deleting refuses an event another session holds; switching to writing rereads the disk; an import that changes exclu says which reservations it suspends. Checked: 1504 node, 35 browser, 29 node-long; depot.js, journal.js 100 %. --- FR --- [FIX] stockage, séance : UTF-8 strict, journal perdu, verrou, exclusions La revue d'ensemble a trouvé des façons de perdre ou d'abîmer des données. Un état enregistré dans un autre encodage s'ouvrait, ses accents changés en U+FFFD, puis s'écrasait ; le décodage est strict, l'état s'ouvre en lecture seule, et un geste depuis son secours le range d'abord à la corbeille. Le journal se décode ligne par ligne : un octet fautif n'écarte que la suite. Un journal disparu se réécrit en entier, jamais par ajout. Supprimer refuse un événement qu'une autre séance tient ; passer en écriture relit le disque ; un import qui change exclu dit quelles réservations il suspend. Vérifié : 1504 node, 35 navigateur, 29 node-long ; depot.js, journal.js 100 %. Assisted-by: Claude Opus 5.5
2026-10-07 01:20:42 -04:00
// LECTURE {chemin, dossier, cause} une lecture refusée autrement que
// sur un absent ; cause
// UTF8_INVALIDE pour un fichier dont
// les octets ne sont pas de l'UTF-8,
// que lireTexte ne lit jamais en
// U+FFFD ; les détails portent
// alors aussi octets, une copie
// des octets du fichier
//
// types.js donne la table entière des codes du stockage.
import { ErreurStockage } from './erreurs.js';
/**
* Une racine : un dossier que l'implémentation connaît par son identifiant
* — portable, documents, ou un dossier choisi.
*
* @typedef {Object} Racine
* @property {string} id
* @property {string} chemin chemin absolu, affichable
*/
/**
* @typedef {Object} Emplacements
* @property {string|null} executable dossier publié par le lanceur portable
* @property {string[]} donneesApplicatives AppData, LocalAppData, temporaire, en absolu
* @property {boolean} insensibleCasse les chemins se comparent sans égard à la casse
* @property {'\\'|'/'} separateur
*/
/**
* Une entrée d'un dossier.
*
* @typedef {Object} Entree
* @property {string} nom
* @property {'fichier'|'dossier'} type
* @property {number} taille octets du fichier en UTF-8 ; 0 pour un dossier
* @property {number} modifie millisecondes, pour l'affichage seulement :
* aucune décision ne s'y fonde, puisque recopier un dossier suffit
* à les fausser (§ 14.10)
*/
/**
* Ce que rend verrouiller. pris : true quand l'appel a créé le verrou. Sinon,
* la séance qui le tient, l'instant de sa prise, et vivant : vrai quand son
* processus tourne sur la même machine, faux quand il y est mort, null quand
* il tourne sur une autre machine. Un verrou qui ne se lit pas rend seance,
* depuis et vivant à null.
*
* @typedef {{pris: true}
* | {pris: false, seance: string|null, depuis: string|null, vivant: boolean|null}} Verrou
*/
/**
* @typedef {Object} SystemeFichiers
* @property {'electron'|'web'|'epreuve'} nature
* @property {boolean} renommageAtomique faux sous web (§ 8.8)
* @property {boolean} verrouDisponible faux sous web (§ 8.8) : verrouiller
* rend alors { pris: true } à chaque appel, et deverrouiller n'efface rien
* @property {() => Promise<Emplacements>} emplacements
* @property {() => Promise<{portable: Racine|null, documents: Racine}>} racines
* portable : data/ à côté de l'exécutable publié ; documents : le
* dossier du produit dans les Documents (§ 8.6)
* @property {() => Promise<Racine|null>} choisirDossier dialogue natif ;
* null quand l'opérateur annule
* @property {(r: Racine) => Promise<{inscriptible: boolean, cause: string|null}>} sonder
* crée la racine, parents compris, efface un .gtt-temoin resté
* d'une séance précédente, écrit .gtt-temoin, le relit, l'efface
* (§ 8.6) ; un échec rend sa cause, sans lever
* @property {(r: Racine) => Promise<'amovible'|'fixe'|'inconnu'>} typeSupport
* @property {(r: Racine, chemin: string) => Promise<string>} lireTexte
* le texte tel qu'écrit : marque d'ordre d'octets, fins de ligne et
* forme de normalisation comprises ; ABSENT pour un fichier absent
[FIX] storage, session: strict UTF-8, lost journal, lock, exclusions The whole-iteration review found ways to lose or corrupt data. A state saved in another encoding opened with its accents turned to U+FFFD and was then overwritten; decoding is now strict, the state opens read-only, and a gesture from its secours first moves it to the trash. A journal decodes line by line, so one bad byte drops only the lines after it. A vanished journal is rewritten whole, never appended to. Deleting refuses an event another session holds; switching to writing rereads the disk; an import that changes exclu says which reservations it suspends. Checked: 1504 node, 35 browser, 29 node-long; depot.js, journal.js 100 %. --- FR --- [FIX] stockage, séance : UTF-8 strict, journal perdu, verrou, exclusions La revue d'ensemble a trouvé des façons de perdre ou d'abîmer des données. Un état enregistré dans un autre encodage s'ouvrait, ses accents changés en U+FFFD, puis s'écrasait ; le décodage est strict, l'état s'ouvre en lecture seule, et un geste depuis son secours le range d'abord à la corbeille. Le journal se décode ligne par ligne : un octet fautif n'écarte que la suite. Un journal disparu se réécrit en entier, jamais par ajout. Supprimer refuse un événement qu'une autre séance tient ; passer en écriture relit le disque ; un import qui change exclu dit quelles réservations il suspend. Vérifié : 1504 node, 35 navigateur, 29 node-long ; depot.js, journal.js 100 %. Assisted-by: Claude Opus 5.5
2026-10-07 01:20:42 -04:00
* ou un dossier. Le décodage est strict : des octets qui ne sont
* pas de l'UTF-8 lèvent LECTURE {chemin, dossier, cause:
* 'UTF8_INVALIDE', octets}, octets étant un Uint8Array, copie des
* octets du fichier, que le dépôt décode ligne par ligne
* @property {(r: Racine, chemin: string, texte: string) => Promise<void>} ecrireAtomique
* <chemin>.ecriture, vidé sur le disque, renommé par-dessus ; réessaie
* le renommage ; un échec laisse la cible intacte (§ 8.8) ; là où
* renommageAtomique est faux, l'écriture va droit sur la cible. Le
* dossier qui porte la cible doit exister : un dossier absent lève
* ECRITURE, car un dossier de travail disparu — un support retiré —
* ne se recrée pas en silence
* @property {(r: Racine, chemin: string, ligne: string) => Promise<void>} ajouterLigne
* ajoute la ligne puis une fin de ligne LF après le dernier octet du
* fichier, sans rien insérer avant, vidé sur le disque ; crée le
* fichier absent, jamais son dossier. La ligne ne porte aucune fin
* de ligne
* @property {(r: Racine, dossier: string) => Promise<Entree[]>} lister
* les entrées directes du dossier, triées par nom, unités UTF-16
* comparées une à une ; ABSENT pour un dossier absent
* @property {(r: Racine, chemin: string) => Promise<void>} creerDossier
[ADD] storage: copy, rename, import, delete events; local settings § 8.7 operations reuse the event store's pairing and alignment instead of copying them. Renaming writes the complete pair under the new name — the store now writes a gesture with the pair it writes elsewhere — before deleting the old one, so no cut leaves zero pairs; an unchanged name writes nothing. Deleting moves state, .precedent, journal, lock and leftovers into a dated trash, matching names as the case-insensitive disk does. creerDossier creates only the root it is aimed at, so a vanished working folder never comes back empty. reglages_locaux.json keeps exactly the two § 8.5 settings; its deferred writer always keeps the last demand. Checked: 1314 node, 35 browser, 27 node-long tests; depot.js at 100 %. --- FR --- [ADD] stockage : copier, renommer, importer, supprimer ; réglages locaux Les opérations du § 8.7 reprennent l'appariement et l'alignement du dépôt au lieu de les recopier. Renommer écrit la paire complète sous le nouveau nom — le dépôt écrit désormais un geste avec la paire qu'il pose ailleurs — avant de supprimer l'ancienne : aucune coupure ne laisse zéro paire ; un nom inchangé n'écrit rien. Supprimer range état, .precedent, journal, verrou et restes dans une corbeille datée, en comparant les noms comme le disque insensible à la casse. creerDossier ne crée que la racine visée : un dossier de travail disparu ne renaît pas vide. reglages_locaux.json garde les deux réglages du § 8.5 ; son écrivain différé garde la dernière demande. Vérifié : 1314 node, 35 navigateur, 27 node-long ; depot.js à 100 %. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:05 -04:00
* '' crée la racine elle-même, parents compris ; un autre chemin
* crée ce qui lui manque sous la racine, qui doit exister : une
* racine absente lève ECRITURE, sans rien créer, car un dossier de
* travail disparu ne renaît pas en silence (§ 8.6). Un dossier
* existant n'est pas une faute
* @property {(r: Racine, de: string, vers: string) => Promise<void>} deplacer
* un fichier ; refuse une cible existante (EXISTE), comparée comme le
* système compare les noms, si bien que sans égard à la casse une
* autre casse du même nom est une cible existante ; ABSENT pour une
* source absente ; le dossier de la cible doit exister
* @property {(r: Racine, chemin: string) => Promise<void>} supprimer
* un fichier ; un fichier absent n'est pas une faute
* @property {(r: Racine, chemin: string, seance: string) => Promise<Verrou>} verrouiller
* création exclusive de {seance, pid, hote, depuis} : un verrou
* présent n'est jamais repris, fût-il de la même séance
* @property {(r: Racine, chemin: string, seance: string) => Promise<void>} deverrouiller
* n'efface que le verrou de cette séance ; un verrou absent n'est
* pas une faute
* @property {(r: Racine) => Promise<void>} ouvrirDansExplorateur
* @property {() => Promise<{nom: string, octets: Uint8Array}|null>} choisirFichierAImporter
* null quand l'opérateur annule
* @property {(nomPropose: string, octets: Uint8Array) => Promise<string|null>} enregistrerSous
* le chemin choisi, ou null quand l'opérateur annule
*/
// Motif d'un segment que toute implémentation accepte. Un segment vide
// vient d'un chemin absolu (« /x »), d'un séparateur doublé ou final. Un
// segment qui finit par un point ou une espace comprend « . » et « .. », et
// ce que Windows ramène à eux ou à un autre nom en retirant points et espaces
// finaux : « ... », « .. » suivi d'une espace, « soiree. ». La barre oblique
// inverse sépare les segments sous Windows ; le deux-points y désigne un
// lecteur (« C:\x », « C:x ») ou un flux de données secondaire ; le
// caractère nul tronque un chemin là où la plateforme le lit en C.
const SEGMENT_ADMIS = /^[^\\:\u{0}]*[^\\:\u{0}. ]$/u;
/**
* Segments d'un chemin relatif à une racine : [] pour '', la racine
* elle-même. Le contrôle ne lit que le texte du chemin ; il ne connaît ni la
* racine ni les liens, que résout chaque implémentation.
*
* @param {string} chemin
* @returns {string[]}
* @throws {ErreurStockage} CHEMIN_REFUSE {chemin} : autre chose qu'une chaîne,
* ou un segment que SEGMENT_ADMIS refuse
*/
export function exigerCheminRelatif(chemin) {
if (typeof chemin !== 'string') throw new ErreurStockage('CHEMIN_REFUSE', { chemin });
if (chemin === '') return [];
const segments = chemin.split('/');
if (!segments.every((segment) => SEGMENT_ADMIS.test(segment))) {
throw new ErreurStockage('CHEMIN_REFUSE', { chemin });
}
return segments;
}