mirror of
https://github.com/xCyanGrizzly/DragonsStash.git
synced 2026-05-10 22:01:16 +00:00
89 lines
2.7 KiB
TypeScript
89 lines
2.7 KiB
TypeScript
import { execFile } from "child_process";
|
|
import { promisify } from "util";
|
|
import path from "path";
|
|
import { childLogger } from "../util/logger.js";
|
|
import type { FileEntry } from "./zip-reader.js";
|
|
|
|
const execFileAsync = promisify(execFile);
|
|
const log = childLogger("7z-reader");
|
|
|
|
/**
|
|
* Parse output of `7z l <file>` to extract file metadata.
|
|
*
|
|
* Example output:
|
|
* Date Time Attr Size Compressed Name
|
|
* ------------------- ----- ------------ ------------ ------------------------
|
|
* 2024-01-15 10:30:00 ....A 12345 10234 folder/file.stl
|
|
* ------------------- ----- ------------ ------------ ------------------------
|
|
*/
|
|
export async function read7zContents(
|
|
filePath: string
|
|
): Promise<FileEntry[]> {
|
|
try {
|
|
const { stdout } = await execFileAsync("7z", ["l", filePath], {
|
|
timeout: 30000,
|
|
maxBuffer: 10 * 1024 * 1024,
|
|
});
|
|
|
|
return parse7zOutput(stdout);
|
|
} catch (err) {
|
|
log.warn({ err, file: filePath }, "Failed to read 7z contents");
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function parse7zOutput(output: string): FileEntry[] {
|
|
const entries: FileEntry[] = [];
|
|
const lines = output.split("\n");
|
|
|
|
let inFileList = false;
|
|
let separatorCount = 0;
|
|
|
|
for (const line of lines) {
|
|
const trimmed = line.trim();
|
|
|
|
// Detect separator lines (------- pattern)
|
|
if (/^-{5,}/.test(trimmed)) {
|
|
separatorCount++;
|
|
if (separatorCount === 1) {
|
|
inFileList = true;
|
|
} else if (separatorCount >= 2) {
|
|
inFileList = false;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (!inFileList) continue;
|
|
|
|
// Parse: Date Time Attr Size [Compressed] Name
|
|
// In solid archives, Compressed is only shown for the first file.
|
|
// 2024-06-14 16:23:14 ....A 225863 595992954 IDP02S02_Steak/01.jpg
|
|
// 2024-06-14 16:26:30 ....A 188040 IDP02S02_Steak/02.jpg
|
|
const match = trimmed.match(
|
|
/^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\s+(\S+)\s+(\d+)\s+(\d+\s+)?(.+)$/
|
|
);
|
|
|
|
if (match) {
|
|
const [, attr, uncompressedStr, compressedRaw, filePath] = match;
|
|
|
|
// Skip directory entries (D attribute or trailing slash)
|
|
if (attr.startsWith("D") || filePath.endsWith("/") || filePath.endsWith("\\")) continue;
|
|
// Skip entries with 0 size
|
|
if (uncompressedStr === "0") continue;
|
|
|
|
const compressedStr = compressedRaw?.trim() || uncompressedStr;
|
|
const ext = path.extname(filePath).toLowerCase();
|
|
entries.push({
|
|
path: filePath,
|
|
fileName: path.basename(filePath),
|
|
extension: ext ? ext.slice(1) : null,
|
|
compressedSize: BigInt(compressedStr),
|
|
uncompressedSize: BigInt(uncompressedStr),
|
|
crc32: null,
|
|
});
|
|
}
|
|
}
|
|
|
|
return entries;
|
|
}
|