50 lines
2.8 KiB
TypeScript
50 lines
2.8 KiB
TypeScript
import "server-only";
|
|
import { createReadStream, createWriteStream } from "node:fs";
|
|
import { mkdir, stat, statfs, unlink, readdir, rename } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { tmpdir } from "node:os";
|
|
import { pipeline } from "node:stream/promises";
|
|
import { Readable, Transform } from "node:stream";
|
|
import JSZip from "jszip";
|
|
export const downloadZipDirectory = process.env.CANVAS_DOWNLOAD_ZIP_DIR || join(tmpdir(), "canvas-download-zips-v2");
|
|
const directory = downloadZipDirectory;
|
|
export async function writeDownloadZip(zip: JSZip, id: string, progress: (percent: number) => void) {
|
|
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
// Remove abandoned files after restart as well as expired in-process tasks.
|
|
for (const name of await readdir(directory)) {
|
|
const path = join(directory, name);
|
|
const info = await stat(path).catch(() => null);
|
|
if (info?.isFile() && name.endsWith(".partial") && info.mtimeMs < Date.now() - 60 * 60_000) await unlink(path).catch(() => undefined);
|
|
}
|
|
const path = join(directory, `${id}.zip`);
|
|
const temporary = `${path}.partial`;
|
|
try {
|
|
const reserve = 1024 ** 3 + 32 * 1024 ** 2;
|
|
const initialDisk = await statfs(directory);
|
|
if (initialDisk.bavail * initialDisk.bsize < reserve)
|
|
throw new Error("服务器下载空间不足,请减少本次图片数量");
|
|
let sinceCheck = 8 * 1024 ** 2;
|
|
const guard = new Transform({ transform(chunk, _encoding, callback) {
|
|
sinceCheck += chunk.length;
|
|
if (sinceCheck < 8 * 1024 ** 2) { callback(null, chunk); return; }
|
|
sinceCheck = 0;
|
|
// Input files are released as their streams close, so available disk grows
|
|
// while the ZIP is written. Check current space instead of a stale budget.
|
|
void statfs(directory).then(disk => {
|
|
callback(disk.bavail * disk.bsize < reserve ? new Error("服务器下载空间不足,请减少本次图片数量") : null, chunk);
|
|
}, error => callback(error));
|
|
} });
|
|
await pipeline(zip.generateNodeStream({ type: "nodebuffer", streamFiles: true, compression: "STORE" },
|
|
metadata => progress(metadata.percent)), guard, createWriteStream(temporary, { mode: 0o600 }));
|
|
await rename(temporary, path);
|
|
return { path, bytes: (await stat(path)).size };
|
|
} catch (error) { await unlink(temporary).catch(() => undefined); throw error; }
|
|
}
|
|
export function readDownloadZip(path: string, range?: { start: number; end: number }) {
|
|
return Readable.toWeb(createReadStream(path, range)) as ReadableStream<Uint8Array>;
|
|
}
|
|
export function streamDownloadZip(zip: JSZip) {
|
|
return Readable.toWeb(zip.generateNodeStream({ type: "nodebuffer", streamFiles: true, compression: "STORE" }) as Readable) as ReadableStream<Uint8Array>;
|
|
}
|
|
export async function removeDownloadZip(path: string) { await unlink(path).catch(() => undefined); }
|