@aventii/files (1.0.0)

Published 2026-08-24 09:34:52 +00:00 by Valentin

Installation

@aventii:registry=
npm install @aventii/files@1.0.0
"@aventii/files": "1.0.0"

About this package

@aventii/files

Abstraction générique de stockage de fichiers binaires pour les microservices Node.js Aventii. @aventii/files standardise l'accès au stockage, pas la politique métier qui décide où et pourquoi un fichier doit être stocké.

Files sait associer une clé opaque à des octets. Il ne sait pas ce que ces octets représentent.

Installation

npm install @aventii/files

Le package est CommonJS, sans dépendance runtime externe en V1.

Utilisation minimale

const { createFileManager, createLocalStorageAdapter } = require('@aventii/files');
const files = createFileManager({
	adapter: createLocalStorageAdapter({
		root: './uploads'
	})
});
await files.write('2026/04/13/ae27367bf.pdf', Buffer.from('contenu'));
const content = await files.read('2026/04/13/ae27367bf.pdf');

Le package fournit l'écriture, la lecture, l'existence, la suppression, la taille technique et les streams Node.js.

Principe général

métier du microservice
        ↓
construction de la clé
        ↓
@aventii/files
        ↓
adapter
        ↓
backend physique

Le consommateur décide de la clé et @aventii/files la considère comme opaque. Un service PDF peut produire :

2026/04/13/ae27367bf.pdf

Un service de photos peut produire :

ae24/6a5e1ff5861ee639addd914c.png

Un service documentaire peut produire :

document-1770812345678-a4f91c.pdf

Ces trois valeurs sont seulement des clés POSIX relatives pour le package.

API publique

const {
	createFileManager,
	createLocalStorageAdapter,
	FileError
} = require('@aventii/files');

createFileManager(options)

Crée la façade de stockage utilisée par un microservice.

const files = createFileManager({ adapter });

L'adapter doit fournir :

{
	write,
	read,
	exists,
	delete,
	stat,
	createReadStream,
	createWriteStream
}

Le contrat est vérifié immédiatement à la création. Une méthode absente ou non fonctionnelle produit une TypeError. Le manager retourne les sept mêmes opérations. Il valide le contrat commun et délègue au backend. Il ne choisit jamais une clé à la place du consommateur.

Clés de stockage

Une clé est une chaîne POSIX relative. Exemples valides :

file.pdf
2026/04/13/file.pdf
ae24/user.png
photos/products/abc/main.webp
folder/file name.bin

Exemples invalides :

/absolute/file.pdf
../file.pdf
folder/../file.pdf
./file.pdf
folder//file.pdf
folder/
folder\file.pdf

Une clé invalide produit un FileError de code INVALID_KEY. Le séparateur canonique est toujours /, même si le filesystem physique utilise un autre séparateur. La clé n'est ni trimée ni réécrite avant délégation. Le package refuse :

  • une chaîne vide ou composée uniquement d'espaces ;
  • un slash initial ;
  • un slash final ;
  • un segment vide ;
  • . ;
  • .. ;
  • le séparateur \ ;
  • le caractère NUL.

Clé opaque

Le package ne considère jamais 2026 comme une année, 04 comme un mois ou ae24 comme un shard. La sémantique des segments appartient exclusivement au consommateur.

Clé persistable

Une base métier peut conserver :

{
	storageKey: '2026/04/13/file.pdf'
}

Elle ne devrait pas conserver :

{
	storageKey: '/app/uploads/2026/04/13/file.pdf'
}

Le second format couple les données au filesystem local.

write(key, data)

Écrit ou remplace un fichier.

await files.write('avatars/user.png', buffer);

Types acceptés :

Buffer
Uint8Array

Un autre type produit INVALID_DATA. Une écriture sur une clé déjà présente remplace son contenu. La V1 ne fournit pas de mode append. La création des répertoires physiques éventuels appartient à l'adapter.

read(key)

Lit entièrement le fichier en mémoire.

const content = await files.read('avatar.png');

Le résultat public est toujours un Buffer. Un adapter peut retourner un Uint8Array ; le manager le normalise en Buffer. Un fichier absent produit FILE_NOT_FOUND. read() ne transforme pas l'absence en null ou undefined. Pour tester sans exception, utiliser exists().

exists(key)

Teste l'existence d'un fichier.

if (await files.exists(key)) {
	// fichier présent
}

Retourne uniquement true ou false. Une absence normale produit false. Une panne du backend, une erreur de permission ou une erreur technique reste une exception. Le package ne confond donc jamais « fichier absent » et « stockage indisponible ». L'adapter local retourne true uniquement pour un fichier régulier.

delete(key)

Supprime un fichier.

const deleted = await files.delete(key);

Retour :

true  => un fichier a effectivement été supprimé
false => aucun fichier n'existait sous cette clé

La suppression d'un fichier déjà absent n'est pas une erreur. Une autre erreur de suppression reste une exception.

stat(key)

Retourne les métadonnées techniques minimales garanties.

const result = await files.stat(key);

Contrat V1 :

{
	size: 182734
}

