'use strict'; const fs = require('fs'); const path = require('path'); const { createGzip } = require('zlib'); const tar = require('tar-stream'); const utils = require('./utils'); /** Flat archives need unique names: "a.txt", "a (2).txt", ... */ function uniqueNamer() { const used = new Map(); return (rawName, fallback) => { const base = utils.toSafeBasename(rawName, fallback); const prev = used.get(base) || 0; used.set(base, prev + 1); if (prev === 0) return base; const ext = path.extname(base); const stem = ext ? base.slice(0, -ext.length) : base; return `${ stem } (${ prev + 1 })${ ext }`; }; } const fileOf = (store, info) => store.getFilename(info.metadata.sid + '++' + info.key); async function pipeZip(res, bucket, store) { res.header('Content-Type', 'application/zip'); const { ZipArchive } = await import('archiver'); const archive = new ZipArchive(); archive.on('error', err => { console.error(err); res.destroy(err); }); archive.pipe(res); const name = uniqueNamer(); for (const info of bucket) { await new Promise((resolve, reject) => { const stream = fs.createReadStream(fileOf(store, info)); stream.on('end', resolve); stream.on('error', reject); archive.append(stream, { name: name(info.metadata.name, info.key) }); }); } await archive.finalize(); } async function pipeTarGz(res, bucket, store) { res.header('Content-Type', 'application/gzip'); const pack = tar.pack(); pack.on('error', err => res.destroy(err)); pack.pipe(createGzip()).pipe(res); const name = uniqueNamer(); for (const info of bucket) { await new Promise((resolve, reject) => { const readStream = fs.createReadStream(fileOf(store, info)); const entry = pack.entry({ name: name(info.metadata.name, info.key), size: info.size }); readStream.on('error', reject); entry.on('error', reject); entry.on('finish', resolve); readStream.pipe(entry); }); } pack.finalize(); } /** * Stream all files of a bucket as zip or tar.gz. Errors while reading a file * abort the response, so a broken archive is never mistaken for a complete one. */ async function pipeArchive(res, bucket, format, store) { try { if (format === 'zip') await pipeZip(res, bucket, store); else await pipeTarGz(res, bucket, store); } catch (err) { console.error(err); res.destroy(err); } } module.exports = { pipeArchive, uniqueNamer };