Files
toolbox-portal/scripts/archive.mjs

78 lines
2.6 KiB
JavaScript

import { createReadStream, createWriteStream } from 'node:fs';
import { mkdir, readdir, stat } from 'node:fs/promises';
import path from 'node:path';
import { pipeline } from 'node:stream/promises';
import archiver from 'archiver';
import {
canonicalExistingPath,
canonicalPathsOverlap,
canonicalProspectivePath,
} from './path-safety.mjs';
const ARCHIVE_DATE = new Date('1980-01-01T00:00:00.000Z');
function compareCodePoints(left, right) {
const leftPoints = Array.from(left, (value) => value.codePointAt(0));
const rightPoints = Array.from(right, (value) => value.codePointAt(0));
const length = Math.min(leftPoints.length, rightPoints.length);
for (let index = 0; index < length; index += 1) {
if (leftPoints[index] !== rightPoints[index])
return leftPoints[index] - rightPoints[index];
}
return leftPoints.length - rightPoints.length;
}
async function listFiles(root, relative = '') {
const directory = path.join(root, relative);
const entries = await readdir(directory, { withFileTypes: true });
const files = [];
for (const entry of entries.sort((left, right) =>
compareCodePoints(left.name, right.name)
)) {
const child = path.posix.join(
relative.split(path.sep).join('/'),
entry.name
);
if (entry.isDirectory()) files.push(...(await listFiles(root, child)));
else if (entry.isFile()) files.push(child);
else
throw new Error(
`Archive input contains an unsupported file type: ${child}`
);
}
return files;
}
export async function createStaticArchive(inputDirectory, outputFile) {
const source = await canonicalExistingPath(
inputDirectory,
'Archive input directory'
);
const output = await canonicalProspectivePath(outputFile);
if (canonicalPathsOverlap(source, output))
throw new Error('Archive output and input directory must not overlap.');
const sourceStat = await stat(source);
if (!sourceStat.isDirectory())
throw new Error(`Archive input is not a directory: ${source}`);
await mkdir(path.dirname(output), { recursive: true });
const archive = archiver('zip', { zlib: { level: 9 } });
const destination = createWriteStream(output, { flags: 'wx' });
archive.on('warning', (error) => {
if (error.code !== 'ENOENT') destination.destroy(error);
});
archive.on('error', (error) => destination.destroy(error));
const writing = pipeline(archive, destination);
for (const relative of await listFiles(source)) {
archive.append(createReadStream(path.join(source, relative)), {
name: relative,
date: ARCHIVE_DATE,
mode: 0o644,
});
}
await archive.finalize();
await writing;
return output;
}