Harden hybrid extract readiness for partial archives
Require near-complete file size checks in Item-Recovery and hybrid ready-set detection so partially downloaded RAR parts are not marked completed and extracted prematurely.
This commit is contained in:
@@ -3636,6 +3636,242 @@ describe("download manager", () => {
|
||||
expect(snapshot.session.items[itemId]?.fullStatus).toBe("Entpacken abgebrochen (wird fortgesetzt)");
|
||||
});
|
||||
|
||||
it("does not recover partial archive files as completed", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
||||
tempDirs.push(root);
|
||||
|
||||
const outputDir = path.join(root, "downloads", "partial-recovery");
|
||||
const extractDir = path.join(root, "extract", "partial-recovery");
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
|
||||
const archivePath = path.join(outputDir, "partial.repack.part1.rar");
|
||||
const totalBytes = 1_000_000;
|
||||
fs.writeFileSync(archivePath, Buffer.alloc(860_000, 1));
|
||||
|
||||
const session = emptySession();
|
||||
const packageId = "partial-recovery-pkg";
|
||||
const itemId = "partial-recovery-item";
|
||||
const createdAt = Date.now() - 20_000;
|
||||
session.packageOrder = [packageId];
|
||||
session.packages[packageId] = {
|
||||
id: packageId,
|
||||
name: "partial-recovery",
|
||||
outputDir,
|
||||
extractDir,
|
||||
status: "downloading",
|
||||
itemIds: [itemId],
|
||||
cancelled: false,
|
||||
enabled: true,
|
||||
createdAt,
|
||||
updatedAt: createdAt
|
||||
};
|
||||
session.items[itemId] = {
|
||||
id: itemId,
|
||||
packageId,
|
||||
url: "https://dummy/partial-recovery",
|
||||
provider: "megadebrid",
|
||||
status: "queued",
|
||||
retries: 0,
|
||||
speedBps: 0,
|
||||
downloadedBytes: 0,
|
||||
totalBytes,
|
||||
progressPercent: 0,
|
||||
fileName: path.basename(archivePath),
|
||||
targetPath: archivePath,
|
||||
resumable: true,
|
||||
attempts: 0,
|
||||
lastError: "",
|
||||
fullStatus: "Wartet",
|
||||
createdAt,
|
||||
updatedAt: createdAt
|
||||
};
|
||||
|
||||
const manager = new DownloadManager(
|
||||
{
|
||||
...defaultSettings(),
|
||||
token: "rd-token",
|
||||
outputDir: path.join(root, "downloads"),
|
||||
extractDir: path.join(root, "extract"),
|
||||
autoExtract: false
|
||||
},
|
||||
session,
|
||||
createStoragePaths(path.join(root, "state"))
|
||||
);
|
||||
|
||||
const internal = manager as unknown as {
|
||||
handlePackagePostProcessing: (packageId: string) => Promise<void>;
|
||||
};
|
||||
await internal.handlePackagePostProcessing(packageId);
|
||||
|
||||
const item = manager.getSnapshot().session.items[itemId];
|
||||
expect(item?.status).toBe("queued");
|
||||
expect(item?.fullStatus).toBe("Wartet");
|
||||
});
|
||||
|
||||
it("recovers near-complete archive files with known size", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
||||
tempDirs.push(root);
|
||||
|
||||
const outputDir = path.join(root, "downloads", "near-complete-recovery");
|
||||
const extractDir = path.join(root, "extract", "near-complete-recovery");
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
|
||||
const archivePath = path.join(outputDir, "near.complete.part1.rar");
|
||||
const totalBytes = 1_000_000;
|
||||
const fileSize = 996_000;
|
||||
fs.writeFileSync(archivePath, Buffer.alloc(fileSize, 2));
|
||||
|
||||
const session = emptySession();
|
||||
const packageId = "near-complete-recovery-pkg";
|
||||
const itemId = "near-complete-recovery-item";
|
||||
const createdAt = Date.now() - 20_000;
|
||||
session.packageOrder = [packageId];
|
||||
session.packages[packageId] = {
|
||||
id: packageId,
|
||||
name: "near-complete-recovery",
|
||||
outputDir,
|
||||
extractDir,
|
||||
status: "downloading",
|
||||
itemIds: [itemId],
|
||||
cancelled: false,
|
||||
enabled: true,
|
||||
createdAt,
|
||||
updatedAt: createdAt
|
||||
};
|
||||
session.items[itemId] = {
|
||||
id: itemId,
|
||||
packageId,
|
||||
url: "https://dummy/near-complete-recovery",
|
||||
provider: "megadebrid",
|
||||
status: "queued",
|
||||
retries: 0,
|
||||
speedBps: 0,
|
||||
downloadedBytes: 0,
|
||||
totalBytes,
|
||||
progressPercent: 0,
|
||||
fileName: path.basename(archivePath),
|
||||
targetPath: archivePath,
|
||||
resumable: true,
|
||||
attempts: 0,
|
||||
lastError: "",
|
||||
fullStatus: "Wartet",
|
||||
createdAt,
|
||||
updatedAt: createdAt
|
||||
};
|
||||
|
||||
const manager = new DownloadManager(
|
||||
{
|
||||
...defaultSettings(),
|
||||
token: "rd-token",
|
||||
outputDir: path.join(root, "downloads"),
|
||||
extractDir: path.join(root, "extract"),
|
||||
autoExtract: false
|
||||
},
|
||||
session,
|
||||
createStoragePaths(path.join(root, "state"))
|
||||
);
|
||||
|
||||
const internal = manager as unknown as {
|
||||
handlePackagePostProcessing: (packageId: string) => Promise<void>;
|
||||
};
|
||||
await internal.handlePackagePostProcessing(packageId);
|
||||
|
||||
const item = manager.getSnapshot().session.items[itemId];
|
||||
expect(item?.status).toBe("completed");
|
||||
expect(item?.downloadedBytes).toBe(fileSize);
|
||||
});
|
||||
|
||||
it("skips hybrid-ready multipart archives when a completed part is still too small", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
||||
tempDirs.push(root);
|
||||
|
||||
const outputDir = path.join(root, "downloads", "hybrid-size-guard");
|
||||
const extractDir = path.join(root, "extract", "hybrid-size-guard");
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
|
||||
const part1 = path.join(outputDir, "show.s01e01.part1.rar");
|
||||
const part2 = path.join(outputDir, "show.s01e01.part2.rar");
|
||||
fs.writeFileSync(part1, Buffer.alloc(900_000, 3));
|
||||
fs.writeFileSync(part2, Buffer.alloc(700_000, 4));
|
||||
|
||||
const session = emptySession();
|
||||
const packageId = "hybrid-size-guard-pkg";
|
||||
const createdAt = Date.now() - 20_000;
|
||||
session.packageOrder = [packageId];
|
||||
session.packages[packageId] = {
|
||||
id: packageId,
|
||||
name: "hybrid-size-guard",
|
||||
outputDir,
|
||||
extractDir,
|
||||
status: "downloading",
|
||||
itemIds: ["hybrid-size-guard-item-1", "hybrid-size-guard-item-2"],
|
||||
cancelled: false,
|
||||
enabled: true,
|
||||
createdAt,
|
||||
updatedAt: createdAt
|
||||
};
|
||||
session.items["hybrid-size-guard-item-1"] = {
|
||||
id: "hybrid-size-guard-item-1",
|
||||
packageId,
|
||||
url: "https://dummy/hybrid-size-guard/1",
|
||||
provider: "megadebrid",
|
||||
status: "completed",
|
||||
retries: 0,
|
||||
speedBps: 0,
|
||||
downloadedBytes: 900_000,
|
||||
totalBytes: 1_000_000,
|
||||
progressPercent: 100,
|
||||
fileName: path.basename(part1),
|
||||
targetPath: part1,
|
||||
resumable: true,
|
||||
attempts: 1,
|
||||
lastError: "",
|
||||
fullStatus: "Entpacken - Ausstehend",
|
||||
createdAt,
|
||||
updatedAt: createdAt
|
||||
};
|
||||
session.items["hybrid-size-guard-item-2"] = {
|
||||
id: "hybrid-size-guard-item-2",
|
||||
packageId,
|
||||
url: "https://dummy/hybrid-size-guard/2",
|
||||
provider: "megadebrid",
|
||||
status: "completed",
|
||||
retries: 0,
|
||||
speedBps: 0,
|
||||
downloadedBytes: 700_000,
|
||||
totalBytes: 700_000,
|
||||
progressPercent: 100,
|
||||
fileName: path.basename(part2),
|
||||
targetPath: part2,
|
||||
resumable: true,
|
||||
attempts: 1,
|
||||
lastError: "",
|
||||
fullStatus: "Entpacken - Ausstehend",
|
||||
createdAt,
|
||||
updatedAt: createdAt
|
||||
};
|
||||
|
||||
const manager = new DownloadManager(
|
||||
{
|
||||
...defaultSettings(),
|
||||
token: "rd-token",
|
||||
outputDir: path.join(root, "downloads"),
|
||||
extractDir: path.join(root, "extract"),
|
||||
autoExtract: true,
|
||||
hybridExtract: true
|
||||
},
|
||||
session,
|
||||
createStoragePaths(path.join(root, "state"))
|
||||
);
|
||||
|
||||
const internal = manager as unknown as {
|
||||
session: ReturnType<typeof emptySession>;
|
||||
findReadyArchiveSets: (pkg: ReturnType<typeof emptySession>["packages"][string]) => Promise<Set<string>>;
|
||||
};
|
||||
const ready = await internal.findReadyArchiveSets(internal.session.packages[packageId]);
|
||||
expect(ready.size).toBe(0);
|
||||
});
|
||||
|
||||
it("recovers pending extraction on startup for completed package", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
||||
tempDirs.push(root);
|
||||
|
||||
Reference in New Issue
Block a user