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:
+31
-2
@@ -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()) {
|
||||
|
||||
Reference in New Issue
Block a user