Archives ZIP
createZipStream assemble une archive ZIP à la volée, entrée par entrée, sans compression et sans dépendance, ZIP64 compris.
Un drive ou une médiathèque propose de télécharger une sélection en un seul fichier. createZipStream produit cette archive pendant qu'elle part vers le navigateur : un fichier source est ouvert à la fois, lu jusqu'au bout, puis le suivant. La mémoire du serveur ne dépend donc pas de la taille de l'archive.
Import
import { createZipStream } from '@fraza/core/storage/zip';Module serveur : il calcule les sommes de contrôle avec node:zlib. Sans dépendance : il remplace archiver dans les modules.
API
| Signature | Retour | Rôle |
|---|---|---|
createZipStream(entries, { forceZip64 }?) | ReadableStream<Uint8Array> | Archive des entries, prête à servir de corps de réponse. |
createZipStream(entries, { forceZip64 }?)
entries est un tableau ou n'importe quel itérable, asynchrone compris, d'objets { name, size?, mtime?, open } :
| Champ | Rôle |
|---|---|
name | Chemin dans l'archive, dossiers séparés par /. Un nom terminé par / est un dossier vide, sans open. |
size | Taille annoncée, en octets. Vérifiée à la fin de la lecture : une source plus courte fait échouer l'archive au lieu de livrer un fichier tronqué sans le dire. |
mtime | Date de modification, maintenant par défaut. |
open() | Fonction asynchrone appelée quand vient le tour de l'entrée, jamais avant. Retourne un ReadableStream web, un Readable Node, un Buffer, ou null pour sauter l'entrée. |
import { getFileStream } from '@fraza/core/storage';
import { createZipStream } from '@fraza/core/storage/zip';
import { warn } from '@fraza/core/shared/logger';
// Dans le handler d'une route : le routeur du core streame `file.body`.
function zipFile(files, filename) {
const stream = createZipStream(files.map((file) => ({
name: file.path,
size: Number(file.size_bytes),
mtime: file.updated_at,
open: async () => {
const fetched = await getFileStream(file.storage_key);
if (!fetched.success) {
warn(`zip: storage miss for ${file.storage_key}, skipped`);
return null;
}
return fetched.data.body;
},
})));
return { success: true, file: { body: stream, contentType: 'application/zip', filename } };
}open() est appelé avant que la moindre ligne de l'entrée ne soit écrite : une entrée dont le fichier manque est sautée sans laisser d'en-tête orphelin dans l'archive. Une erreur levée par open() ou par une source fait échouer le flux, et le navigateur reçoit un téléchargement interrompu. Pour sauter un fichier plutôt qu'échouer, attraper l'erreur dans open() et retourner null.
forceZip64 écrit les enregistrements ZIP64 même pour une petite archive. Il ne sert qu'à vérifier qu'un lecteur les accepte : sans lui, ZIP64 s'active seul dès qu'il est nécessaire.
Format
Les entrées sont stockées sans compression. Ce qui transite par un drive ou une médiathèque est presque toujours déjà compressé (JPEG, PNG, MP4, PDF, fichiers Office) : recompresser coûterait du processeur pour un gain négligeable.
La somme de contrôle CRC-32 d'une entrée n'est connue qu'une fois ses octets envoyés. Chaque fichier est donc suivi d'un descripteur de données, et l'en-tête local porte des zéros. Le répertoire central, écrit à la fin, reprend les valeurs exactes.
ZIP64 s'active dès qu'une taille, une position dans l'archive ou le nombre d'entrées dépasse ce que les champs classiques tiennent : 4 Gio par fichier ou par position, 65 535 entrées. Les archives sont vérifiées avec Info-ZIP unzip, libarchive (bsdtar, qui lit en flux) et le module zipfile de Python, au-delà de 4 Gio compris.
Les noms sont en UTF-8 et signalés comme tels : « Résumé.pdf » s'extrait sous ce nom, sans caractères de substitution. La date de modification est écrite deux fois, au format MS-DOS dans l'heure locale du serveur, et en UTC dans le champ d'horodatage étendu que lisent la plupart des outils.
Notes
Les chemins restent dans l'archive. Les segments vides, . et .. sont retirés, les barres obliques inverses deviennent des /, et les caractères de contrôle disparaissent. Une entrée ../../etc/passwd devient etc/passwd : un outil d'extraction négligent ne peut pas écrire hors du dossier choisi.
Un nom répété est renommé. Deux fichiers photo.jpg donnent photo.jpg et photo (2).jpg, au lieu de laisser l'outil d'extraction demander lequel garder. Un dossier répété est ignoré.
L'annulation libère la source. Quand le navigateur ferme la connexion, le flux en cours de lecture est annulé (ReadableStream) ou détruit (Readable), ce qui ferme la connexion au stockage, et aucune entrée suivante n'est ouverte.
La durée, pas la mémoire, limite une archive. Borner le nombre d'entrées et la taille cumulée côté appelant, avant d'appeler createZipStream, et dire à l'utilisateur ce qui a été écarté.