size est exprimé en octets. Aucune autre propriété n'est garantie. Un adapter futur peut connaître un ETag, une date de modification ou un content type, sans que ces propriétés deviennent automatiquement publiques. Le manager retourne un nouvel objet contenant uniquement size.

createReadStream(key)

Crée un stream Node.js lisible.

const stream = await files.createReadStream('2026/04/13/document.pdf');
stream.pipe(res);

Cette API évite de charger tout le fichier en mémoire. Le package ne dépend pas d'Express ; res appartient au consommateur. Avec l'adapter local, le fichier est ouvert avant le retour. Un fichier absent rejette donc la Promise avec FILE_NOT_FOUND au lieu de produire seulement une erreur différée du stream. Le fichier doit être un fichier régulier.

createWriteStream(key)

Crée un stream Node.js inscriptible.

const output = await files.createWriteStream('2026/04/13/generated.pdf');
pdfDocument.pipe(output);

Cette API convient aux générateurs qui produisent naturellement un flux. Avec l'adapter local, les répertoires intermédiaires sont créés automatiquement. Un fichier déjà présent sous la même clé est remplacé. Le package ne connaît pas PDFKit ou un autre générateur.

createLocalStorageAdapter(options)

Crée le backend filesystem local.

const adapter = createLocalStorageAdapter({
	root: './uploads'
});

root est obligatoire et doit être une chaîne non vide. La valeur est résolue en chemin absolu privé à la création. L'adapter ne crée pas la racine immédiatement. Elle sera créée si une écriture ou un stream d'écriture en a besoin.

Traduction physique

Avec :

root: '/app/uploads'

la clé :

2026/04/13/file.pdf

correspond physiquement à :

/app/uploads/2026/04/13/file.pdf

Cette traduction est strictement privée à l'adapter. Le chemin physique n'est jamais retourné comme clé.

Répertoires intermédiaires

L'écriture de :

2026/04/13/file.pdf

crée récursivement les répertoires nécessaires. La suppression d'un fichier ne supprime pas les répertoires devenus vides.

Confinement sous root

Les règles de clé bloquent les traversées évidentes. L'adapter vérifie également que le chemin résolu reste sous la racine configurée. Cette seconde vérification est volontairement défensive.

Utilisation directe de l'adapter

L'adapter local est exporté et peut être utilisé directement :

const storage = createLocalStorageAdapter({
	root: './uploads'
});
await storage.write('a/file.bin', Buffer.from('x'));

Les mêmes règles de clé et de données s'appliquent. L'usage recommandé dans les microservices reste le manager afin de conserver une façade indépendante du backend :

const files = createFileManager({
	adapter: storage
});

FileError

FileError est exportée :

const { FileError } = require('@aventii/files');

Exemple :

try {
	await files.read(key);
} catch (err) {
	if (err instanceof FileError && err.code === 'FILE_NOT_FOUND') {
		// politique du consommateur
	}
}

Propriétés principales :

{
	name: 'FileError',
	message: '[files] ...',
	code: '...',
	key: '...',
	cause: Error
}

key vaut null lorsqu'aucune clé précise n'est concernée. cause conserve l'erreur technique d'origine lorsqu'elle existe.

Codes d'erreur V1

INVALID_KEY
INVALID_DATA
INVALID_ADAPTER_RESULT
FILE_NOT_FOUND
NOT_A_FILE
WRITE_FAILED
READ_FAILED
EXISTS_FAILED
DELETE_FAILED
STAT_FAILED
READ_STREAM_FAILED
WRITE_STREAM_FAILED
FILES_ERROR

Les erreurs de contrat à la création du manager ou de l'adapter utilisent TypeError.

Contrat d'un adapter

Un adapter compatible expose :

{
	async write(key, data) {},
	async read(key) {},
	async exists(key) {},
	async delete(key) {},
	async stat(key) {},
	async createReadStream(key) {},
	async createWriteStream(key) {}
}

adapter.write

Reçoit la clé et un Buffer ou Uint8Array. La sémantique est « écrire ou remplacer ».

adapter.read

Retourne un Buffer ou Uint8Array. Le manager expose toujours un Buffer.

adapter.exists

Retourne obligatoirement un booléen. Un autre type produit INVALID_ADAPTER_RESULT.

adapter.delete

Retourne obligatoirement un booléen. true signifie supprimé et false signifie absent.

adapter.stat

Retour minimal :

{
	size: 123
}

size doit être un entier sûr positif ou nul.

adapter.createReadStream

Retourne un stream Node lisible compatible avec pipe() et on().

adapter.createWriteStream

Retourne un stream Node inscriptible compatible avec write() et on().

Adapter futur

La V1 fournit uniquement le stockage local. Le contrat permet ultérieurement d'implémenter S3, MinIO, Cloudflare R2, Azure Blob Storage ou un autre object storage sans modifier le manager. Un backend objet pourra traiter directement :

2026/04/13/file.pdf

comme object key. Aucune stratégie de répertoire ne doit être ajoutée au manager.

Pourquoi aucun adapter S3 en V1

