Harden deferred cleanup races
This commit is contained in:
parent
2f44e050bc
commit
a70eacf9cd
@ -47,7 +47,10 @@ export function cleanupCancelledPackageArtifacts(packageDir: string): number {
|
|||||||
return removed;
|
return removed;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function cleanupCancelledPackageArtifactsAsync(packageDir: string): Promise<number> {
|
export async function cleanupCancelledPackageArtifactsAsync(
|
||||||
|
packageDir: string,
|
||||||
|
options: { shouldAbort?: () => boolean } = {}
|
||||||
|
): Promise<number> {
|
||||||
try {
|
try {
|
||||||
await fs.promises.access(packageDir, fs.constants.F_OK);
|
await fs.promises.access(packageDir, fs.constants.F_OK);
|
||||||
} catch {
|
} catch {
|
||||||
@ -58,6 +61,9 @@ export async function cleanupCancelledPackageArtifactsAsync(packageDir: string):
|
|||||||
let touched = 0;
|
let touched = 0;
|
||||||
const stack = [packageDir];
|
const stack = [packageDir];
|
||||||
while (stack.length > 0) {
|
while (stack.length > 0) {
|
||||||
|
if (options.shouldAbort?.()) {
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
const current = stack.pop() as string;
|
const current = stack.pop() as string;
|
||||||
let entries: fs.Dirent[] = [];
|
let entries: fs.Dirent[] = [];
|
||||||
try {
|
try {
|
||||||
@ -67,6 +73,9 @@ export async function cleanupCancelledPackageArtifactsAsync(packageDir: string):
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
|
if (options.shouldAbort?.()) {
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
const full = path.join(current, entry.name);
|
const full = path.join(current, entry.name);
|
||||||
if (entry.isDirectory() && !entry.isSymbolicLink()) {
|
if (entry.isDirectory() && !entry.isSymbolicLink()) {
|
||||||
stack.push(full);
|
stack.push(full);
|
||||||
@ -88,7 +97,10 @@ export async function cleanupCancelledPackageArtifactsAsync(packageDir: string):
|
|||||||
return removed;
|
return removed;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removeDownloadLinkArtifacts(extractDir: string): Promise<number> {
|
export async function removeDownloadLinkArtifacts(
|
||||||
|
extractDir: string,
|
||||||
|
options: { shouldAbort?: () => boolean } = {}
|
||||||
|
): Promise<number> {
|
||||||
try {
|
try {
|
||||||
await fs.promises.access(extractDir);
|
await fs.promises.access(extractDir);
|
||||||
} catch {
|
} catch {
|
||||||
@ -97,10 +109,16 @@ export async function removeDownloadLinkArtifacts(extractDir: string): Promise<n
|
|||||||
let removed = 0;
|
let removed = 0;
|
||||||
const stack = [extractDir];
|
const stack = [extractDir];
|
||||||
while (stack.length > 0) {
|
while (stack.length > 0) {
|
||||||
|
if (options.shouldAbort?.()) {
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
const current = stack.pop() as string;
|
const current = stack.pop() as string;
|
||||||
let entries: fs.Dirent[] = [];
|
let entries: fs.Dirent[] = [];
|
||||||
try { entries = await fs.promises.readdir(current, { withFileTypes: true }); } catch { continue; }
|
try { entries = await fs.promises.readdir(current, { withFileTypes: true }); } catch { continue; }
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
|
if (options.shouldAbort?.()) {
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
const full = path.join(current, entry.name);
|
const full = path.join(current, entry.name);
|
||||||
if (entry.isDirectory() && !entry.isSymbolicLink()) {
|
if (entry.isDirectory() && !entry.isSymbolicLink()) {
|
||||||
stack.push(full);
|
stack.push(full);
|
||||||
@ -140,7 +158,10 @@ export async function removeDownloadLinkArtifacts(extractDir: string): Promise<n
|
|||||||
return removed;
|
return removed;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removeSampleArtifacts(extractDir: string): Promise<{ files: number; dirs: number }> {
|
export async function removeSampleArtifacts(
|
||||||
|
extractDir: string,
|
||||||
|
options: { shouldAbort?: () => boolean } = {}
|
||||||
|
): Promise<{ files: number; dirs: number }> {
|
||||||
try {
|
try {
|
||||||
await fs.promises.access(extractDir);
|
await fs.promises.access(extractDir);
|
||||||
} catch {
|
} catch {
|
||||||
@ -184,10 +205,16 @@ export async function removeSampleArtifacts(extractDir: string): Promise<{ files
|
|||||||
};
|
};
|
||||||
|
|
||||||
while (stack.length > 0) {
|
while (stack.length > 0) {
|
||||||
|
if (options.shouldAbort?.()) {
|
||||||
|
return { files: removedFiles, dirs: removedDirs };
|
||||||
|
}
|
||||||
const current = stack.pop() as string;
|
const current = stack.pop() as string;
|
||||||
let entries: fs.Dirent[] = [];
|
let entries: fs.Dirent[] = [];
|
||||||
try { entries = await fs.promises.readdir(current, { withFileTypes: true }); } catch { continue; }
|
try { entries = await fs.promises.readdir(current, { withFileTypes: true }); } catch { continue; }
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
|
if (options.shouldAbort?.()) {
|
||||||
|
return { files: removedFiles, dirs: removedDirs };
|
||||||
|
}
|
||||||
const full = path.join(current, entry.name);
|
const full = path.join(current, entry.name);
|
||||||
if (entry.isDirectory() || entry.isSymbolicLink()) {
|
if (entry.isDirectory() || entry.isSymbolicLink()) {
|
||||||
const base = entry.name.toLowerCase();
|
const base = entry.name.toLowerCase();
|
||||||
@ -221,6 +248,9 @@ export async function removeSampleArtifacts(extractDir: string): Promise<{ files
|
|||||||
|
|
||||||
sampleDirs.sort((a, b) => b.length - a.length);
|
sampleDirs.sort((a, b) => b.length - a.length);
|
||||||
for (const dir of sampleDirs) {
|
for (const dir of sampleDirs) {
|
||||||
|
if (options.shouldAbort?.()) {
|
||||||
|
return { files: removedFiles, dirs: removedDirs };
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const stat = await fs.promises.lstat(dir);
|
const stat = await fs.promises.lstat(dir);
|
||||||
if (stat.isSymbolicLink()) {
|
if (stat.isSymbolicLink()) {
|
||||||
|
|||||||
@ -1270,6 +1270,10 @@ export class DownloadManager extends EventEmitter {
|
|||||||
|
|
||||||
private packagePostProcessAbortControllers = new Map<string, AbortController>();
|
private packagePostProcessAbortControllers = new Map<string, AbortController>();
|
||||||
|
|
||||||
|
private packageDeferredPostProcessAbortControllers = new Map<string, AbortController>();
|
||||||
|
|
||||||
|
private packagePostProcessVersions = new Map<string, number>();
|
||||||
|
|
||||||
private hybridExtractRequeue = new Set<string>();
|
private hybridExtractRequeue = new Set<string>();
|
||||||
|
|
||||||
// Tracks archive paths already attempted per package until the package/archive state changes
|
// Tracks archive paths already attempted per package until the package/archive state changes
|
||||||
@ -1821,6 +1825,70 @@ export class DownloadManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getPackagePostProcessVersion(packageId: string): number {
|
||||||
|
return this.packagePostProcessVersions.get(packageId) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bumpPackagePostProcessVersion(packageId: string): number {
|
||||||
|
const next = this.getPackagePostProcessVersion(packageId) + 1;
|
||||||
|
this.packagePostProcessVersions.set(packageId, next);
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
private abortPackagePostProcessing(packageId: string, reason: string, invalidateDeferred = true): void {
|
||||||
|
if (invalidateDeferred) {
|
||||||
|
this.bumpPackagePostProcessVersion(packageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const postProcessController = this.packagePostProcessAbortControllers.get(packageId);
|
||||||
|
if (postProcessController && !postProcessController.signal.aborted) {
|
||||||
|
postProcessController.abort(reason);
|
||||||
|
}
|
||||||
|
this.packagePostProcessAbortControllers.delete(packageId);
|
||||||
|
this.packagePostProcessTasks.delete(packageId);
|
||||||
|
|
||||||
|
const deferredController = this.packageDeferredPostProcessAbortControllers.get(packageId);
|
||||||
|
if (deferredController && !deferredController.signal.aborted) {
|
||||||
|
deferredController.abort(reason);
|
||||||
|
}
|
||||||
|
this.packageDeferredPostProcessAbortControllers.delete(packageId);
|
||||||
|
|
||||||
|
this.hybridExtractRequeue.delete(packageId);
|
||||||
|
this.clearHybridArchiveState(packageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private isDeferredPostProcessStillCurrent(
|
||||||
|
packageId: string,
|
||||||
|
pkg: PackageEntry,
|
||||||
|
version: number,
|
||||||
|
signal?: AbortSignal
|
||||||
|
): boolean {
|
||||||
|
if (signal?.aborted) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this.session.packages[packageId] !== pkg) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return this.getPackagePostProcessVersion(packageId) === version;
|
||||||
|
}
|
||||||
|
|
||||||
|
private throwIfDeferredPostProcessAborted(
|
||||||
|
packageId: string,
|
||||||
|
pkg: PackageEntry,
|
||||||
|
version: number,
|
||||||
|
signal?: AbortSignal
|
||||||
|
): void {
|
||||||
|
if (this.isDeferredPostProcessStillCurrent(packageId, pkg, version, signal)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new Error(String(signal?.reason || "aborted:deferred"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private packageOutputDirInUse(outputDir: string): boolean {
|
||||||
|
const key = pathKey(outputDir);
|
||||||
|
return Object.values(this.session.packages).some((pkg) => pathKey(pkg.outputDir) === key);
|
||||||
|
}
|
||||||
|
|
||||||
public resetSessionStats(): void {
|
public resetSessionStats(): void {
|
||||||
const now = nowMs();
|
const now = nowMs();
|
||||||
this.session.totalDownloadedBytes = 0;
|
this.session.totalDownloadedBytes = 0;
|
||||||
@ -1937,10 +2005,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
if (pkg.status === "downloading" || pkg.status === "extracting") {
|
if (pkg.status === "downloading" || pkg.status === "extracting") {
|
||||||
pkg.status = "paused";
|
pkg.status = "paused";
|
||||||
}
|
}
|
||||||
const postProcessController = this.packagePostProcessAbortControllers.get(packageId);
|
this.abortPackagePostProcessing(packageId, "package_toggle");
|
||||||
if (postProcessController && !postProcessController.signal.aborted) {
|
|
||||||
postProcessController.abort("package_toggle");
|
|
||||||
}
|
|
||||||
for (const itemId of pkg.itemIds) {
|
for (const itemId of pkg.itemIds) {
|
||||||
const item = this.session.items[itemId];
|
const item = this.session.items[itemId];
|
||||||
if (!item) {
|
if (!item) {
|
||||||
@ -2293,14 +2358,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.retryStateByItem.delete(itemId);
|
this.retryStateByItem.delete(itemId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const postProcessController = this.packagePostProcessAbortControllers.get(packageId);
|
this.abortPackagePostProcessing(packageId, "skip");
|
||||||
if (postProcessController && !postProcessController.signal.aborted) {
|
|
||||||
postProcessController.abort("skip");
|
|
||||||
}
|
|
||||||
this.packagePostProcessAbortControllers.delete(packageId);
|
|
||||||
this.packagePostProcessTasks.delete(packageId);
|
|
||||||
this.hybridExtractRequeue.delete(packageId);
|
|
||||||
this.clearHybridArchiveState(packageId);
|
|
||||||
|
|
||||||
this.runPackageIds.delete(packageId);
|
this.runPackageIds.delete(packageId);
|
||||||
this.runCompletedPackages.delete(packageId);
|
this.runCompletedPackages.delete(packageId);
|
||||||
@ -2335,12 +2393,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (policy === "overwrite") {
|
if (policy === "overwrite") {
|
||||||
const postProcessController = this.packagePostProcessAbortControllers.get(packageId);
|
this.abortPackagePostProcessing(packageId, "overwrite");
|
||||||
if (postProcessController && !postProcessController.signal.aborted) {
|
|
||||||
postProcessController.abort("overwrite");
|
|
||||||
}
|
|
||||||
this.packagePostProcessAbortControllers.delete(packageId);
|
|
||||||
this.packagePostProcessTasks.delete(packageId);
|
|
||||||
const canDeleteExtractDir = this.isPackageSpecificExtractDir(pkg) && !this.isExtractDirSharedWithOtherPackages(pkg.id, pkg.extractDir);
|
const canDeleteExtractDir = this.isPackageSpecificExtractDir(pkg) && !this.isExtractDirSharedWithOtherPackages(pkg.id, pkg.extractDir);
|
||||||
if (canDeleteExtractDir) {
|
if (canDeleteExtractDir) {
|
||||||
try {
|
try {
|
||||||
@ -2994,7 +3047,11 @@ export class DownloadManager extends EventEmitter {
|
|||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async autoRenameExtractedVideoFiles(extractDir: string, pkg?: PackageEntry): Promise<number> {
|
private async autoRenameExtractedVideoFiles(
|
||||||
|
extractDir: string,
|
||||||
|
pkg?: PackageEntry,
|
||||||
|
shouldAbort?: () => boolean
|
||||||
|
): Promise<number> {
|
||||||
if (!this.settings.autoRename4sf4sj) {
|
if (!this.settings.autoRename4sf4sj) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@ -3033,6 +3090,9 @@ export class DownloadManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const sourcePath of videoFiles) {
|
for (const sourcePath of videoFiles) {
|
||||||
|
if (shouldAbort?.()) {
|
||||||
|
return renamed;
|
||||||
|
}
|
||||||
const sourceName = path.basename(sourcePath);
|
const sourceName = path.basename(sourcePath);
|
||||||
const sourceExt = path.extname(sourceName);
|
const sourceExt = path.extname(sourceName);
|
||||||
const sourceBaseName = path.basename(sourceName, sourceExt);
|
const sourceBaseName = path.basename(sourceName, sourceExt);
|
||||||
@ -3309,10 +3369,13 @@ export class DownloadManager extends EventEmitter {
|
|||||||
return removed;
|
return removed;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async cleanupRemainingArchiveArtifacts(packageDir: string): Promise<number> {
|
private async cleanupRemainingArchiveArtifacts(packageDir: string, shouldAbort?: () => boolean): Promise<number> {
|
||||||
if (this.settings.cleanupMode === "none") {
|
if (this.settings.cleanupMode === "none") {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
if (shouldAbort?.()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
const candidates = await findArchiveCandidates(packageDir);
|
const candidates = await findArchiveCandidates(packageDir);
|
||||||
if (candidates.length === 0) {
|
if (candidates.length === 0) {
|
||||||
return 0;
|
return 0;
|
||||||
@ -3322,6 +3385,9 @@ export class DownloadManager extends EventEmitter {
|
|||||||
const dirFilesCache = new Map<string, string[]>();
|
const dirFilesCache = new Map<string, string[]>();
|
||||||
const targets = new Set<string>();
|
const targets = new Set<string>();
|
||||||
for (const sourceFile of candidates) {
|
for (const sourceFile of candidates) {
|
||||||
|
if (shouldAbort?.()) {
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
const dir = path.dirname(sourceFile);
|
const dir = path.dirname(sourceFile);
|
||||||
let filesInDir = dirFilesCache.get(dir);
|
let filesInDir = dirFilesCache.get(dir);
|
||||||
if (!filesInDir) {
|
if (!filesInDir) {
|
||||||
@ -3340,6 +3406,9 @@ export class DownloadManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const targetPath of targets) {
|
for (const targetPath of targets) {
|
||||||
|
if (shouldAbort?.()) {
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
if (!await this.existsAsync(targetPath)) {
|
if (!await this.existsAsync(targetPath)) {
|
||||||
continue;
|
continue;
|
||||||
@ -3404,7 +3473,11 @@ export class DownloadManager extends EventEmitter {
|
|||||||
return fallbackPath;
|
return fallbackPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async collectMkvFilesToLibrary(packageId: string, pkg: PackageEntry): Promise<void> {
|
private async collectMkvFilesToLibrary(
|
||||||
|
packageId: string,
|
||||||
|
pkg: PackageEntry,
|
||||||
|
shouldAbort?: () => boolean
|
||||||
|
): Promise<void> {
|
||||||
if (!this.settings.collectMkvToLibrary) {
|
if (!this.settings.collectMkvToLibrary) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -3440,6 +3513,9 @@ export class DownloadManager extends EventEmitter {
|
|||||||
const mkvFiles: string[] = [];
|
const mkvFiles: string[] = [];
|
||||||
let sampleSkipped = 0;
|
let sampleSkipped = 0;
|
||||||
for (const filePath of allMkvFiles) {
|
for (const filePath of allMkvFiles) {
|
||||||
|
if (shouldAbort?.()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const parentDir = path.basename(path.dirname(filePath)).toLowerCase();
|
const parentDir = path.basename(path.dirname(filePath)).toLowerCase();
|
||||||
const stem = path.parse(path.basename(filePath)).name;
|
const stem = path.parse(path.basename(filePath)).name;
|
||||||
if (sampleDirNames.has(parentDir) || sampleTokenRe.test(stem)) {
|
if (sampleDirNames.has(parentDir) || sampleTokenRe.test(stem)) {
|
||||||
@ -3468,6 +3544,9 @@ export class DownloadManager extends EventEmitter {
|
|||||||
let failed = 0;
|
let failed = 0;
|
||||||
|
|
||||||
for (const sourcePath of mkvFiles) {
|
for (const sourcePath of mkvFiles) {
|
||||||
|
if (shouldAbort?.()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (isPathInsideDir(sourcePath, targetDir)) {
|
if (isPathInsideDir(sourcePath, targetDir)) {
|
||||||
skipped += 1;
|
skipped += 1;
|
||||||
continue;
|
continue;
|
||||||
@ -3604,10 +3683,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const postProcessController = this.packagePostProcessAbortControllers.get(packageId);
|
this.abortPackagePostProcessing(packageId, "cancel");
|
||||||
if (postProcessController && !postProcessController.signal.aborted) {
|
|
||||||
postProcessController.abort("cancel");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.removePackageFromSession(packageId, itemIds);
|
this.removePackageFromSession(packageId, itemIds);
|
||||||
this.persistSoon();
|
this.persistSoon();
|
||||||
@ -3615,7 +3691,9 @@ export class DownloadManager extends EventEmitter {
|
|||||||
|
|
||||||
this.cleanupQueue = this.cleanupQueue
|
this.cleanupQueue = this.cleanupQueue
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
const removed = await cleanupCancelledPackageArtifactsAsync(outputDir);
|
const removed = await cleanupCancelledPackageArtifactsAsync(outputDir, {
|
||||||
|
shouldAbort: () => this.packageOutputDirInUse(outputDir)
|
||||||
|
});
|
||||||
logger.info(`Paket ${packageName} abgebrochen, ${removed} Artefakte gelöscht`);
|
logger.info(`Paket ${packageName} abgebrochen, ${removed} Artefakte gelöscht`);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -3671,14 +3749,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. Abort post-processing (extraction) if active for THIS package
|
// 2. Abort post-processing (extraction) if active for THIS package
|
||||||
const postProcessController = this.packagePostProcessAbortControllers.get(packageId);
|
this.abortPackagePostProcessing(packageId, "reset");
|
||||||
if (postProcessController && !postProcessController.signal.aborted) {
|
|
||||||
postProcessController.abort("reset");
|
|
||||||
}
|
|
||||||
this.packagePostProcessAbortControllers.delete(packageId);
|
|
||||||
this.packagePostProcessTasks.delete(packageId);
|
|
||||||
this.hybridExtractRequeue.delete(packageId);
|
|
||||||
this.clearHybridArchiveState(packageId);
|
|
||||||
this.runCompletedPackages.delete(packageId);
|
this.runCompletedPackages.delete(packageId);
|
||||||
|
|
||||||
// 3. Clean up extraction progress manifest (.rd_extract_progress.json)
|
// 3. Clean up extraction progress manifest (.rd_extract_progress.json)
|
||||||
@ -3761,14 +3832,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
// Reset parent package status if it was completed/failed (now has queued items again)
|
// Reset parent package status if it was completed/failed (now has queued items again)
|
||||||
for (const pkgId of affectedPackageIds) {
|
for (const pkgId of affectedPackageIds) {
|
||||||
// Abort active post-processing for this package
|
// Abort active post-processing for this package
|
||||||
const postProcessController = this.packagePostProcessAbortControllers.get(pkgId);
|
this.abortPackagePostProcessing(pkgId, "reset");
|
||||||
if (postProcessController && !postProcessController.signal.aborted) {
|
|
||||||
postProcessController.abort("reset");
|
|
||||||
}
|
|
||||||
this.packagePostProcessAbortControllers.delete(pkgId);
|
|
||||||
this.packagePostProcessTasks.delete(pkgId);
|
|
||||||
this.hybridExtractRequeue.delete(pkgId);
|
|
||||||
this.clearHybridArchiveState(pkgId);
|
|
||||||
this.runCompletedPackages.delete(pkgId);
|
this.runCompletedPackages.delete(pkgId);
|
||||||
this.historyRecordedPackages.delete(pkgId);
|
this.historyRecordedPackages.delete(pkgId);
|
||||||
|
|
||||||
@ -5837,12 +5901,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.historyRecordedPackages.delete(packageId);
|
this.historyRecordedPackages.delete(packageId);
|
||||||
const postProcessController = this.packagePostProcessAbortControllers.get(packageId);
|
this.abortPackagePostProcessing(packageId, "package_removed");
|
||||||
if (postProcessController && !postProcessController.signal.aborted) {
|
|
||||||
postProcessController.abort("package_removed");
|
|
||||||
}
|
|
||||||
this.packagePostProcessAbortControllers.delete(packageId);
|
|
||||||
this.packagePostProcessTasks.delete(packageId);
|
|
||||||
for (const itemId of itemIds) {
|
for (const itemId of itemIds) {
|
||||||
this.retryAfterByItem.delete(itemId);
|
this.retryAfterByItem.delete(itemId);
|
||||||
this.retryStateByItem.delete(itemId);
|
this.retryStateByItem.delete(itemId);
|
||||||
@ -5859,8 +5918,6 @@ export class DownloadManager extends EventEmitter {
|
|||||||
// would make runPackageIds empty, disabling the "size > 0" filter guard and
|
// would make runPackageIds empty, disabling the "size > 0" filter guard and
|
||||||
// causing "Start Selected" to continue with ALL packages after cleanup.
|
// causing "Start Selected" to continue with ALL packages after cleanup.
|
||||||
this.runCompletedPackages.delete(packageId);
|
this.runCompletedPackages.delete(packageId);
|
||||||
this.hybridExtractRequeue.delete(packageId);
|
|
||||||
this.clearHybridArchiveState(packageId);
|
|
||||||
this.resetSessionTotalsIfQueueEmpty();
|
this.resetSessionTotalsIfQueueEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -9947,7 +10004,18 @@ export class DownloadManager extends EventEmitter {
|
|||||||
alreadyMarkedExtracted: boolean,
|
alreadyMarkedExtracted: boolean,
|
||||||
extractedCount: number
|
extractedCount: number
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const replacedController = this.packageDeferredPostProcessAbortControllers.get(packageId);
|
||||||
|
if (replacedController && !replacedController.signal.aborted) {
|
||||||
|
replacedController.abort("deferred_replaced");
|
||||||
|
}
|
||||||
|
const deferredController = new AbortController();
|
||||||
|
this.packageDeferredPostProcessAbortControllers.set(packageId, deferredController);
|
||||||
|
const deferredVersion = this.getPackagePostProcessVersion(packageId);
|
||||||
|
const shouldAbort = (): boolean => !this.isDeferredPostProcessStillCurrent(packageId, pkg, deferredVersion, deferredController.signal);
|
||||||
|
const throwIfAborted = (): void => this.throwIfDeferredPostProcessAborted(packageId, pkg, deferredVersion, deferredController.signal);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
throwIfAborted();
|
||||||
// ── Nested extraction: extract archives found inside the extracted output ──
|
// ── Nested extraction: extract archives found inside the extracted output ──
|
||||||
if ((extractedCount > 0 || alreadyMarkedExtracted) && failed === 0 && this.settings.autoExtract) {
|
if ((extractedCount > 0 || alreadyMarkedExtracted) && failed === 0 && this.settings.autoExtract) {
|
||||||
const nestedBlacklist = /\.(iso|img|bin|dmg|vhd|vhdx|vmdk|wim)$/i;
|
const nestedBlacklist = /\.(iso|img|bin|dmg|vhd|vhdx|vmdk|wim)$/i;
|
||||||
@ -9975,6 +10043,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
extractCpuPriority: this.settings.extractCpuPriority,
|
extractCpuPriority: this.settings.extractCpuPriority,
|
||||||
onLog: (level, message) => this.logPackageForPackage(pkg, level, `Nested-Extractor: ${message}`),
|
onLog: (level, message) => this.logPackageForPackage(pkg, level, `Nested-Extractor: ${message}`),
|
||||||
});
|
});
|
||||||
|
throwIfAborted();
|
||||||
extractedCount += nestedResult.extracted;
|
extractedCount += nestedResult.extracted;
|
||||||
logger.info(`Deferred Nested-Extraction Ende: extracted=${nestedResult.extracted}, failed=${nestedResult.failed}`);
|
logger.info(`Deferred Nested-Extraction Ende: extracted=${nestedResult.extracted}, failed=${nestedResult.failed}`);
|
||||||
this.logPackageForPackage(pkg, "INFO", "Deferred Nested-Extraction Ende", {
|
this.logPackageForPackage(pkg, "INFO", "Deferred Nested-Extraction Ende", {
|
||||||
@ -9991,7 +10060,8 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.logPackageForPackage(pkg, "INFO", "Deferred Auto-Rename gestartet", {
|
this.logPackageForPackage(pkg, "INFO", "Deferred Auto-Rename gestartet", {
|
||||||
extractDir: pkg.extractDir
|
extractDir: pkg.extractDir
|
||||||
});
|
});
|
||||||
await this.autoRenameExtractedVideoFiles(pkg.extractDir, pkg);
|
throwIfAborted();
|
||||||
|
await this.autoRenameExtractedVideoFiles(pkg.extractDir, pkg, shouldAbort);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Archive cleanup (source archives in outputDir) ──
|
// ── Archive cleanup (source archives in outputDir) ──
|
||||||
@ -10000,11 +10070,12 @@ export class DownloadManager extends EventEmitter {
|
|||||||
if ((extractedCount > 0 || alreadyMarkedExtracted) && failed === 0 && this.settings.cleanupMode !== "none") {
|
if ((extractedCount > 0 || alreadyMarkedExtracted) && failed === 0 && this.settings.cleanupMode !== "none") {
|
||||||
pkg.postProcessLabel = "Aufräumen...";
|
pkg.postProcessLabel = "Aufräumen...";
|
||||||
this.emitState();
|
this.emitState();
|
||||||
|
throwIfAborted();
|
||||||
const sourceAndTargetEqual = path.resolve(pkg.outputDir).toLowerCase() === path.resolve(pkg.extractDir).toLowerCase();
|
const sourceAndTargetEqual = path.resolve(pkg.outputDir).toLowerCase() === path.resolve(pkg.extractDir).toLowerCase();
|
||||||
if (!sourceAndTargetEqual) {
|
if (!sourceAndTargetEqual) {
|
||||||
const candidates = await findArchiveCandidates(pkg.outputDir);
|
const candidates = await findArchiveCandidates(pkg.outputDir);
|
||||||
if (candidates.length > 0) {
|
if (candidates.length > 0) {
|
||||||
const removed = await cleanupArchives(candidates, this.settings.cleanupMode);
|
const removed = await cleanupArchives(candidates, this.settings.cleanupMode, { shouldAbort });
|
||||||
if (removed > 0) {
|
if (removed > 0) {
|
||||||
logger.info(`Deferred Archive-Cleanup: pkg=${pkg.name}, entfernt=${removed}`);
|
logger.info(`Deferred Archive-Cleanup: pkg=${pkg.name}, entfernt=${removed}`);
|
||||||
}
|
}
|
||||||
@ -10014,7 +10085,8 @@ export class DownloadManager extends EventEmitter {
|
|||||||
|
|
||||||
// ── Hybrid archive cleanup (wenn bereits als extracted markiert) ──
|
// ── Hybrid archive cleanup (wenn bereits als extracted markiert) ──
|
||||||
if (this.settings.autoExtract && alreadyMarkedExtracted && failed === 0 && success > 0 && this.settings.cleanupMode !== "none") {
|
if (this.settings.autoExtract && alreadyMarkedExtracted && failed === 0 && success > 0 && this.settings.cleanupMode !== "none") {
|
||||||
const removedArchives = await this.cleanupRemainingArchiveArtifacts(pkg.outputDir);
|
throwIfAborted();
|
||||||
|
const removedArchives = await this.cleanupRemainingArchiveArtifacts(pkg.outputDir, shouldAbort);
|
||||||
if (removedArchives > 0) {
|
if (removedArchives > 0) {
|
||||||
logger.info(`Hybrid-Post-Cleanup entfernte Archive: pkg=${pkg.name}, entfernt=${removedArchives}`);
|
logger.info(`Hybrid-Post-Cleanup entfernte Archive: pkg=${pkg.name}, entfernt=${removedArchives}`);
|
||||||
}
|
}
|
||||||
@ -10022,14 +10094,15 @@ export class DownloadManager extends EventEmitter {
|
|||||||
|
|
||||||
// ── Link/Sample artifact removal ──
|
// ── Link/Sample artifact removal ──
|
||||||
if ((extractedCount > 0 || alreadyMarkedExtracted) && failed === 0) {
|
if ((extractedCount > 0 || alreadyMarkedExtracted) && failed === 0) {
|
||||||
|
throwIfAborted();
|
||||||
if (this.settings.removeLinkFilesAfterExtract) {
|
if (this.settings.removeLinkFilesAfterExtract) {
|
||||||
const removedLinks = await removeDownloadLinkArtifacts(pkg.extractDir);
|
const removedLinks = await removeDownloadLinkArtifacts(pkg.extractDir, { shouldAbort });
|
||||||
if (removedLinks > 0) {
|
if (removedLinks > 0) {
|
||||||
logger.info(`Deferred Link-Cleanup: pkg=${pkg.name}, entfernt=${removedLinks}`);
|
logger.info(`Deferred Link-Cleanup: pkg=${pkg.name}, entfernt=${removedLinks}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.settings.removeSamplesAfterExtract) {
|
if (this.settings.removeSamplesAfterExtract) {
|
||||||
const removedSamples = await removeSampleArtifacts(pkg.extractDir);
|
const removedSamples = await removeSampleArtifacts(pkg.extractDir, { shouldAbort });
|
||||||
if (removedSamples.files > 0 || removedSamples.dirs > 0) {
|
if (removedSamples.files > 0 || removedSamples.dirs > 0) {
|
||||||
logger.info(`Deferred Sample-Cleanup: pkg=${pkg.name}, files=${removedSamples.files}, dirs=${removedSamples.dirs}`);
|
logger.info(`Deferred Sample-Cleanup: pkg=${pkg.name}, files=${removedSamples.files}, dirs=${removedSamples.dirs}`);
|
||||||
}
|
}
|
||||||
@ -10038,6 +10111,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
|
|
||||||
// ── Resume state cleanup ──
|
// ── Resume state cleanup ──
|
||||||
if ((extractedCount > 0 || alreadyMarkedExtracted) && failed === 0) {
|
if ((extractedCount > 0 || alreadyMarkedExtracted) && failed === 0) {
|
||||||
|
throwIfAborted();
|
||||||
await clearExtractResumeState(pkg.outputDir, packageId);
|
await clearExtractResumeState(pkg.outputDir, packageId);
|
||||||
// Backward compatibility: older versions used .rd_extract_progress.json without package suffix.
|
// Backward compatibility: older versions used .rd_extract_progress.json without package suffix.
|
||||||
await clearExtractResumeState(pkg.outputDir);
|
await clearExtractResumeState(pkg.outputDir);
|
||||||
@ -10045,6 +10119,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
|
|
||||||
// ── Empty directory tree removal ──
|
// ── Empty directory tree removal ──
|
||||||
if ((extractedCount > 0 || alreadyMarkedExtracted) && failed === 0 && this.settings.cleanupMode === "delete") {
|
if ((extractedCount > 0 || alreadyMarkedExtracted) && failed === 0 && this.settings.cleanupMode === "delete") {
|
||||||
|
throwIfAborted();
|
||||||
if (!(await hasAnyFilesRecursive(pkg.outputDir))) {
|
if (!(await hasAnyFilesRecursive(pkg.outputDir))) {
|
||||||
const removedDirs = await removeEmptyDirectoryTree(pkg.outputDir);
|
const removedDirs = await removeEmptyDirectoryTree(pkg.outputDir);
|
||||||
if (removedDirs > 0) {
|
if (removedDirs > 0) {
|
||||||
@ -10055,11 +10130,13 @@ export class DownloadManager extends EventEmitter {
|
|||||||
|
|
||||||
// ── MKV collection ──
|
// ── MKV collection ──
|
||||||
if (success > 0 && (pkg.status === "completed" || pkg.status === "failed")) {
|
if (success > 0 && (pkg.status === "completed" || pkg.status === "failed")) {
|
||||||
|
throwIfAborted();
|
||||||
pkg.postProcessLabel = "Verschiebe MKVs...";
|
pkg.postProcessLabel = "Verschiebe MKVs...";
|
||||||
this.emitState();
|
this.emitState();
|
||||||
await this.collectMkvFilesToLibrary(packageId, pkg);
|
await this.collectMkvFilesToLibrary(packageId, pkg, shouldAbort);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throwIfAborted();
|
||||||
pkg.postProcessLabel = undefined;
|
pkg.postProcessLabel = undefined;
|
||||||
pkg.updatedAt = nowMs();
|
pkg.updatedAt = nowMs();
|
||||||
this.persistSoon();
|
this.persistSoon();
|
||||||
@ -10067,12 +10144,29 @@ export class DownloadManager extends EventEmitter {
|
|||||||
|
|
||||||
this.applyPackageDoneCleanup(packageId);
|
this.applyPackageDoneCleanup(packageId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn(`Deferred Post-Extraction Fehler: pkg=${pkg.name}, reason=${compactErrorText(error)}`);
|
const reason = compactErrorText(error);
|
||||||
|
if (reason.includes("aborted:deferred")
|
||||||
|
|| reason.includes("deferred_replaced")
|
||||||
|
|| reason.includes("package_removed")
|
||||||
|
|| reason === "reset"
|
||||||
|
|| reason === "cancel"
|
||||||
|
|| reason === "overwrite"
|
||||||
|
|| reason === "skip"
|
||||||
|
|| reason === "package_toggle") {
|
||||||
|
logger.info(`Deferred Post-Extraction abgebrochen: pkg=${pkg.name}, reason=${reason}`);
|
||||||
|
} else {
|
||||||
|
logger.warn(`Deferred Post-Extraction Fehler: pkg=${pkg.name}, reason=${reason}`);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
pkg.postProcessLabel = undefined;
|
if (this.packageDeferredPostProcessAbortControllers.get(packageId) === deferredController) {
|
||||||
pkg.updatedAt = nowMs();
|
this.packageDeferredPostProcessAbortControllers.delete(packageId);
|
||||||
this.persistSoon();
|
}
|
||||||
this.emitState();
|
if (this.session.packages[packageId] === pkg && this.getPackagePostProcessVersion(packageId) === deferredVersion) {
|
||||||
|
pkg.postProcessLabel = undefined;
|
||||||
|
pkg.updatedAt = nowMs();
|
||||||
|
this.persistSoon();
|
||||||
|
this.emitState();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2744,7 +2744,11 @@ export function collectArchiveCleanupTargets(sourceArchivePath: string, director
|
|||||||
return Array.from(targets);
|
return Array.from(targets);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function cleanupArchives(sourceFiles: string[], cleanupMode: CleanupMode): Promise<number> {
|
export async function cleanupArchives(
|
||||||
|
sourceFiles: string[],
|
||||||
|
cleanupMode: CleanupMode,
|
||||||
|
options: { shouldAbort?: () => boolean } = {}
|
||||||
|
): Promise<number> {
|
||||||
if (cleanupMode === "none") {
|
if (cleanupMode === "none") {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@ -2752,6 +2756,9 @@ export async function cleanupArchives(sourceFiles: string[], cleanupMode: Cleanu
|
|||||||
const targets = new Set<string>();
|
const targets = new Set<string>();
|
||||||
const dirFilesCache = new Map<string, string[]>();
|
const dirFilesCache = new Map<string, string[]>();
|
||||||
for (const sourceFile of sourceFiles) {
|
for (const sourceFile of sourceFiles) {
|
||||||
|
if (options.shouldAbort?.()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
const dir = path.dirname(sourceFile);
|
const dir = path.dirname(sourceFile);
|
||||||
let filesInDir = dirFilesCache.get(dir);
|
let filesInDir = dirFilesCache.get(dir);
|
||||||
if (!filesInDir) {
|
if (!filesInDir) {
|
||||||
@ -2795,6 +2802,9 @@ export async function cleanupArchives(sourceFiles: string[], cleanupMode: Cleanu
|
|||||||
};
|
};
|
||||||
|
|
||||||
for (const filePath of targets) {
|
for (const filePath of targets) {
|
||||||
|
if (options.shouldAbort?.()) {
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const fileExists = await fs.promises.access(filePath).then(() => true, () => false);
|
const fileExists = await fs.promises.access(filePath).then(() => true, () => false);
|
||||||
if (!fileExists) {
|
if (!fileExists) {
|
||||||
|
|||||||
@ -6677,6 +6677,171 @@ describe("download manager", () => {
|
|||||||
expect(snapshot.session.items[itemId]?.fullStatus).toBe("Entpackt (Quelle fehlt)");
|
expect(snapshot.session.items[itemId]?.fullStatus).toBe("Entpackt (Quelle fehlt)");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("stops deferred post-extraction cleanup after package reset", async () => {
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
||||||
|
tempDirs.push(root);
|
||||||
|
|
||||||
|
const sharedDir = path.join(root, "shared");
|
||||||
|
fs.mkdirSync(sharedDir, { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(sharedDir, "episode.part01.rar"), "archive", "utf8");
|
||||||
|
|
||||||
|
const session = emptySession();
|
||||||
|
const packageId = "deferred-reset-pkg";
|
||||||
|
const itemId = "deferred-reset-item";
|
||||||
|
const createdAt = Date.now() - 20_000;
|
||||||
|
session.packageOrder = [packageId];
|
||||||
|
session.packages[packageId] = {
|
||||||
|
id: packageId,
|
||||||
|
name: "Deferred Reset",
|
||||||
|
outputDir: sharedDir,
|
||||||
|
extractDir: sharedDir,
|
||||||
|
status: "completed",
|
||||||
|
itemIds: [itemId],
|
||||||
|
cancelled: false,
|
||||||
|
enabled: true,
|
||||||
|
createdAt,
|
||||||
|
updatedAt: createdAt
|
||||||
|
};
|
||||||
|
session.items[itemId] = {
|
||||||
|
id: itemId,
|
||||||
|
packageId,
|
||||||
|
url: "https://dummy/deferred-reset",
|
||||||
|
provider: "realdebrid",
|
||||||
|
status: "completed",
|
||||||
|
retries: 0,
|
||||||
|
speedBps: 0,
|
||||||
|
downloadedBytes: 123,
|
||||||
|
totalBytes: 123,
|
||||||
|
progressPercent: 100,
|
||||||
|
fileName: "episode.part01.rar",
|
||||||
|
targetPath: path.join(sharedDir, "episode.part01.rar"),
|
||||||
|
resumable: true,
|
||||||
|
attempts: 1,
|
||||||
|
lastError: "",
|
||||||
|
fullStatus: "Fertig (123 B)",
|
||||||
|
createdAt,
|
||||||
|
updatedAt: createdAt
|
||||||
|
};
|
||||||
|
|
||||||
|
const manager = new DownloadManager(
|
||||||
|
{
|
||||||
|
...defaultSettings(),
|
||||||
|
token: "rd-token",
|
||||||
|
outputDir: path.join(root, "downloads"),
|
||||||
|
extractDir: path.join(root, "extract"),
|
||||||
|
autoExtract: true,
|
||||||
|
cleanupMode: "delete"
|
||||||
|
},
|
||||||
|
session,
|
||||||
|
createStoragePaths(path.join(root, "state"))
|
||||||
|
);
|
||||||
|
|
||||||
|
let renameStarted = false;
|
||||||
|
let releaseRename = (): void => {};
|
||||||
|
const renameGate = new Promise<void>((resolve) => {
|
||||||
|
releaseRename = resolve;
|
||||||
|
});
|
||||||
|
const internal = manager as any;
|
||||||
|
internal.autoRenameExtractedVideoFiles = vi.fn(async () => {
|
||||||
|
renameStarted = true;
|
||||||
|
await renameGate;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
const cleanupRemainingArchiveArtifacts = vi.fn(async () => 0);
|
||||||
|
internal.cleanupRemainingArchiveArtifacts = cleanupRemainingArchiveArtifacts;
|
||||||
|
|
||||||
|
const deferredPromise = internal.runDeferredPostExtraction(
|
||||||
|
packageId,
|
||||||
|
internal.session.packages[packageId],
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
true,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => renameStarted, 4000);
|
||||||
|
manager.resetPackage(packageId);
|
||||||
|
releaseRename();
|
||||||
|
await deferredPromise;
|
||||||
|
|
||||||
|
expect(cleanupRemainingArchiveArtifacts).not.toHaveBeenCalled();
|
||||||
|
const snapshot = manager.getSnapshot();
|
||||||
|
expect(snapshot.session.packages[packageId]?.status).toBe("queued");
|
||||||
|
expect(snapshot.session.items[itemId]?.status).toBe("queued");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not let cancelled cleanup delete archives for a re-added package in the same folder", async () => {
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
||||||
|
tempDirs.push(root);
|
||||||
|
|
||||||
|
const packageName = "Cancel Cleanup";
|
||||||
|
const outputDir = path.join(root, "downloads", packageName);
|
||||||
|
fs.mkdirSync(outputDir, { recursive: true });
|
||||||
|
const archivePath = path.join(outputDir, "episode.part01.rar");
|
||||||
|
fs.writeFileSync(archivePath, "archive", "utf8");
|
||||||
|
|
||||||
|
const session = emptySession();
|
||||||
|
const packageId = "cancel-cleanup-pkg";
|
||||||
|
const itemId = "cancel-cleanup-item";
|
||||||
|
const createdAt = Date.now() - 20_000;
|
||||||
|
session.packageOrder = [packageId];
|
||||||
|
session.packages[packageId] = {
|
||||||
|
id: packageId,
|
||||||
|
name: packageName,
|
||||||
|
outputDir,
|
||||||
|
extractDir: path.join(root, "extract", packageName),
|
||||||
|
status: "queued",
|
||||||
|
itemIds: [itemId],
|
||||||
|
cancelled: false,
|
||||||
|
enabled: true,
|
||||||
|
createdAt,
|
||||||
|
updatedAt: createdAt
|
||||||
|
};
|
||||||
|
session.items[itemId] = {
|
||||||
|
id: itemId,
|
||||||
|
packageId,
|
||||||
|
url: "https://dummy/episode.part01.rar",
|
||||||
|
provider: null,
|
||||||
|
status: "queued",
|
||||||
|
retries: 0,
|
||||||
|
speedBps: 0,
|
||||||
|
downloadedBytes: 0,
|
||||||
|
totalBytes: null,
|
||||||
|
progressPercent: 0,
|
||||||
|
fileName: "episode.part01.rar",
|
||||||
|
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"))
|
||||||
|
);
|
||||||
|
|
||||||
|
manager.cancelPackage(packageId);
|
||||||
|
manager.addPackages([{ name: packageName, links: ["https://dummy/episode.part01.rar"] }]);
|
||||||
|
|
||||||
|
await waitFor(() => manager.getSnapshot().session.packageOrder.length === 1, 4000);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
|
||||||
|
expect(fs.existsSync(archivePath)).toBe(true);
|
||||||
|
const snapshot = manager.getSnapshot();
|
||||||
|
const remainingPackage = snapshot.session.packages[snapshot.session.packageOrder[0]];
|
||||||
|
expect(remainingPackage?.outputDir).toBe(outputDir);
|
||||||
|
});
|
||||||
|
|
||||||
it("does not delete startup archives when any completed item has an extract error", async () => {
|
it("does not delete startup archives when any completed item has an extract error", async () => {
|
||||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
||||||
tempDirs.push(root);
|
tempDirs.push(root);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user