mirror of
https://github.com/xCyanGrizzly/DragonsStash.git
synced 2026-09-21 05:21:43 +00:00
feat(worker): sparse-file reconstruction helper for ranged archive listing
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { open } from "fs/promises";
|
||||
import { listFromSparse } from "./sparse-list.js";
|
||||
|
||||
describe("listFromSparse", () => {
|
||||
it("writes each region at its offset into a sparse file and passes the path to the lister", async () => {
|
||||
const size = 1_000_000;
|
||||
const regions = [
|
||||
{ offset: 0, bytes: Buffer.from("HEAD") },
|
||||
{ offset: size - 4, bytes: Buffer.from("TAIL") },
|
||||
];
|
||||
let seenPath = "";
|
||||
const entries = await listFromSparse(
|
||||
[{ fileName: "sample.7z", size, regions }],
|
||||
async (firstPartPath) => {
|
||||
seenPath = firstPartPath;
|
||||
const fh = await open(firstPartPath, "r");
|
||||
try {
|
||||
const head = Buffer.alloc(4); await fh.read(head, 0, 4, 0);
|
||||
const tail = Buffer.alloc(4); await fh.read(tail, 0, 4, size - 4);
|
||||
const hole = Buffer.alloc(4); await fh.read(hole, 0, 4, 500_000);
|
||||
expect(head.toString()).toBe("HEAD");
|
||||
expect(tail.toString()).toBe("TAIL");
|
||||
expect(hole.equals(Buffer.alloc(4))).toBe(true); // gap is zero
|
||||
} finally { await fh.close(); }
|
||||
return [{ path: "a/b.stl", fileName: "b.stl", extension: "stl", compressedSize: 1n, uncompressedSize: 1n, crc32: null }];
|
||||
},
|
||||
);
|
||||
expect(seenPath.endsWith("sample.7z")).toBe(true);
|
||||
expect(entries).not.toBeNull();
|
||||
expect(entries!).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("returns null when the lister yields no entries", async () => {
|
||||
const res = await listFromSparse(
|
||||
[{ fileName: "x.7z", size: 100, regions: [{ offset: 0, bytes: Buffer.from("A") }] }],
|
||||
async () => [],
|
||||
);
|
||||
expect(res).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
import { mkdtemp, open, rm } from "fs/promises";
|
||||
import path from "path";
|
||||
import { config } from "../../util/config.js";
|
||||
import { childLogger } from "../../util/logger.js";
|
||||
import type { FileEntry } from "../zip-reader.js";
|
||||
|
||||
const log = childLogger("sparse-list");
|
||||
|
||||
export interface SparsePart {
|
||||
fileName: string;
|
||||
size: number;
|
||||
regions: { offset: number; bytes: Buffer }[];
|
||||
}
|
||||
|
||||
export type SparseLister = (firstPartPath: string) => Promise<FileEntry[]>;
|
||||
|
||||
/**
|
||||
* Reconstruct archive header bytes into sparse temp files (data areas left as
|
||||
* zero holes), run `lister` on the first part, return its entries.
|
||||
* Returns null on any error or when the lister finds nothing.
|
||||
*/
|
||||
export async function listFromSparse(
|
||||
parts: SparsePart[],
|
||||
lister: SparseLister,
|
||||
): Promise<FileEntry[] | null> {
|
||||
if (parts.length === 0) return null;
|
||||
const dir = await mkdtemp(path.join(config.tempDir, "ranged-"));
|
||||
try {
|
||||
let firstPath = "";
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const p = parts[i];
|
||||
const filePath = path.join(dir, p.fileName);
|
||||
if (i === 0) firstPath = filePath;
|
||||
const fh = await open(filePath, "w");
|
||||
try {
|
||||
await fh.truncate(p.size); // create the sparse hole
|
||||
for (const r of p.regions) {
|
||||
await fh.write(r.bytes, 0, r.bytes.length, r.offset);
|
||||
}
|
||||
} finally {
|
||||
await fh.close();
|
||||
}
|
||||
}
|
||||
const entries = await lister(firstPath);
|
||||
return entries.length > 0 ? entries : null;
|
||||
} catch (err) {
|
||||
log.warn({ err }, "sparse listing failed");
|
||||
return null;
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user