Le package est extrait de besoins réellement présents dans les microservices actuels. Ces microservices utilisent le stockage local. Ajouter S3 maintenant introduirait du code non extrait de l'existant, des dépendances supplémentaires et des comportements sans consommateur réel. Le contrat prépare cette évolution sans l'implémenter prématurément.

Exemple : service PDF

Le service peut conserver sa convention de date :

function getPdfKey(date, id) {
	return [
		date.getFullYear(),
		String(date.getMonth() + 1).padStart(2, '0'),
		String(date.getDate()).padStart(2, '0'),
		`${id}.pdf`
	].join('/');
}

Puis :

const stream = await files.createWriteStream(getPdfKey(date, id));
doc.pipe(stream);

Le package ne connaît ni la date, ni l'identifiant, ni PDFKit.

Exemple : photos

Le service conserve sa stratégie de répartition :

function avatarKey(userId) {
	const shard = getShard(userId);
	return `${shard}/${userId}.png`;
}

Puis :

const image = await sharp(buffer)
	.resize(256, 256, { fit: 'cover' })
	.png({ quality: 85 })
	.toBuffer();
await files.write(avatarKey(userId), image);

Sharp reste dans le microservice.

Exemple : document temporaire

Le consommateur peut aussi choisir une clé plate :

const key = `document-${Date.now()}-${randomHex}.pdf`;
await files.write(key, req.file.buffer);

Le package n'impose aucune hiérarchie.

Architecture recommandée

Dans un microservice :

infrastructure/
    files.js
routes/
services/
index.js

Exemple :

// infrastructure/files.js
const { createFileManager, createLocalStorageAdapter } = require('@aventii/files');
const config = require('./config');
module.exports = createFileManager({
	adapter: createLocalStorageAdapter({
		root: config.FILES_DIRECTORY
	})
});

Les routes ou services consomment ensuite cette instance. La configuration et l'assemblage du package restent ainsi dans infrastructure/.

Articulation avec @aventii/runtime

L'adapter local n'ouvre aucune ressource persistante nécessitant un hook close(). @aventii/files n'installe aucun gestionnaire de signal. Il ne possède aucun lifecycle global.

Articulation avec @aventii/config

Le répertoire racine peut venir d'une configuration déjà validée :

const adapter = createLocalStorageAdapter({
	root: config.FILES_DIRECTORY
});

@aventii/files ne lit jamais process.env.

Articulation avec @aventii/metrics

Le package ne décide d'aucune métrique métier. Le consommateur peut mesurer le nombre d'uploads, le volume, les suppressions ou le temps de génération. stat() fournit la taille minimale nécessaire aux usages qui mesurent le volume.

Sécurité

La clé est considérée non fiable. Elle est validée avant chaque opération du manager. L'adapter local applique également la validation lorsqu'il est utilisé directement. Le confinement sous root est vérifié après résolution. Ces protections ne remplacent jamais l'autorisation métier.

Ce que Files ne fait pas

@aventii/files ne doit pas :

  • choisir ou générer une clé ;
  • connaître les utilisateurs, profils, produits, trajets ou réservations ;
  • connaître les documents d'identité ou les véhicules ;
  • connaître les PDF ou les images ;
  • connaître Sharp, PDFKit, Multer ou Express ;
  • définir une politique MIME ;
  • définir une taille maximale d'upload ;
  • décider qui peut lire, écrire ou supprimer ;
  • décider combien de temps un fichier doit être conservé ;
  • gérer une validation humaine ;
  • stocker des métadonnées métier ;
  • imposer une stratégie de sharding ;
  • imposer une arborescence ;
  • générer des URLs publiques ou pré-signées ;
  • lire process.env ;
  • connaître un produit Aventii particulier. Ces responsabilités restent dans les microservices consommateurs.

Fonctions volontairement absentes de V1

Il n'existe pas de :

list()
listByPrefix()
copy()
move()
rename()
append()
generateKey()
getPublicUrl()
presign()
setMetadata()
writeFromPath()

Elles pourront être ajoutées lorsqu'un consommateur réel les exigera.

Tests

Le package utilise le runner natif Node :

npm test

qui exécute :

node --test

Les tests couvrent le contrat de l'adapter, la délégation, les clés, les opérations locales, les erreurs et les streams. Les tests filesystem utilisent des répertoires temporaires et ne modifient pas le projet.

Résumé

Sans @aventii/files, plusieurs services réimplémentent :

validation de clé
+
résolution filesystem
+
mkdir recursive
+
readFile / writeFile
+
unlink / stat
+
streams
+
normalisation d'erreurs

Avec @aventii/files :

consommateur
    │
    ├── construit la clé
    ▼
createFileManager()
    ▼
adapter
    ▼
backend physique

La frontière V1 est volontairement étroite :

le consommateur décide de la clé et du métier ; @aventii/files transforme une clé opaque en opérations fiables sur des octets.

Details
npm
2026-08-24 09:34:52 +00:00
2
UNLICENSED
latest
9.1 KiB
Assets (1)
files-1.0.0.tgz 9.1 KiB
Versions (1) View all
1.0.0 2026-08-24