Offer archive-set or whole-package offline cleanup

This commit is contained in:
Sucukdeluxe
2026-09-04 21:42:55 +02:00
parent 8b061cdbc1
commit 2a721dc955
12 changed files with 207 additions and 72 deletions
+37 -1
View File
@@ -9,6 +9,42 @@ import { getPackagesWithOfflineLinks } from "../src/shared/offline-packages";
import type { SessionState } from "../src/shared/types";
describe("remove packages containing offline links", () => {
it.each([
["episode03.part1.rar", "episode03.part2.rar"],
["episode03.rar", "episode03.r00"],
["episode03.zip.001", "episode03.zip.002"],
["episode03.7z.001", "episode03.7z.002"],
["episode03.001", "episode03.002"]
])("removes the archive set containing offline %s and keeps other episodes", async (first, second) => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "mdd-offline-archive-"));
const manager = new DownloadManager({ ...defaultSettings(), outputDir: root, autoExtract: false }, emptySession(), createStoragePaths(path.join(root, "state")));
try {
const names = [first, second, "episode04.part1.rar", "episode04.part2.rar", "notes.txt"];
manager.addPackages([{ name: "Season", links: names.map((name) => `https://dummy/${name}`), fileNames: names }]);
const session = (manager as unknown as { session: SessionState }).session;
const pkg = session.packages[session.packageOrder[0]];
const ids = [...pkg.itemIds];
fs.mkdirSync(pkg.outputDir, { recursive: true });
for (const [index, id] of ids.entries()) {
session.items[id].fileName = names[index];
session.items[id].targetPath = path.join(pkg.outputDir, names[index]);
session.items[id].onlineStatus = index === 1 ? "offline" : "online";
fs.writeFileSync(session.items[id].targetPath, names[index]);
}
session.items[ids[0]].status = "completed";
expect(manager.removeOfflinePackages([pkg.id])).toBe(1);
await (manager as any).cleanupQueue;
expect(pkg.itemIds).toEqual(ids.slice(2));
expect(session.items[ids[0]]).toBeUndefined();
expect(session.items[ids[1]]).toBeUndefined();
expect(session.packageOrder).toEqual([pkg.id]);
for (const name of names) expect(fs.readFileSync(path.join(pkg.outputDir, name), "utf8")).toBe(name);
} finally {
manager.clearPersistTimer();
fs.rmSync(root, { recursive: true, force: true });
}
});
it("removes complete partial and offline packages, preserves files and rechecks the confirmed set", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "mdd-offline-removal-"));
const manager = new DownloadManager({ ...defaultSettings(), outputDir: root, autoExtract: false }, emptySession(), createStoragePaths(path.join(root, "state")));
@@ -30,7 +66,7 @@ describe("remove packages containing offline links", () => {
for (const file of preservedPaths) fs.writeFileSync(file, "keep this content");
const active = { abortController: new AbortController(), abortReason: "none" };
(manager as any).activeTasks.set(packages.partial.itemIds[1], active);
expect(manager.removeOfflinePackages([...confirmed, confirmed[0], "missing"])).toBe(2);
expect(manager.removeOfflinePackages([...confirmed, confirmed[0], "missing"], "package")).toBe(2);
await (manager as any).cleanupQueue;
expect(active.abortController.signal.aborted).toBe(true);
expect(active.abortReason).toBe("stop");
+24 -4
View File
@@ -1,5 +1,6 @@
import type { ElectronApi } from "../../src/shared/preload-api";
import { getPackagesWithOfflineLinks } from "../../src/shared/offline-packages";
import { resolveOfflineArchiveItemsFromList } from "../../src/shared/offline-archive-items";
import type { HistoryEntry, RendererSettings, RendererSettingsUpdate } from "../../src/shared/types";
import type { VisualFixture } from "./fixtures";
@@ -14,6 +15,21 @@ export function createVisualElectronApi(
search = typeof window === "undefined" ? "" : window.location.search
): ElectronApi {
const searchParams = new URLSearchParams(search);
if (searchParams.get("offline-cleanup") === "multipart") {
const { session } = fixture.snapshot;
const pkg = session.packages[session.packageOrder[0]];
const originals = pkg.itemIds.map((id) => session.items[id]);
originals.forEach((item, index) => {
item.fileName = `episode03.part${index + 1}.rar`;
item.targetPath = "";
item.onlineStatus = index === 1 ? "offline" : "online";
});
for (let part = 1; part <= 2; part++) {
const id = `${pkg.id}-episode04-${part}`;
session.items[id] = { ...originals[0], id, fileName: `episode04.part${part}.rar`, targetPath: "", onlineStatus: "online" };
pkg.itemIds.push(id);
}
}
const historyState = searchParams.get("history-state");
if (searchParams.get("animations") === "off") fixture.snapshot.settings.animatePackageDisclosure = false;
let archivePasswordList = searchParams.get("archive-passwords") === "configured"
@@ -111,14 +127,18 @@ export function createVisualElectronApi(
fixture.snapshot.session.paused = !fixture.snapshot.session.paused;
return fixture.snapshot.session.paused;
},
removeOfflinePackages: async (packageIds) => {
removeOfflinePackages: async (packageIds, scope = "archive") => {
const { session } = fixture.snapshot;
const candidates = getPackagesWithOfflineLinks(packageIds, session.packages, session.items);
for (const id of candidates) {
for (const itemId of session.packages[id].itemIds) delete session.items[itemId];
delete session.packages[id];
const pkg = session.packages[id];
const items = pkg.itemIds.map((itemId) => session.items[itemId]).filter(Boolean);
const targets = scope === "package" ? items : items.filter((item) => item.onlineStatus === "offline").flatMap((item) => [item, ...resolveOfflineArchiveItemsFromList(item.fileName, items)]);
for (const item of targets) delete session.items[item.id];
pkg.itemIds = pkg.itemIds.filter((itemId) => session.items[itemId]);
if (pkg.itemIds.length === 0) delete session.packages[id];
}
session.packageOrder = session.packageOrder.filter((id) => !candidates.includes(id));
session.packageOrder = session.packageOrder.filter((id) => session.packages[id]);
emitStateUpdate();
return candidates.length;
},