78 lines
2.6 KiB
JavaScript
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;
|
|
}
|