fix: validate extraction ownership before writes
Reject symlink and reparse boundaries before internal ZIP or JVM outputs are opened, track opened, committed, partial, removed output lifecycle events, and convert output callback failures into controlled extractor failures without poisoning the JVM daemon. Gate legacy recovery behind an atomically created package-generation owner marker and keep unmarked reused or shared directories fail closed. Parse native RAR output from strictly verified locale-independent candidates while retaining ambiguous rename rejection.
This commit is contained in:
+260
-183
@@ -25,9 +25,23 @@ import { resetVideoToolingCache } from "../src/main/video-processor";
|
||||
import { createDownloadHealthState, evaluateDownloadHealth } from "../src/main/download-health-monitor";
|
||||
import type { AppSettings, DownloadItem, HistoryEntry, PackageEntry } from "../src/shared/types";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
const tempDirs: string[] = [];
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
function writePackageOutputOwnerMarker(pkg: PackageEntry): void {
|
||||
const ownerId = crypto.randomUUID().toLowerCase();
|
||||
const generation = Math.max(1, Number(pkg.resultGeneration || 1));
|
||||
pkg.outputOwnerId = ownerId;
|
||||
pkg.outputOwnerGeneration = generation;
|
||||
fs.mkdirSync(pkg.extractDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(pkg.extractDir, ".rd-package-output-owner-v1.json"), JSON.stringify({
|
||||
version: 1,
|
||||
packageId: pkg.id,
|
||||
generation,
|
||||
ownerId
|
||||
}));
|
||||
}
|
||||
|
||||
describe("runWithLimitedConcurrency", () => {
|
||||
it("processes the full batch without exceeding the configured worker count", async () => {
|
||||
let active = 0;
|
||||
@@ -6760,7 +6774,6 @@ describe("download manager", () => {
|
||||
createdAt,
|
||||
updatedAt: createdAt
|
||||
};
|
||||
|
||||
const manager = new DownloadManager(
|
||||
{
|
||||
...defaultSettings(),
|
||||
@@ -11596,17 +11609,18 @@ describe("download manager", () => {
|
||||
downloadedBytes: 123,
|
||||
totalBytes: 123,
|
||||
progressPercent: 100,
|
||||
fileName: "missing-source-ok.part01.rar",
|
||||
fileName: "missing-source-ok.part01.rar",
|
||||
targetPath: path.join(outputDir, "missing-source-ok.part01.rar"),
|
||||
resumable: true,
|
||||
attempts: 1,
|
||||
lastError: "",
|
||||
fullStatus: "Fertig (123 B)",
|
||||
createdAt,
|
||||
updatedAt: createdAt
|
||||
};
|
||||
|
||||
const manager = new DownloadManager(
|
||||
updatedAt: createdAt
|
||||
};
|
||||
writePackageOutputOwnerMarker(session.packages[packageId]);
|
||||
|
||||
const manager = new DownloadManager(
|
||||
{
|
||||
...defaultSettings(),
|
||||
token: "rd-token",
|
||||
@@ -11715,9 +11729,10 @@ describe("download manager", () => {
|
||||
fs.writeFileSync(path.join(extractDir, "Season 1", "Episode01.mkv"), "video", "utf8");
|
||||
fs.writeFileSync(path.join(extractDir, "Season 1", "episode.links.txt"), "https://example.com/file", "utf8");
|
||||
fs.writeFileSync(path.join(extractDir, "Season 1", "sample", "sample.mkv"), "sample-video", "utf8");
|
||||
fs.writeFileSync(path.join(extractDir, "Season 1", "sample", "readme.txt"), "sample-text", "utf8");
|
||||
|
||||
const mkvLibraryDir = path.join(root, "mkv-library");
|
||||
fs.writeFileSync(path.join(extractDir, "Season 1", "sample", "readme.txt"), "sample-text", "utf8");
|
||||
writePackageOutputOwnerMarker(session.packages[packageId]);
|
||||
|
||||
const mkvLibraryDir = path.join(root, "mkv-library");
|
||||
const manager = new DownloadManager(
|
||||
{
|
||||
...defaultSettings(),
|
||||
@@ -11764,9 +11779,10 @@ describe("download manager", () => {
|
||||
} = createCompletedArchiveSession(root, packageName, sourceFileName);
|
||||
|
||||
session.packages[packageId].status = "completed";
|
||||
session.items[itemId].fullStatus = "Entpackt - Done (<1s)";
|
||||
fs.mkdirSync(extractDir, { recursive: true });
|
||||
fs.writeFileSync(originalExtractedPath, "video", "utf8");
|
||||
session.items[itemId].fullStatus = "Entpackt - Done (<1s)";
|
||||
fs.mkdirSync(extractDir, { recursive: true });
|
||||
fs.writeFileSync(originalExtractedPath, "video", "utf8");
|
||||
writePackageOutputOwnerMarker(session.packages[packageId]);
|
||||
|
||||
const manager = new DownloadManager(
|
||||
{
|
||||
@@ -12664,20 +12680,21 @@ describe("download manager", () => {
|
||||
const packageId = `${packageName}-pkg`;
|
||||
const createdAt = Date.now() - 20_000;
|
||||
session.packageOrder = [packageId];
|
||||
session.packages[packageId] = {
|
||||
id: packageId,
|
||||
name: packageName,
|
||||
outputDir,
|
||||
extractDir,
|
||||
status: "downloading",
|
||||
itemIds: [],
|
||||
cancelled: false,
|
||||
enabled: true,
|
||||
createdAt,
|
||||
updatedAt: createdAt
|
||||
};
|
||||
|
||||
const mkvLibraryDir = path.join(root, "mkv-library");
|
||||
session.packages[packageId] = {
|
||||
id: packageId,
|
||||
name: packageName,
|
||||
outputDir,
|
||||
extractDir,
|
||||
status: "downloading",
|
||||
itemIds: [],
|
||||
cancelled: false,
|
||||
enabled: true,
|
||||
createdAt,
|
||||
updatedAt: createdAt
|
||||
};
|
||||
writePackageOutputOwnerMarker(session.packages[packageId]);
|
||||
|
||||
const mkvLibraryDir = path.join(root, "mkv-library");
|
||||
const manager = new DownloadManager(
|
||||
{
|
||||
...defaultSettings(),
|
||||
@@ -12718,28 +12735,29 @@ describe("download manager", () => {
|
||||
const episodeFolder = "Herzflimmern.Die.Klinik.am.See.S07E12.German.720p.Webrip.x264-TVARCHiV";
|
||||
const epDir = path.join(extractDir, episodeFolder);
|
||||
fs.mkdirSync(epDir, { recursive: true });
|
||||
const rawName = "tvarchiv.herzflimmern.die.klinik.am.see.s07e12-720.mkv";
|
||||
const rawPath = path.join(epDir, rawName);
|
||||
fs.writeFileSync(rawPath, Buffer.alloc(4096, 7));
|
||||
|
||||
const session = emptySession();
|
||||
const packageId = `${packageName}-pkg`;
|
||||
const createdAt = Date.now() - 60_000;
|
||||
session.packageOrder = [packageId];
|
||||
session.packages[packageId] = {
|
||||
id: packageId,
|
||||
name: packageName,
|
||||
outputDir,
|
||||
extractDir,
|
||||
status: "completed",
|
||||
itemIds: [],
|
||||
cancelled: false,
|
||||
enabled: true,
|
||||
createdAt,
|
||||
updatedAt: createdAt
|
||||
};
|
||||
|
||||
const mkvLibraryDir = path.join(root, "mkv-library");
|
||||
const rawName = "tvarchiv.herzflimmern.die.klinik.am.see.s07e12-720.mkv";
|
||||
const rawPath = path.join(epDir, rawName);
|
||||
fs.writeFileSync(rawPath, Buffer.alloc(4096, 7));
|
||||
|
||||
const session = emptySession();
|
||||
const packageId = `${packageName}-pkg`;
|
||||
const createdAt = Date.now() - 60_000;
|
||||
session.packageOrder = [packageId];
|
||||
session.packages[packageId] = {
|
||||
id: packageId,
|
||||
name: packageName,
|
||||
outputDir,
|
||||
extractDir,
|
||||
status: "completed",
|
||||
itemIds: [],
|
||||
cancelled: false,
|
||||
enabled: true,
|
||||
createdAt,
|
||||
updatedAt: createdAt
|
||||
};
|
||||
writePackageOutputOwnerMarker(session.packages[packageId]);
|
||||
|
||||
const mkvLibraryDir = path.join(root, "mkv-library");
|
||||
const manager = new DownloadManager(
|
||||
{
|
||||
...defaultSettings(),
|
||||
@@ -12831,30 +12849,31 @@ describe("download manager", () => {
|
||||
const packageName = "Revenge.2011.S04.GERMAN.DL.720p.WEB.x264-TSCC";
|
||||
const outputDir = path.join(root, "downloads", packageName);
|
||||
const extractDir = path.join(root, "extract", packageName);
|
||||
const episodeFolder = "Revenge.2011.S04E19.Interview.GERMAN.DL.720p.WEB.x264-TSCC";
|
||||
const epDir = path.join(extractDir, episodeFolder);
|
||||
fs.mkdirSync(epDir, { recursive: true });
|
||||
const epName = `${episodeFolder}.mkv`;
|
||||
fs.writeFileSync(path.join(epDir, epName), Buffer.alloc(4096, 9));
|
||||
|
||||
const session = emptySession();
|
||||
const packageId = `${packageName}-pkg`;
|
||||
const createdAt = Date.now() - 60_000;
|
||||
session.packageOrder = [packageId];
|
||||
session.packages[packageId] = {
|
||||
id: packageId,
|
||||
name: packageName,
|
||||
outputDir,
|
||||
extractDir,
|
||||
status: "completed",
|
||||
itemIds: [],
|
||||
cancelled: false,
|
||||
enabled: true,
|
||||
createdAt,
|
||||
updatedAt: createdAt
|
||||
};
|
||||
|
||||
const mkvLibraryDir = path.join(root, "mkv-library");
|
||||
const episodeFolder = "Revenge.2011.S04E19.Interview.GERMAN.DL.720p.WEB.x264-TSCC";
|
||||
const epDir = path.join(extractDir, episodeFolder);
|
||||
fs.mkdirSync(epDir, { recursive: true });
|
||||
const epName = `${episodeFolder}.mkv`;
|
||||
fs.writeFileSync(path.join(epDir, epName), Buffer.alloc(4096, 9));
|
||||
|
||||
const session = emptySession();
|
||||
const packageId = `${packageName}-pkg`;
|
||||
const createdAt = Date.now() - 60_000;
|
||||
session.packageOrder = [packageId];
|
||||
session.packages[packageId] = {
|
||||
id: packageId,
|
||||
name: packageName,
|
||||
outputDir,
|
||||
extractDir,
|
||||
status: "completed",
|
||||
itemIds: [],
|
||||
cancelled: false,
|
||||
enabled: true,
|
||||
createdAt,
|
||||
updatedAt: createdAt
|
||||
};
|
||||
writePackageOutputOwnerMarker(session.packages[packageId]);
|
||||
|
||||
const mkvLibraryDir = path.join(root, "mkv-library");
|
||||
const manager = new DownloadManager(
|
||||
{
|
||||
...defaultSettings(),
|
||||
@@ -12887,29 +12906,30 @@ describe("download manager", () => {
|
||||
const outputDir = path.join(root, "downloads", packageName);
|
||||
const extractDir = path.join(root, "extract", packageName);
|
||||
fs.mkdirSync(extractDir, { recursive: true });
|
||||
const epName = "Some.Show.S01E01.GERMAN.720p.WEB.x264-GRP.mkv";
|
||||
const bonusName = "Some.Show.Making.Of.GERMAN.720p.WEB.x264-GRP.mkv";
|
||||
fs.writeFileSync(path.join(extractDir, epName), Buffer.alloc(4096, 1));
|
||||
fs.writeFileSync(path.join(extractDir, bonusName), Buffer.alloc(4096, 2));
|
||||
|
||||
const session = emptySession();
|
||||
const packageId = `${packageName}-pkg`;
|
||||
const createdAt = Date.now() - 60_000;
|
||||
session.packageOrder = [packageId];
|
||||
session.packages[packageId] = {
|
||||
id: packageId,
|
||||
name: packageName,
|
||||
outputDir,
|
||||
extractDir,
|
||||
status: "completed",
|
||||
itemIds: [],
|
||||
cancelled: false,
|
||||
enabled: true,
|
||||
createdAt,
|
||||
updatedAt: createdAt
|
||||
};
|
||||
|
||||
const mkvLibraryDir = path.join(root, "mkv-library");
|
||||
const epName = "Some.Show.S01E01.GERMAN.720p.WEB.x264-GRP.mkv";
|
||||
const bonusName = "Some.Show.Making.Of.GERMAN.720p.WEB.x264-GRP.mkv";
|
||||
fs.writeFileSync(path.join(extractDir, epName), Buffer.alloc(4096, 1));
|
||||
fs.writeFileSync(path.join(extractDir, bonusName), Buffer.alloc(4096, 2));
|
||||
|
||||
const session = emptySession();
|
||||
const packageId = `${packageName}-pkg`;
|
||||
const createdAt = Date.now() - 60_000;
|
||||
session.packageOrder = [packageId];
|
||||
session.packages[packageId] = {
|
||||
id: packageId,
|
||||
name: packageName,
|
||||
outputDir,
|
||||
extractDir,
|
||||
status: "completed",
|
||||
itemIds: [],
|
||||
cancelled: false,
|
||||
enabled: true,
|
||||
createdAt,
|
||||
updatedAt: createdAt
|
||||
};
|
||||
writePackageOutputOwnerMarker(session.packages[packageId]);
|
||||
|
||||
const mkvLibraryDir = path.join(root, "mkv-library");
|
||||
const manager = new DownloadManager(
|
||||
{
|
||||
...defaultSettings(),
|
||||
@@ -12947,29 +12967,30 @@ describe("download manager", () => {
|
||||
fs.mkdirSync(epDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(epDir, raw), Buffer.alloc(4096, 8));
|
||||
};
|
||||
const folderA = "Fluss-Monster.S04E08a.Am.Essequibo.Teil.1.German.DOKU.SATRiP.XviD";
|
||||
const folderB = "Fluss-Monster.S04E08b.Am.Essequibo.Teil.2.German.DOKU.SATRiP.XviD";
|
||||
mk(folderA, "safari-fm-s04e08a.avi");
|
||||
mk(folderB, "safari-fm-s04e08b.avi");
|
||||
|
||||
const session = emptySession();
|
||||
const packageId = `${packageName}-pkg`;
|
||||
const createdAt = Date.now() - 60_000;
|
||||
session.packageOrder = [packageId];
|
||||
session.packages[packageId] = {
|
||||
id: packageId,
|
||||
name: packageName,
|
||||
outputDir,
|
||||
extractDir,
|
||||
status: "completed",
|
||||
itemIds: [],
|
||||
cancelled: false,
|
||||
enabled: true,
|
||||
createdAt,
|
||||
updatedAt: createdAt
|
||||
};
|
||||
|
||||
const mkvLibraryDir = path.join(root, "mkv-library");
|
||||
const folderA = "Fluss-Monster.S04E08a.Am.Essequibo.Teil.1.German.DOKU.SATRiP.XviD";
|
||||
const folderB = "Fluss-Monster.S04E08b.Am.Essequibo.Teil.2.German.DOKU.SATRiP.XviD";
|
||||
mk(folderA, "safari-fm-s04e08a.avi");
|
||||
mk(folderB, "safari-fm-s04e08b.avi");
|
||||
|
||||
const session = emptySession();
|
||||
const packageId = `${packageName}-pkg`;
|
||||
const createdAt = Date.now() - 60_000;
|
||||
session.packageOrder = [packageId];
|
||||
session.packages[packageId] = {
|
||||
id: packageId,
|
||||
name: packageName,
|
||||
outputDir,
|
||||
extractDir,
|
||||
status: "completed",
|
||||
itemIds: [],
|
||||
cancelled: false,
|
||||
enabled: true,
|
||||
createdAt,
|
||||
updatedAt: createdAt
|
||||
};
|
||||
writePackageOutputOwnerMarker(session.packages[packageId]);
|
||||
|
||||
const mkvLibraryDir = path.join(root, "mkv-library");
|
||||
const manager = new DownloadManager(
|
||||
{
|
||||
...defaultSettings(),
|
||||
@@ -13003,30 +13024,31 @@ describe("download manager", () => {
|
||||
const packageName = "Steven.Spielbergs.Taken.S01.German.720p.HDTV.x264-GTVG";
|
||||
const outputDir = path.join(root, "downloads", packageName);
|
||||
const extractDir = path.join(root, "extract", packageName);
|
||||
const epFolder = "Steven.Spielbergs.Taken.E01.Hinter.dem.Himmel.German.720p.HDTV.x264-GTVG";
|
||||
const cleanName = "Steven.Spielbergs.Taken.S01E01.German.720p.HDTV.x264-GTVG.mkv";
|
||||
const epDir = path.join(extractDir, epFolder);
|
||||
fs.mkdirSync(epDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(epDir, cleanName), Buffer.alloc(4096, 5));
|
||||
|
||||
const session = emptySession();
|
||||
const packageId = `${packageName}-pkg`;
|
||||
const createdAt = Date.now() - 60_000;
|
||||
session.packageOrder = [packageId];
|
||||
session.packages[packageId] = {
|
||||
id: packageId,
|
||||
name: packageName,
|
||||
outputDir,
|
||||
extractDir,
|
||||
status: "completed",
|
||||
itemIds: [],
|
||||
cancelled: false,
|
||||
enabled: true,
|
||||
createdAt,
|
||||
updatedAt: createdAt
|
||||
};
|
||||
|
||||
const mkvLibraryDir = path.join(root, "mkv-library");
|
||||
const epFolder = "Steven.Spielbergs.Taken.E01.Hinter.dem.Himmel.German.720p.HDTV.x264-GTVG";
|
||||
const cleanName = "Steven.Spielbergs.Taken.S01E01.German.720p.HDTV.x264-GTVG.mkv";
|
||||
const epDir = path.join(extractDir, epFolder);
|
||||
fs.mkdirSync(epDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(epDir, cleanName), Buffer.alloc(4096, 5));
|
||||
|
||||
const session = emptySession();
|
||||
const packageId = `${packageName}-pkg`;
|
||||
const createdAt = Date.now() - 60_000;
|
||||
session.packageOrder = [packageId];
|
||||
session.packages[packageId] = {
|
||||
id: packageId,
|
||||
name: packageName,
|
||||
outputDir,
|
||||
extractDir,
|
||||
status: "completed",
|
||||
itemIds: [],
|
||||
cancelled: false,
|
||||
enabled: true,
|
||||
createdAt,
|
||||
updatedAt: createdAt
|
||||
};
|
||||
writePackageOutputOwnerMarker(session.packages[packageId]);
|
||||
|
||||
const mkvLibraryDir = path.join(root, "mkv-library");
|
||||
const manager = new DownloadManager(
|
||||
{
|
||||
...defaultSettings(),
|
||||
@@ -13274,13 +13296,10 @@ describe("download manager", () => {
|
||||
}
|
||||
fs.writeFileSync(path.join(outputDir, "info.nfo"), Buffer.from("nfo"));
|
||||
|
||||
const s01Mkvs = [
|
||||
"Ugly.Americans.S01E01.German.mkv",
|
||||
"Ugly.Americans.S01E02.German.mkv"
|
||||
];
|
||||
for (const mkv of s01Mkvs) {
|
||||
fs.writeFileSync(path.join(extractDir, mkv), Buffer.alloc(4096, 9));
|
||||
}
|
||||
const s01Mkvs = [
|
||||
"Ugly.Americans.S01E01.German.mkv",
|
||||
"Ugly.Americans.S01E02.German.mkv"
|
||||
];
|
||||
|
||||
const session = emptySession();
|
||||
const packageId = `${packageName}-pkg`;
|
||||
@@ -13311,13 +13330,25 @@ describe("download manager", () => {
|
||||
collectMkvToLibrary: true,
|
||||
mkvLibraryDir,
|
||||
enableIntegrityCheck: false,
|
||||
cleanupMode: "delete"
|
||||
},
|
||||
session,
|
||||
createStoragePaths(path.join(root, "state"))
|
||||
);
|
||||
|
||||
await (manager as any).collectMkvFilesToLibrary(packageId, session.packages[packageId]);
|
||||
cleanupMode: "delete"
|
||||
},
|
||||
session,
|
||||
createStoragePaths(path.join(root, "state"))
|
||||
);
|
||||
|
||||
expect(await (manager as any).ensurePackageOutputOwnerMarker(session.packages[packageId])).toBe(true);
|
||||
const ownerMarkerPath = path.join(extractDir, ".rd-package-output-owner-v1.json");
|
||||
const ownerMarker = JSON.parse(fs.readFileSync(ownerMarkerPath, "utf8"));
|
||||
expect(ownerMarker).toEqual(expect.objectContaining({
|
||||
version: 1,
|
||||
packageId,
|
||||
generation: 1,
|
||||
ownerId: session.packages[packageId].outputOwnerId
|
||||
}));
|
||||
for (const mkv of s01Mkvs) {
|
||||
fs.writeFileSync(path.join(extractDir, mkv), Buffer.alloc(4096, 9));
|
||||
}
|
||||
await (manager as any).collectMkvFilesToLibrary(packageId, session.packages[packageId]);
|
||||
|
||||
for (const mkv of s01Mkvs) {
|
||||
expect(fs.existsSync(path.join(mkvLibraryDir, mkv))).toBe(true);
|
||||
@@ -13373,8 +13404,54 @@ describe("download manager", () => {
|
||||
expect(fs.existsSync(path.join(libraryDir, "foreign.mkv"))).toBe(false);
|
||||
expect(session.packages["package-a"].outputRecords).toEqual([]);
|
||||
});
|
||||
|
||||
it("does NOT move bonus files from Extras subdirectory to flat library", async () => {
|
||||
|
||||
it("does not adopt foreign files from a reused package-name directory without its generation owner marker", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-reused-owner-"));
|
||||
tempDirs.push(root);
|
||||
const packageName = "reused-package";
|
||||
const extractDir = path.join(root, "extract", packageName);
|
||||
const libraryDir = path.join(root, "library");
|
||||
fs.mkdirSync(extractDir, { recursive: true });
|
||||
const foreignPath = path.join(extractDir, "foreign.mkv");
|
||||
fs.writeFileSync(foreignPath, "foreign");
|
||||
const session = emptySession();
|
||||
const packageId = "reused-package-id";
|
||||
session.packageOrder = [packageId];
|
||||
session.packages[packageId] = {
|
||||
id: packageId,
|
||||
name: packageName,
|
||||
outputDir: path.join(root, "downloads", packageName),
|
||||
extractDir,
|
||||
status: "completed",
|
||||
itemIds: [],
|
||||
cancelled: false,
|
||||
enabled: true,
|
||||
outputProvenanceVersion: 1,
|
||||
outputRecords: [],
|
||||
createdAt: 1_000,
|
||||
updatedAt: 1_000
|
||||
};
|
||||
const manager = new DownloadManager(
|
||||
{
|
||||
...defaultSettings(),
|
||||
autoExtract: true,
|
||||
createExtractSubfolder: true,
|
||||
collectMkvToLibrary: true,
|
||||
mkvLibraryDir: libraryDir
|
||||
},
|
||||
session,
|
||||
createStoragePaths(path.join(root, "state"))
|
||||
);
|
||||
|
||||
await (manager as any).collectMkvFilesToLibrary(packageId, session.packages[packageId]);
|
||||
|
||||
expect(fs.existsSync(foreignPath)).toBe(true);
|
||||
expect(fs.existsSync(path.join(libraryDir, "foreign.mkv"))).toBe(false);
|
||||
expect(session.packages[packageId].outputRecords).toEqual([]);
|
||||
expect(fs.existsSync(path.join(extractDir, ".rd-package-output-owner-v1.json"))).toBe(false);
|
||||
});
|
||||
|
||||
it("does NOT move bonus files from Extras subdirectory to flat library", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
||||
tempDirs.push(root);
|
||||
|
||||
@@ -13425,12 +13502,12 @@ describe("download manager", () => {
|
||||
resumable: true,
|
||||
attempts: 1,
|
||||
lastError: "",
|
||||
fullStatus: "Fertig (100 MB)",
|
||||
createdAt,
|
||||
updatedAt: createdAt
|
||||
};
|
||||
|
||||
const mkvLibraryDir = path.join(root, "mkv-library");
|
||||
fullStatus: "Fertig (100 MB)",
|
||||
createdAt,
|
||||
updatedAt: createdAt
|
||||
};
|
||||
|
||||
const mkvLibraryDir = path.join(root, "mkv-library");
|
||||
|
||||
new DownloadManager(
|
||||
{
|
||||
@@ -13516,12 +13593,12 @@ describe("download manager", () => {
|
||||
resumable: true,
|
||||
attempts: 1,
|
||||
lastError: "",
|
||||
fullStatus: "Fertig (100 MB)",
|
||||
createdAt,
|
||||
updatedAt: createdAt
|
||||
};
|
||||
|
||||
const mkvLibraryDir = path.join(root, "mkv-library");
|
||||
fullStatus: "Fertig (100 MB)",
|
||||
createdAt,
|
||||
updatedAt: createdAt
|
||||
};
|
||||
|
||||
const mkvLibraryDir = path.join(root, "mkv-library");
|
||||
|
||||
new DownloadManager(
|
||||
{
|
||||
@@ -13644,12 +13721,12 @@ describe("download manager", () => {
|
||||
resumable: true,
|
||||
attempts: 1,
|
||||
lastError: "",
|
||||
fullStatus: "Fertig",
|
||||
createdAt,
|
||||
updatedAt: createdAt
|
||||
};
|
||||
|
||||
const mkvLibraryDir = path.join(root, "mkv-library");
|
||||
fullStatus: "Fertig",
|
||||
createdAt,
|
||||
updatedAt: createdAt
|
||||
};
|
||||
|
||||
const mkvLibraryDir = path.join(root, "mkv-library");
|
||||
new DownloadManager(
|
||||
{
|
||||
...defaultSettings(),
|
||||
@@ -13723,12 +13800,12 @@ describe("download manager", () => {
|
||||
resumable: true,
|
||||
attempts: 1,
|
||||
lastError: "",
|
||||
fullStatus: "Fertig",
|
||||
createdAt,
|
||||
updatedAt: createdAt
|
||||
};
|
||||
|
||||
const mkvLibraryDir = path.join(root, "mkv-library");
|
||||
fullStatus: "Fertig",
|
||||
createdAt,
|
||||
updatedAt: createdAt
|
||||
};
|
||||
|
||||
const mkvLibraryDir = path.join(root, "mkv-library");
|
||||
fs.mkdirSync(mkvLibraryDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(mkvLibraryDir, "Episode01.mkv"), Buffer.from("video"));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user