DodoTransfer/lib/archive.js
2026-11-05 10:28:10 +09:00

81 lines
2.4 KiB
JavaScript

'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 };