Refactor: Extractor in 18 Sektionen reorganisiert
This commit is contained in:
+86
-86
@@ -1,86 +1,86 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { encryptBackup, decryptBackup } from "../src/main/backup-crypto";
|
||||
|
||||
describe("backup-crypto", () => {
|
||||
it("encrypts and decrypts a round-trip correctly", () => {
|
||||
const original = JSON.stringify({
|
||||
version: 2,
|
||||
settings: { token: "my-secret-api-key", outputDir: "C:\\Downloads" },
|
||||
session: { packages: {}, items: {} },
|
||||
history: [{ id: "h1", name: "Test" }]
|
||||
});
|
||||
|
||||
const encrypted = encryptBackup(original);
|
||||
const decrypted = decryptBackup(encrypted);
|
||||
expect(decrypted).toBe(original);
|
||||
});
|
||||
|
||||
it("produces binary output that is not plaintext readable", () => {
|
||||
const secret = "super-secret-token-12345";
|
||||
const plaintext = JSON.stringify({ settings: { token: secret } });
|
||||
const encrypted = encryptBackup(plaintext);
|
||||
|
||||
// The encrypted buffer should NOT contain the secret in plaintext
|
||||
expect(encrypted.toString("utf8")).not.toContain(secret);
|
||||
expect(encrypted.toString("latin1")).not.toContain(secret);
|
||||
});
|
||||
|
||||
it("starts with the MDD1 magic bytes", () => {
|
||||
const encrypted = encryptBackup("test");
|
||||
expect(encrypted.subarray(0, 4).toString("utf8")).toBe("MDD1");
|
||||
});
|
||||
|
||||
it("produces different ciphertext for the same input (random IV)", () => {
|
||||
const plaintext = "same input data";
|
||||
const a = encryptBackup(plaintext);
|
||||
const b = encryptBackup(plaintext);
|
||||
// IVs are different, so full buffers must differ
|
||||
expect(a.equals(b)).toBe(false);
|
||||
// But both decrypt to the same plaintext
|
||||
expect(decryptBackup(a)).toBe(plaintext);
|
||||
expect(decryptBackup(b)).toBe(plaintext);
|
||||
});
|
||||
|
||||
it("throws on truncated data", () => {
|
||||
const encrypted = encryptBackup("test data");
|
||||
const truncated = encrypted.subarray(0, 10);
|
||||
expect(() => decryptBackup(truncated)).toThrow();
|
||||
});
|
||||
|
||||
it("throws on corrupted ciphertext", () => {
|
||||
const encrypted = encryptBackup("test data");
|
||||
// Flip a byte in the ciphertext area
|
||||
const corrupted = Buffer.from(encrypted);
|
||||
corrupted[corrupted.length - 1] ^= 0xff;
|
||||
expect(() => decryptBackup(corrupted)).toThrow();
|
||||
});
|
||||
|
||||
it("throws on wrong magic bytes", () => {
|
||||
const encrypted = encryptBackup("test data");
|
||||
const wrongMagic = Buffer.from(encrypted);
|
||||
wrongMagic[0] = 0x00;
|
||||
expect(() => decryptBackup(wrongMagic)).toThrow(/Signatur/);
|
||||
});
|
||||
|
||||
it("throws on empty buffer", () => {
|
||||
expect(() => decryptBackup(Buffer.alloc(0))).toThrow();
|
||||
});
|
||||
|
||||
it("handles large payloads", () => {
|
||||
const large = JSON.stringify({ data: "x".repeat(1_000_000) });
|
||||
const encrypted = encryptBackup(large);
|
||||
const decrypted = decryptBackup(encrypted);
|
||||
expect(decrypted).toBe(large);
|
||||
});
|
||||
|
||||
it("handles unicode content", () => {
|
||||
const unicode = JSON.stringify({ name: "Ünïcödé 日本語 🎉", path: "C:\\Benutzer\\Ö" });
|
||||
const encrypted = encryptBackup(unicode);
|
||||
expect(decryptBackup(encrypted)).toBe(unicode);
|
||||
});
|
||||
|
||||
it("handles empty string round-trip", () => {
|
||||
const encrypted = encryptBackup("");
|
||||
expect(decryptBackup(encrypted)).toBe("");
|
||||
});
|
||||
});
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { encryptBackup, decryptBackup } from "../src/main/backup-crypto";
|
||||
|
||||
describe("backup-crypto", () => {
|
||||
it("encrypts and decrypts a round-trip correctly", () => {
|
||||
const original = JSON.stringify({
|
||||
version: 2,
|
||||
settings: { token: "my-secret-api-key", outputDir: "C:\\Downloads" },
|
||||
session: { packages: {}, items: {} },
|
||||
history: [{ id: "h1", name: "Test" }]
|
||||
});
|
||||
|
||||
const encrypted = encryptBackup(original);
|
||||
const decrypted = decryptBackup(encrypted);
|
||||
expect(decrypted).toBe(original);
|
||||
});
|
||||
|
||||
it("produces binary output that is not plaintext readable", () => {
|
||||
const secret = "super-secret-token-12345";
|
||||
const plaintext = JSON.stringify({ settings: { token: secret } });
|
||||
const encrypted = encryptBackup(plaintext);
|
||||
|
||||
// The encrypted buffer should NOT contain the secret in plaintext
|
||||
expect(encrypted.toString("utf8")).not.toContain(secret);
|
||||
expect(encrypted.toString("latin1")).not.toContain(secret);
|
||||
});
|
||||
|
||||
it("starts with the MDD1 magic bytes", () => {
|
||||
const encrypted = encryptBackup("test");
|
||||
expect(encrypted.subarray(0, 4).toString("utf8")).toBe("MDD1");
|
||||
});
|
||||
|
||||
it("produces different ciphertext for the same input (random IV)", () => {
|
||||
const plaintext = "same input data";
|
||||
const a = encryptBackup(plaintext);
|
||||
const b = encryptBackup(plaintext);
|
||||
// IVs are different, so full buffers must differ
|
||||
expect(a.equals(b)).toBe(false);
|
||||
// But both decrypt to the same plaintext
|
||||
expect(decryptBackup(a)).toBe(plaintext);
|
||||
expect(decryptBackup(b)).toBe(plaintext);
|
||||
});
|
||||
|
||||
it("throws on truncated data", () => {
|
||||
const encrypted = encryptBackup("test data");
|
||||
const truncated = encrypted.subarray(0, 10);
|
||||
expect(() => decryptBackup(truncated)).toThrow();
|
||||
});
|
||||
|
||||
it("throws on corrupted ciphertext", () => {
|
||||
const encrypted = encryptBackup("test data");
|
||||
// Flip a byte in the ciphertext area
|
||||
const corrupted = Buffer.from(encrypted);
|
||||
corrupted[corrupted.length - 1] ^= 0xff;
|
||||
expect(() => decryptBackup(corrupted)).toThrow();
|
||||
});
|
||||
|
||||
it("throws on wrong magic bytes", () => {
|
||||
const encrypted = encryptBackup("test data");
|
||||
const wrongMagic = Buffer.from(encrypted);
|
||||
wrongMagic[0] = 0x00;
|
||||
expect(() => decryptBackup(wrongMagic)).toThrow(/Signatur/);
|
||||
});
|
||||
|
||||
it("throws on empty buffer", () => {
|
||||
expect(() => decryptBackup(Buffer.alloc(0))).toThrow();
|
||||
});
|
||||
|
||||
it("handles large payloads", () => {
|
||||
const large = JSON.stringify({ data: "x".repeat(1_000_000) });
|
||||
const encrypted = encryptBackup(large);
|
||||
const decrypted = decryptBackup(encrypted);
|
||||
expect(decrypted).toBe(large);
|
||||
});
|
||||
|
||||
it("handles unicode content", () => {
|
||||
const unicode = JSON.stringify({ name: "Ünïcödé 日本語 🎉", path: "C:\\Benutzer\\Ö" });
|
||||
const encrypted = encryptBackup(unicode);
|
||||
expect(decryptBackup(encrypted)).toBe(unicode);
|
||||
});
|
||||
|
||||
it("handles empty string round-trip", () => {
|
||||
const encrypted = encryptBackup("");
|
||||
expect(decryptBackup(encrypted)).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
+109
-109
@@ -1,109 +1,109 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { cleanupCancelledPackageArtifacts, removeDownloadLinkArtifacts, removeSampleArtifacts } from "../src/main/cleanup";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
for (const dir of tempDirs.splice(0)) {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe("cleanup", () => {
|
||||
it("removes archive artifacts but keeps media", () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-clean-"));
|
||||
tempDirs.push(dir);
|
||||
fs.writeFileSync(path.join(dir, "release.part1.rar"), "x");
|
||||
fs.writeFileSync(path.join(dir, "movie.mkv"), "x");
|
||||
|
||||
const removed = cleanupCancelledPackageArtifacts(dir);
|
||||
expect(removed).toBeGreaterThan(0);
|
||||
expect(fs.existsSync(path.join(dir, "release.part1.rar"))).toBe(false);
|
||||
expect(fs.existsSync(path.join(dir, "movie.mkv"))).toBe(true);
|
||||
});
|
||||
|
||||
it("removes sample artifacts and link files", async () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-clean-"));
|
||||
tempDirs.push(dir);
|
||||
fs.mkdirSync(path.join(dir, "Samples"), { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, "Samples", "demo-sample.mkv"), "x");
|
||||
fs.writeFileSync(path.join(dir, "download_links.txt"), "https://example.com/a\n");
|
||||
|
||||
const links = await removeDownloadLinkArtifacts(dir);
|
||||
const samples = await removeSampleArtifacts(dir);
|
||||
expect(links).toBeGreaterThan(0);
|
||||
expect(samples.files + samples.dirs).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("cleans up archive files in nested directories", () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-clean-"));
|
||||
tempDirs.push(dir);
|
||||
|
||||
// Create nested directory structure with archive files
|
||||
const sub1 = path.join(dir, "season1");
|
||||
const sub2 = path.join(dir, "season1", "extras");
|
||||
fs.mkdirSync(sub2, { recursive: true });
|
||||
|
||||
fs.writeFileSync(path.join(sub1, "episode.part1.rar"), "x");
|
||||
fs.writeFileSync(path.join(sub1, "episode.part2.rar"), "x");
|
||||
fs.writeFileSync(path.join(sub2, "bonus.zip"), "x");
|
||||
fs.writeFileSync(path.join(sub2, "bonus.7z"), "x");
|
||||
// Non-archive files should be kept
|
||||
fs.writeFileSync(path.join(sub1, "video.mkv"), "real content");
|
||||
fs.writeFileSync(path.join(sub2, "subtitle.srt"), "subtitle content");
|
||||
|
||||
const removed = cleanupCancelledPackageArtifacts(dir);
|
||||
expect(removed).toBe(4); // 2 rar parts + zip + 7z
|
||||
expect(fs.existsSync(path.join(sub1, "episode.part1.rar"))).toBe(false);
|
||||
expect(fs.existsSync(path.join(sub1, "episode.part2.rar"))).toBe(false);
|
||||
expect(fs.existsSync(path.join(sub2, "bonus.zip"))).toBe(false);
|
||||
expect(fs.existsSync(path.join(sub2, "bonus.7z"))).toBe(false);
|
||||
// Non-archives kept
|
||||
expect(fs.existsSync(path.join(sub1, "video.mkv"))).toBe(true);
|
||||
expect(fs.existsSync(path.join(sub2, "subtitle.srt"))).toBe(true);
|
||||
});
|
||||
|
||||
it("detects link artifacts by URL content in text files", async () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-clean-"));
|
||||
tempDirs.push(dir);
|
||||
|
||||
// File with link-like name containing URLs should be removed
|
||||
fs.writeFileSync(path.join(dir, "download_links.txt"), "https://rapidgator.net/file/abc123\nhttps://uploaded.net/file/def456\n");
|
||||
// File with link-like name but no URLs should be kept
|
||||
fs.writeFileSync(path.join(dir, "my_downloads.txt"), "Just some random text without URLs");
|
||||
// Regular text file that doesn't match the link pattern should be kept
|
||||
fs.writeFileSync(path.join(dir, "readme.txt"), "https://example.com");
|
||||
// .url files should always be removed
|
||||
fs.writeFileSync(path.join(dir, "bookmark.url"), "[InternetShortcut]\nURL=https://example.com");
|
||||
// .dlc files should always be removed
|
||||
fs.writeFileSync(path.join(dir, "container.dlc"), "encrypted-data");
|
||||
|
||||
const removed = await removeDownloadLinkArtifacts(dir);
|
||||
expect(removed).toBeGreaterThanOrEqual(3); // download_links.txt + bookmark.url + container.dlc
|
||||
expect(fs.existsSync(path.join(dir, "download_links.txt"))).toBe(false);
|
||||
expect(fs.existsSync(path.join(dir, "bookmark.url"))).toBe(false);
|
||||
expect(fs.existsSync(path.join(dir, "container.dlc"))).toBe(false);
|
||||
// Non-matching files should be kept
|
||||
expect(fs.existsSync(path.join(dir, "readme.txt"))).toBe(true);
|
||||
});
|
||||
|
||||
it("does not recurse into sample symlink or junction targets", async () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-clean-"));
|
||||
const external = fs.mkdtempSync(path.join(os.tmpdir(), "rd-clean-ext-"));
|
||||
tempDirs.push(dir, external);
|
||||
|
||||
const outsideFile = path.join(external, "outside-sample.mkv");
|
||||
fs.writeFileSync(outsideFile, "keep", "utf8");
|
||||
|
||||
const linkedSampleDir = path.join(dir, "sample");
|
||||
const linkType: fs.symlink.Type = process.platform === "win32" ? "junction" : "dir";
|
||||
fs.symlinkSync(external, linkedSampleDir, linkType);
|
||||
|
||||
const result = await removeSampleArtifacts(dir);
|
||||
expect(result.files).toBe(0);
|
||||
expect(fs.existsSync(outsideFile)).toBe(true);
|
||||
});
|
||||
});
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { cleanupCancelledPackageArtifacts, removeDownloadLinkArtifacts, removeSampleArtifacts } from "../src/main/cleanup";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
for (const dir of tempDirs.splice(0)) {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe("cleanup", () => {
|
||||
it("removes archive artifacts but keeps media", () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-clean-"));
|
||||
tempDirs.push(dir);
|
||||
fs.writeFileSync(path.join(dir, "release.part1.rar"), "x");
|
||||
fs.writeFileSync(path.join(dir, "movie.mkv"), "x");
|
||||
|
||||
const removed = cleanupCancelledPackageArtifacts(dir);
|
||||
expect(removed).toBeGreaterThan(0);
|
||||
expect(fs.existsSync(path.join(dir, "release.part1.rar"))).toBe(false);
|
||||
expect(fs.existsSync(path.join(dir, "movie.mkv"))).toBe(true);
|
||||
});
|
||||
|
||||
it("removes sample artifacts and link files", async () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-clean-"));
|
||||
tempDirs.push(dir);
|
||||
fs.mkdirSync(path.join(dir, "Samples"), { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, "Samples", "demo-sample.mkv"), "x");
|
||||
fs.writeFileSync(path.join(dir, "download_links.txt"), "https://example.com/a\n");
|
||||
|
||||
const links = await removeDownloadLinkArtifacts(dir);
|
||||
const samples = await removeSampleArtifacts(dir);
|
||||
expect(links).toBeGreaterThan(0);
|
||||
expect(samples.files + samples.dirs).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("cleans up archive files in nested directories", () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-clean-"));
|
||||
tempDirs.push(dir);
|
||||
|
||||
// Create nested directory structure with archive files
|
||||
const sub1 = path.join(dir, "season1");
|
||||
const sub2 = path.join(dir, "season1", "extras");
|
||||
fs.mkdirSync(sub2, { recursive: true });
|
||||
|
||||
fs.writeFileSync(path.join(sub1, "episode.part1.rar"), "x");
|
||||
fs.writeFileSync(path.join(sub1, "episode.part2.rar"), "x");
|
||||
fs.writeFileSync(path.join(sub2, "bonus.zip"), "x");
|
||||
fs.writeFileSync(path.join(sub2, "bonus.7z"), "x");
|
||||
// Non-archive files should be kept
|
||||
fs.writeFileSync(path.join(sub1, "video.mkv"), "real content");
|
||||
fs.writeFileSync(path.join(sub2, "subtitle.srt"), "subtitle content");
|
||||
|
||||
const removed = cleanupCancelledPackageArtifacts(dir);
|
||||
expect(removed).toBe(4); // 2 rar parts + zip + 7z
|
||||
expect(fs.existsSync(path.join(sub1, "episode.part1.rar"))).toBe(false);
|
||||
expect(fs.existsSync(path.join(sub1, "episode.part2.rar"))).toBe(false);
|
||||
expect(fs.existsSync(path.join(sub2, "bonus.zip"))).toBe(false);
|
||||
expect(fs.existsSync(path.join(sub2, "bonus.7z"))).toBe(false);
|
||||
// Non-archives kept
|
||||
expect(fs.existsSync(path.join(sub1, "video.mkv"))).toBe(true);
|
||||
expect(fs.existsSync(path.join(sub2, "subtitle.srt"))).toBe(true);
|
||||
});
|
||||
|
||||
it("detects link artifacts by URL content in text files", async () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-clean-"));
|
||||
tempDirs.push(dir);
|
||||
|
||||
// File with link-like name containing URLs should be removed
|
||||
fs.writeFileSync(path.join(dir, "download_links.txt"), "https://rapidgator.net/file/abc123\nhttps://uploaded.net/file/def456\n");
|
||||
// File with link-like name but no URLs should be kept
|
||||
fs.writeFileSync(path.join(dir, "my_downloads.txt"), "Just some random text without URLs");
|
||||
// Regular text file that doesn't match the link pattern should be kept
|
||||
fs.writeFileSync(path.join(dir, "readme.txt"), "https://example.com");
|
||||
// .url files should always be removed
|
||||
fs.writeFileSync(path.join(dir, "bookmark.url"), "[InternetShortcut]\nURL=https://example.com");
|
||||
// .dlc files should always be removed
|
||||
fs.writeFileSync(path.join(dir, "container.dlc"), "encrypted-data");
|
||||
|
||||
const removed = await removeDownloadLinkArtifacts(dir);
|
||||
expect(removed).toBeGreaterThanOrEqual(3); // download_links.txt + bookmark.url + container.dlc
|
||||
expect(fs.existsSync(path.join(dir, "download_links.txt"))).toBe(false);
|
||||
expect(fs.existsSync(path.join(dir, "bookmark.url"))).toBe(false);
|
||||
expect(fs.existsSync(path.join(dir, "container.dlc"))).toBe(false);
|
||||
// Non-matching files should be kept
|
||||
expect(fs.existsSync(path.join(dir, "readme.txt"))).toBe(true);
|
||||
});
|
||||
|
||||
it("does not recurse into sample symlink or junction targets", async () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-clean-"));
|
||||
const external = fs.mkdtempSync(path.join(os.tmpdir(), "rd-clean-ext-"));
|
||||
tempDirs.push(dir, external);
|
||||
|
||||
const outsideFile = path.join(external, "outside-sample.mkv");
|
||||
fs.writeFileSync(outsideFile, "keep", "utf8");
|
||||
|
||||
const linkedSampleDir = path.join(dir, "sample");
|
||||
const linkType: fs.symlink.Type = process.platform === "win32" ? "junction" : "dir";
|
||||
fs.symlinkSync(external, linkedSampleDir, linkType);
|
||||
|
||||
const result = await removeSampleArtifacts(dir);
|
||||
expect(result.files).toBe(0);
|
||||
expect(fs.existsSync(outsideFile)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
+998
-998
File diff suppressed because it is too large
Load Diff
+5447
-5447
File diff suppressed because it is too large
Load Diff
+491
-491
File diff suppressed because it is too large
Load Diff
+546
-546
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user