fix: harden upload cancellation and audit logging

Preserve pre-start and batch cancellation requests, reject late upload success after cancellation, and wait for cancellation acknowledgements before removing queue entries.

Separate formatted link logs from privacy-safe source cleanup and upload plan audits, persist audit fallback paths, redact support bundles, and expose audit diagnostics safely.

Improve queue selection and destructive-action clarity, show the Settings save action only while changes are pending, and add regression coverage for all updated behavior.
This commit is contained in:
Sucukdeluxe
2026-08-13 14:31:46 +02:00
parent 59cead80c6
commit bfb3a39fed
20 changed files with 733 additions and 101 deletions
+31 -2
View File
@@ -39,6 +39,8 @@ class UploadManager extends EventEmitter {
this.activeJobs = new Map(); // uploadId -> { jobId, speedKbs, bytesUploaded, hoster }
this.jobAbortControllers = new Map(); // jobId -> AbortController
this.cancelledJobIds = new Set();
this.pendingCancelledJobIds = new Set();
this.pendingCancelAll = false;
this.sessionBytes = 0;
this._transientErrorTotal = 0;
this.lastStartTime = {}; // hoster -> timestamp of last upload start
@@ -330,14 +332,20 @@ class UploadManager extends EventEmitter {
}
async startBatch(tasks, opts = {}) {
const pendingCancelledJobIds = new Set(this.pendingCancelledJobIds);
const pendingCancelAll = this.pendingCancelAll;
this.pendingCancelledJobIds.clear();
this.pendingCancelAll = false;
this.running = true;
this.stopAfterActive = false;
this.stopAfterActive = pendingCancelAll;
this.abortController = new AbortController();
if (pendingCancelAll) this.abortController.abort();
this.startTime = Date.now();
this.sessionBytes = 0;
this.activeJobs.clear();
this.jobAbortControllers.clear();
this.cancelledJobIds.clear();
for (const jobId of pendingCancelledJobIds) this.cancelledJobIds.add(jobId);
this._doodApiKeyCache.clear(); // re-derive doodstream keys fresh each batch
this._baselineCache.clear(); // re-fetch baselines per batch (a long batch could outlast remote-side relevance)
this.semaphores = {};
@@ -447,6 +455,7 @@ class UploadManager extends EventEmitter {
const maxAttempts = Math.max(1, (settings.retries || 0) + 1);
const jobAbortController = new AbortController();
if (this.cancelledJobIds.has(jobId)) jobAbortController.abort();
const { signal, cleanup: cleanupSignals } = this._combineSignals(batchSignal, jobAbortController.signal);
this.jobAbortControllers.set(jobId, jobAbortController);
@@ -500,6 +509,12 @@ class UploadManager extends EventEmitter {
};
try {
if (signal.aborted || this.cancelledJobIds.has(jobId)) {
const error = 'Abgebrochen';
emitFinalStatus('aborted', { error, attempt: 0 });
recordFinalResult('aborted', { error });
return;
}
if (fileNotFound) {
const error = 'Datei nicht gefunden';
emitFinalStatus('skipped', { error, attempt: 0 });
@@ -714,6 +729,8 @@ class UploadManager extends EventEmitter {
const result = await this._executeUpload(task, progressCb, uploadSignalBundle.signal, throttle, fileProbe);
if (signal.aborted || this.cancelledJobIds.has(jobId)) throw new Error('Aborted');
const elapsed = Math.round((Date.now() - jobStart) / 1000);
this.sessionBytes += fileSize;
this.activeJobs.delete(uploadId);
@@ -847,6 +864,12 @@ class UploadManager extends EventEmitter {
this._noteSuspectReject(task.hoster, task.accountId, fileSize);
const alt = await this._trySuspectRejectAlternates(task, { uploadId, jobId, fileName, fileSize, settings, signal, fileProbe });
if (alt) {
if (signal.aborted || this.cancelledJobIds.has(jobId)) {
const error = 'Abgebrochen';
emitFinalStatus('aborted', { error });
recordFinalResult('aborted', { error });
return;
}
emitFinalStatus('done', { result: alt.result, speedKbs: alt.speedKbs, elapsed: alt.elapsed, attempt: 1 });
recordFinalResult('done', { result: alt.result });
return;
@@ -1017,6 +1040,7 @@ class UploadManager extends EventEmitter {
: hosterThrottle || globalThrottle;
const result = await this._executeUpload(task, progressCb, signal, throttle, fileProbe);
if (signal.aborted || this.cancelledJobIds.has(jobId)) throw new Error('Aborted');
this.activeJobs.delete(uploadId);
this.sessionBytes += fileSize;
emitFinalStatus('done', { result, speedKbs: currentSpeedKbs, elapsed: Math.round((Date.now() - jobStart) / 1000), attempt });
@@ -1159,6 +1183,7 @@ class UploadManager extends EventEmitter {
: hosterThrottle || globalThrottle;
try {
const result = await this._executeUpload(task, progressCb, signal, throttle, fileProbe);
if (signal.aborted || this.cancelledJobIds.has(jobId)) throw new Error('Aborted');
this.activeJobs.delete(uploadId);
this.sessionBytes += fileSize;
this._suspectGoodAccounts.set(task.hoster, account.id);
@@ -1442,6 +1467,7 @@ class UploadManager extends EventEmitter {
cancelJobs(jobIds) {
for (const jobId of jobIds || []) {
if (!jobId) continue;
if (!this.running) this.pendingCancelledJobIds.add(jobId);
this.cancelledJobIds.add(jobId);
const controller = this.jobAbortControllers.get(jobId);
if (controller && !controller.signal.aborted) {
@@ -1455,7 +1481,10 @@ class UploadManager extends EventEmitter {
}
cancel() {
if (!this.running) return;
if (!this.running) {
this.pendingCancelAll = true;
return;
}
this.abortController.abort();
this.stopAfterActive = true;
for (const controller of this.jobAbortControllers.values()) {