Preserve uncertain remote commits and destructive source-cleanup requirements across cancellation, persistence failures, retries, and restart recovery. Revalidate account availability after admission waits and prevent late upload results from bypassing cancellation state. Strengthen release argument, CI tag, source allowlist, and secret scanning gates. Run the Electron smoke suite in a fully hidden offscreen harness that cannot reveal, focus, or elevate native windows.
This commit is contained in:
@@ -81,6 +81,19 @@
|
||||
return requiredHosters.filter((hoster) => provisional.has(hoster));
|
||||
}
|
||||
|
||||
function startedHosters(jobs, requiredHosters) {
|
||||
const values = [];
|
||||
let legacyMetadata = false;
|
||||
for (const job of jobs) {
|
||||
if (job.sourceCleanupMetadataVersion !== metadataVersion) continue;
|
||||
if (!Array.isArray(job.sourceCleanupStartedHosters)) legacyMetadata = true;
|
||||
else values.push(...job.sourceCleanupStartedHosters);
|
||||
}
|
||||
if (legacyMetadata) return [...requiredHosters];
|
||||
const started = new Set(uniqueHosters(values));
|
||||
return requiredHosters.filter((hoster) => started.has(hoster));
|
||||
}
|
||||
|
||||
function storedToken(jobs) {
|
||||
for (const job of jobs) {
|
||||
if (typeof job.sourceCleanupToken === 'string' && job.sourceCleanupToken) {
|
||||
@@ -98,13 +111,14 @@
|
||||
return null;
|
||||
}
|
||||
|
||||
function assignMetadata(jobs, token, requiredHosters, confirmed, provisional, fingerprint, touchedJobs, touchedSet) {
|
||||
function assignMetadata(jobs, token, requiredHosters, confirmed, provisional, started, fingerprint, touchedJobs, touchedSet) {
|
||||
for (const job of jobs) {
|
||||
job.sourceCleanupMetadataVersion = metadataVersion;
|
||||
job.sourceCleanupToken = token;
|
||||
job.sourceCleanupRequiredHosters = [...requiredHosters];
|
||||
job.sourceCleanupConfirmedHosters = [...confirmed];
|
||||
job.sourceCleanupProvisionalHosters = [...provisional];
|
||||
job.sourceCleanupStartedHosters = [...started];
|
||||
job.sourceCleanupFingerprint = cloneFingerprint(fingerprint);
|
||||
delete job.sourceCleanupCompletedHosters;
|
||||
if (!touchedSet.has(job)) {
|
||||
@@ -138,15 +152,16 @@
|
||||
...persistedRequired,
|
||||
...siblings.map((job) => job.hoster)
|
||||
]);
|
||||
const startedHosters = new Set(uniqueHosters(
|
||||
const currentStartedHosters = new Set(uniqueHosters(
|
||||
siblings.filter((job) => currentRoundJobs.has(job)).map((job) => job.hoster)
|
||||
));
|
||||
const started = uniqueHosters([...startedHosters(siblings, requiredHosters), ...currentStartedHosters]);
|
||||
const storedConfirmed = confirmedHosters(siblings, requiredHosters);
|
||||
const confirmed = storedConfirmed.filter((hoster) => !startedHosters.has(hoster));
|
||||
const confirmed = storedConfirmed.filter((hoster) => !currentStartedHosters.has(hoster));
|
||||
const provisional = provisionalHosters(siblings, requiredHosters)
|
||||
.filter((hoster) => !startedHosters.has(hoster));
|
||||
.filter((hoster) => !currentStartedHosters.has(hoster));
|
||||
for (const hoster of storedConfirmed) {
|
||||
if (!startedHosters.has(hoster) || revokedSet.has(hoster)) continue;
|
||||
if (!currentStartedHosters.has(hoster) || revokedSet.has(hoster)) continue;
|
||||
revokedSet.add(hoster);
|
||||
revokedHosters.push(hoster);
|
||||
}
|
||||
@@ -158,6 +173,7 @@
|
||||
requiredHosters,
|
||||
confirmed,
|
||||
provisional,
|
||||
started,
|
||||
fingerprint,
|
||||
touchedJobs,
|
||||
touchedSet
|
||||
@@ -249,6 +265,7 @@
|
||||
if (!job || job.status !== 'preview' || job.interrupted) return [];
|
||||
const siblings = relatedJobs(queueJobs, job, platform);
|
||||
const removedHoster = normalizeHoster(job.hoster);
|
||||
if (startedHosters(siblings, storedRequiredHosters(siblings)).includes(removedHoster)) return [];
|
||||
for (const sibling of siblings) {
|
||||
const required = Array.isArray(sibling.sourceCleanupRequiredHosters)
|
||||
? sibling.sourceCleanupRequiredHosters
|
||||
|
||||
+229
-45
@@ -54,6 +54,7 @@ class UploadManager extends EventEmitter {
|
||||
this._baselineCache = new Map(); // hoster:apiKey -> Promise<Set<file_code>> (one fetch shared across all jobs in batch)
|
||||
this._recoveryClaims = createRecoveryClaimRegistry();
|
||||
this._recoveryAuthModes = new Map();
|
||||
this._suspectResolutionGates = new Map();
|
||||
this._batchJobIds = new Set();
|
||||
this._batchTotal = 0;
|
||||
}
|
||||
@@ -74,6 +75,7 @@ class UploadManager extends EventEmitter {
|
||||
this._baselineCache.clear();
|
||||
if (!this.running) this._recoveryClaims.clear();
|
||||
if (!this.running) this._recoveryAuthModes.clear();
|
||||
if (!this.running) this._clearSuspectResolutionGates();
|
||||
}
|
||||
|
||||
switchAccount(hoster, fallbackAccount) {
|
||||
@@ -395,6 +397,7 @@ class UploadManager extends EventEmitter {
|
||||
this._recoveryClaims.clear();
|
||||
this._recoveryClaims = createRecoveryClaimRegistry();
|
||||
this._recoveryAuthModes.clear();
|
||||
this._clearSuspectResolutionGates();
|
||||
this.semaphores = {};
|
||||
this.globalSemaphore = null;
|
||||
this.globalThrottle = null;
|
||||
@@ -482,6 +485,7 @@ class UploadManager extends EventEmitter {
|
||||
|
||||
this._recoveryClaims.clear();
|
||||
this._recoveryAuthModes.clear();
|
||||
this._clearSuspectResolutionGates();
|
||||
this._doodApiKeyCache.clear();
|
||||
this._baselineCache.clear();
|
||||
this.emit('batch-done', summary);
|
||||
@@ -533,12 +537,16 @@ class UploadManager extends EventEmitter {
|
||||
embed_url: payload.result ? payload.result.embed_url || null : null,
|
||||
file_code: payload.result ? payload.result.file_code || null : null
|
||||
};
|
||||
if (payload.remoteCommitUncertain === true || payload.error?.remoteCommitUncertain === true || lastError?.remoteCommitUncertain === true) {
|
||||
result.remoteCommitUncertain = true;
|
||||
}
|
||||
|
||||
results.get(task.file).results.push(result);
|
||||
};
|
||||
|
||||
const emitFinalStatus = (status, payload = {}) => {
|
||||
if (status === 'aborted' && this.abortController.signal.aborted) return;
|
||||
const remoteCommitUncertain = payload.remoteCommitUncertain === true || payload.error?.remoteCommitUncertain === true || lastError?.remoteCommitUncertain === true;
|
||||
this._emitProgress(uploadId, fileName, task.hoster, { accountId: task.accountId,
|
||||
jobId,
|
||||
status,
|
||||
@@ -552,7 +560,8 @@ class UploadManager extends EventEmitter {
|
||||
failureDetails: payload.failureDetails || lastFailureDetails,
|
||||
result: payload.result || null,
|
||||
attempt: payload.attempt || maxAttempts,
|
||||
maxAttempts
|
||||
maxAttempts,
|
||||
remoteCommitUncertain
|
||||
});
|
||||
};
|
||||
|
||||
@@ -613,12 +622,9 @@ class UploadManager extends EventEmitter {
|
||||
// A previous file of at least this size already got a suspect rejection
|
||||
// on this exact account — skip the guaranteed-to-fail multi-GB upload
|
||||
// and go straight to the alternate-account walk below.
|
||||
let memoSuspect = null;
|
||||
if (fileProbe && fileProbe.isVideoLike === true && task.accountId
|
||||
&& this._suspectMemoBlocks(task.hoster, task.accountId, fileSize)) {
|
||||
memoSuspect = new Error('Bekanntes Größen-Limit auf diesem Account (frühere verdächtige Ablehnung)');
|
||||
memoSuspect.fileRejected = true;
|
||||
memoSuspect.suspectReject = true;
|
||||
let memoSuspect = this._createSuspectMemoError(task, fileProbe, fileSize);
|
||||
if (memoSuspect) {
|
||||
this._beginSuspectResolution(task.hoster, jobId);
|
||||
lastError = memoSuspect;
|
||||
this._rotLog('suspect-memo-skip', {
|
||||
jobId, hoster: task.hoster, fileName, accountId: task.accountId, fileSize
|
||||
@@ -746,9 +752,7 @@ class UploadManager extends EventEmitter {
|
||||
} catch { /* progress callbacks must never throw — swallowing is correct, the stream keeps going */ }
|
||||
};
|
||||
|
||||
const result = await this._executeUploadWithAdmission(task, progressCb, uploadSignalBundle.signal, throttle, fileProbe, true, jobId);
|
||||
|
||||
if (signal.aborted || this.cancelledJobIds.has(jobId)) throw new Error('Aborted');
|
||||
const result = await this._executeUploadWithAdmission(task, progressCb, uploadSignalBundle.signal, throttle, fileProbe, fileSize, true, jobId);
|
||||
|
||||
const elapsed = Math.round((Date.now() - jobStart) / 1000);
|
||||
this.sessionBytes += fileSize;
|
||||
@@ -785,6 +789,11 @@ class UploadManager extends EventEmitter {
|
||||
payloadSnippet: lastFailureDetails ? lastFailureDetails.responseSnippet || null : null
|
||||
});
|
||||
}
|
||||
if (err && err.remoteCommitUncertain === true) {
|
||||
lastError = err;
|
||||
break;
|
||||
}
|
||||
|
||||
if (signal.aborted) {
|
||||
lastError = new Error('Abgebrochen');
|
||||
break;
|
||||
@@ -795,11 +804,6 @@ class UploadManager extends EventEmitter {
|
||||
break;
|
||||
}
|
||||
|
||||
if (err && err.remoteCommitUncertain === true) {
|
||||
lastError = err;
|
||||
break;
|
||||
}
|
||||
|
||||
if (isSpeedRestart && attempt < maxAttempts) {
|
||||
lastError = new Error('Geschwindigkeit zu niedrig - Neustart');
|
||||
await this._sleep(3000, signal);
|
||||
@@ -855,6 +859,16 @@ class UploadManager extends EventEmitter {
|
||||
|
||||
const wasStopped = this.stopAfterActive && !signal.aborted;
|
||||
const wasAborted = signal.aborted || this.cancelledJobIds.has(jobId);
|
||||
if (lastError && lastError.remoteCommitUncertain === true) {
|
||||
const cancelledUncertain = lastError.cancelledAfterUploadStart === true;
|
||||
const status = cancelledUncertain ? 'aborted' : 'error';
|
||||
const error = cancelledUncertain
|
||||
? 'Abgebrochen; Remote-Status konnte nicht bestätigt werden'
|
||||
: (lastError.message || 'Remote-Upload konnte nicht eindeutig bestätigt werden');
|
||||
emitFinalStatus(status, { error, remoteCommitUncertain: true });
|
||||
recordFinalResult(status, { error, remoteCommitUncertain: true });
|
||||
return;
|
||||
}
|
||||
if (wasStopped || wasAborted) {
|
||||
const error = wasStopped ? 'Warteschlange angehalten' : 'Abgebrochen';
|
||||
emitFinalStatus('aborted', { error });
|
||||
@@ -888,12 +902,6 @@ 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;
|
||||
@@ -1063,8 +1071,7 @@ class UploadManager extends EventEmitter {
|
||||
? { consume: async (bytes, sig) => { await hosterThrottle.consume(bytes, sig); await globalThrottle.consume(bytes, sig); } }
|
||||
: hosterThrottle || globalThrottle;
|
||||
|
||||
const result = await this._executeUploadWithAdmission(task, progressCb, signal, throttle, fileProbe, true, jobId);
|
||||
if (signal.aborted || this.cancelledJobIds.has(jobId)) throw new Error('Aborted');
|
||||
const result = await this._executeUploadWithAdmission(task, progressCb, signal, throttle, fileProbe, fileSize, true, jobId);
|
||||
this.activeJobs.delete(uploadId);
|
||||
this.sessionBytes += fileSize;
|
||||
emitFinalStatus('done', { result, speedKbs: currentSpeedKbs, elapsed: Math.round((Date.now() - jobStart) / 1000), attempt });
|
||||
@@ -1085,6 +1092,7 @@ class UploadManager extends EventEmitter {
|
||||
rotationRetry: true
|
||||
});
|
||||
}
|
||||
if (err && err.remoteCommitUncertain === true) break;
|
||||
if (signal.aborted || this.stopAfterActive) break;
|
||||
if (this._isFileRejectedError(err)) break;
|
||||
if (this._isHosterTransientError(err)) break;
|
||||
@@ -1096,6 +1104,16 @@ class UploadManager extends EventEmitter {
|
||||
|
||||
const stoppedLate = this.stopAfterActive && !signal.aborted;
|
||||
const abortedLate = signal.aborted || this.cancelledJobIds.has(jobId);
|
||||
if (lastError && lastError.remoteCommitUncertain === true) {
|
||||
const cancelledUncertain = lastError.cancelledAfterUploadStart === true;
|
||||
const status = cancelledUncertain ? 'aborted' : 'error';
|
||||
const error = cancelledUncertain
|
||||
? 'Abgebrochen; Remote-Status konnte nicht bestätigt werden'
|
||||
: (lastError.message || 'Remote-Upload konnte nicht eindeutig bestätigt werden');
|
||||
emitFinalStatus(status, { error, remoteCommitUncertain: true });
|
||||
recordFinalResult(status, { error, remoteCommitUncertain: true });
|
||||
return;
|
||||
}
|
||||
if (stoppedLate || abortedLate) {
|
||||
const error = stoppedLate ? 'Warteschlange angehalten' : 'Abgebrochen';
|
||||
emitFinalStatus('aborted', { error });
|
||||
@@ -1109,14 +1127,22 @@ class UploadManager extends EventEmitter {
|
||||
emitFinalStatus('error', { error });
|
||||
recordFinalResult('error', { error });
|
||||
} catch (err) {
|
||||
if (err && err.remoteCommitUncertain === true) lastError = err;
|
||||
const wasStopped = this.stopAfterActive && !signal.aborted;
|
||||
const error = wasStopped
|
||||
const remoteCommitUncertain = !!(err && err.remoteCommitUncertain === true);
|
||||
const cancelledUncertain = remoteCommitUncertain && err.cancelledAfterUploadStart === true;
|
||||
const error = remoteCommitUncertain
|
||||
? (cancelledUncertain ? 'Abgebrochen; Remote-Status konnte nicht bestätigt werden' : (err.message || 'Remote-Upload konnte nicht eindeutig bestätigt werden'))
|
||||
: wasStopped
|
||||
? 'Warteschlange angehalten'
|
||||
: (signal.aborted || this.cancelledJobIds.has(jobId) ? 'Abgebrochen' : (err && err.message ? err.message : 'Unbekannter Fehler'));
|
||||
const status = signal.aborted || this.cancelledJobIds.has(jobId) || wasStopped ? 'aborted' : 'error';
|
||||
emitFinalStatus(status, { error });
|
||||
recordFinalResult(status === 'error' ? 'error' : 'aborted', { error });
|
||||
const status = remoteCommitUncertain
|
||||
? (cancelledUncertain ? 'aborted' : 'error')
|
||||
: (signal.aborted || this.cancelledJobIds.has(jobId) || wasStopped ? 'aborted' : 'error');
|
||||
emitFinalStatus(status, { error, remoteCommitUncertain });
|
||||
recordFinalResult(status, { error, remoteCommitUncertain });
|
||||
} finally {
|
||||
this._endSuspectResolution(task.hoster, jobId);
|
||||
this.activeJobs.delete(uploadId);
|
||||
this.jobAbortControllers.delete(jobId);
|
||||
cleanupSignals();
|
||||
@@ -1203,8 +1229,7 @@ class UploadManager extends EventEmitter {
|
||||
? { consume: async (bytes, sig) => { await hosterThrottle.consume(bytes, sig); await globalThrottle.consume(bytes, sig); } }
|
||||
: hosterThrottle || globalThrottle;
|
||||
try {
|
||||
const result = await this._executeUploadWithAdmission(task, progressCb, signal, throttle, fileProbe, false, jobId);
|
||||
if (signal.aborted || this.cancelledJobIds.has(jobId)) throw new Error('Aborted');
|
||||
const result = await this._executeUploadWithAdmission(task, progressCb, signal, throttle, fileProbe, fileSize, false, jobId);
|
||||
this.activeJobs.delete(uploadId);
|
||||
this.sessionBytes += fileSize;
|
||||
this._suspectGoodAccounts.set(task.hoster, account.id);
|
||||
@@ -1222,8 +1247,8 @@ class UploadManager extends EventEmitter {
|
||||
suspectAlternate: true
|
||||
});
|
||||
}
|
||||
if (signal.aborted || this.stopAfterActive) break;
|
||||
if (err && err.remoteCommitUncertain === true) throw err;
|
||||
if (signal.aborted || this.stopAfterActive) break;
|
||||
if (err && err.suspectReject === true) {
|
||||
this._noteSuspectReject(task.hoster, account.id, fileSize);
|
||||
}
|
||||
@@ -1255,27 +1280,47 @@ class UploadManager extends EventEmitter {
|
||||
return null;
|
||||
}
|
||||
|
||||
async _executeUploadWithAdmission(task, progressCb, signal, throttle, fileProbe, coordinateAccountFailure = true, jobId = task.jobId) {
|
||||
async _executeUploadWithAdmission(task, progressCb, signal, throttle, fileProbe, fileSize, coordinateAccountFailure = true, jobId = task.jobId) {
|
||||
while (true) {
|
||||
const context = await this._createRecoveryContext(task);
|
||||
this._throwIfUploadStartBlocked(signal);
|
||||
let retryAdmission = false;
|
||||
const operation = async () => {
|
||||
this._throwIfUploadStartBlocked(signal);
|
||||
const hosterSemaphore = this._getSemaphore(task.hoster);
|
||||
const globalSemaphore = this._getGlobalSemaphore();
|
||||
let hosterSlotAcquired = false;
|
||||
let globalSlotAcquired = false;
|
||||
const releaseSlots = () => {
|
||||
if (globalSlotAcquired && globalSemaphore) globalSemaphore.release();
|
||||
if (hosterSlotAcquired) hosterSemaphore.release();
|
||||
globalSlotAcquired = false;
|
||||
hosterSlotAcquired = false;
|
||||
};
|
||||
const acquireSlots = async () => {
|
||||
await hosterSemaphore.acquire(signal);
|
||||
hosterSlotAcquired = true;
|
||||
this._throwIfUploadStartBlocked(signal);
|
||||
if (globalSemaphore) {
|
||||
await globalSemaphore.acquire(signal);
|
||||
globalSlotAcquired = true;
|
||||
this._throwIfUploadStartBlocked(signal);
|
||||
}
|
||||
};
|
||||
try {
|
||||
if (this._swapFailedAccount(task, jobId, path.basename(task.file))) {
|
||||
retryAdmission = true;
|
||||
return null;
|
||||
await this._waitForSuspectResolution(task.hoster, jobId, signal);
|
||||
this._throwIfUploadStartBlocked(signal);
|
||||
const accountFailed = !!task.accountId && this._failedAccounts.has(task.hoster + ':' + task.accountId);
|
||||
if (accountFailed) {
|
||||
if (this._swapFailedAccount(task, jobId, path.basename(task.file))) {
|
||||
retryAdmission = true;
|
||||
return null;
|
||||
}
|
||||
if (!coordinateAccountFailure) {
|
||||
const error = new Error('Account became unavailable before upload');
|
||||
error.accountUnavailable = true;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
const settings = this._getSettings(task.hoster);
|
||||
if (settings.timeIntervalSec > 0) {
|
||||
@@ -1283,28 +1328,133 @@ class UploadManager extends EventEmitter {
|
||||
} else {
|
||||
await acquireSlots();
|
||||
}
|
||||
this._throwIfUploadStartBlocked(signal);
|
||||
if (this._hasForeignSuspectResolution(task.hoster, jobId)) {
|
||||
releaseSlots();
|
||||
await this._waitForSuspectResolution(task.hoster, jobId, signal);
|
||||
this._throwIfUploadStartBlocked(signal);
|
||||
retryAdmission = true;
|
||||
return null;
|
||||
}
|
||||
const lateMemoSuspect = coordinateAccountFailure
|
||||
? this._createSuspectMemoError(task, fileProbe, fileSize)
|
||||
: null;
|
||||
if (lateMemoSuspect) {
|
||||
this._rotLog('suspect-memo-skip', {
|
||||
jobId,
|
||||
hoster: task.hoster,
|
||||
fileName: path.basename(task.file),
|
||||
accountId: task.accountId,
|
||||
fileSize
|
||||
});
|
||||
this._beginSuspectResolution(task.hoster, jobId);
|
||||
throw lateMemoSuspect;
|
||||
}
|
||||
try {
|
||||
this._throwIfUploadStartBlocked(signal);
|
||||
return await this._executeUpload(task, progressCb, signal, throttle, fileProbe, context);
|
||||
} catch (err) {
|
||||
if (coordinateAccountFailure) await this._coordinateAccountFailure(task, err, signal, jobId);
|
||||
if (context.recoveryClaim && err && err.remoteCommitUncertain === true) {
|
||||
throw context.recoveryClaim.markUncertain(err);
|
||||
if (coordinateAccountFailure && err && err.suspectReject === true) {
|
||||
this._beginSuspectResolution(task.hoster, jobId);
|
||||
}
|
||||
if (err && err.remoteCommitUncertain === true) {
|
||||
if (context.recoveryClaim) throw context.recoveryClaim.markUncertain(err);
|
||||
throw err;
|
||||
}
|
||||
if (coordinateAccountFailure) await this._coordinateAccountFailure(task, err, signal, jobId);
|
||||
this._throwIfUploadStartBlocked(signal);
|
||||
throw err;
|
||||
}
|
||||
} finally {
|
||||
if (globalSlotAcquired && globalSemaphore) globalSemaphore.release();
|
||||
if (hosterSlotAcquired) hosterSemaphore.release();
|
||||
releaseSlots();
|
||||
}
|
||||
};
|
||||
const result = context.recoveryClaim
|
||||
? await context.recoveryClaim.runExclusive(operation, signal)
|
||||
: await operation();
|
||||
if (retryAdmission) continue;
|
||||
if (retryAdmission) {
|
||||
this._throwIfUploadStartBlocked(signal);
|
||||
continue;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
_throwIfUploadStartBlocked(signal) {
|
||||
if (signal && signal.aborted) {
|
||||
const error = new Error('Aborted');
|
||||
error.name = 'AbortError';
|
||||
throw error;
|
||||
}
|
||||
if (this.stopAfterActive) {
|
||||
const error = new Error('Warteschlange angehalten');
|
||||
error.stopAfterActive = true;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
_createSuspectMemoError(task, fileProbe, fileSize) {
|
||||
if (!fileProbe || fileProbe.isVideoLike !== true || !task.accountId) return null;
|
||||
if (!this._suspectMemoBlocks(task.hoster, task.accountId, fileSize)) return null;
|
||||
const error = new Error('Bekanntes Größen-Limit auf diesem Account (frühere verdächtige Ablehnung)');
|
||||
error.fileRejected = true;
|
||||
error.suspectReject = true;
|
||||
return error;
|
||||
}
|
||||
|
||||
_beginSuspectResolution(hoster, jobId) {
|
||||
const existing = this._suspectResolutionGates.get(hoster);
|
||||
if (existing) return existing.ownerJobId === jobId;
|
||||
let release;
|
||||
const promise = new Promise(resolve => {
|
||||
release = resolve;
|
||||
});
|
||||
this._suspectResolutionGates.set(hoster, { ownerJobId: jobId, promise, release });
|
||||
return true;
|
||||
}
|
||||
|
||||
_hasForeignSuspectResolution(hoster, jobId) {
|
||||
const gate = this._suspectResolutionGates.get(hoster);
|
||||
return !!gate && gate.ownerJobId !== jobId;
|
||||
}
|
||||
|
||||
_waitForSuspectResolution(hoster, jobId, signal) {
|
||||
const gate = this._suspectResolutionGates.get(hoster);
|
||||
if (!gate || gate.ownerJobId === jobId) return Promise.resolve();
|
||||
if (!signal) return gate.promise;
|
||||
if (signal.aborted) {
|
||||
const error = new Error('Aborted');
|
||||
error.name = 'AbortError';
|
||||
return Promise.reject(error);
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const onAbort = () => {
|
||||
signal.removeEventListener('abort', onAbort);
|
||||
const error = new Error('Aborted');
|
||||
error.name = 'AbortError';
|
||||
reject(error);
|
||||
};
|
||||
signal.addEventListener('abort', onAbort, { once: true });
|
||||
gate.promise.then(() => {
|
||||
signal.removeEventListener('abort', onAbort);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_endSuspectResolution(hoster, jobId) {
|
||||
const gate = this._suspectResolutionGates.get(hoster);
|
||||
if (!gate || gate.ownerJobId !== jobId) return false;
|
||||
this._suspectResolutionGates.delete(hoster);
|
||||
gate.release();
|
||||
return true;
|
||||
}
|
||||
|
||||
_clearSuspectResolutionGates() {
|
||||
for (const gate of this._suspectResolutionGates.values()) gate.release();
|
||||
this._suspectResolutionGates.clear();
|
||||
}
|
||||
|
||||
async _executeUpload(task, progressCb, signal, throttle, fileProbe, context) {
|
||||
const result = await this._executeUploadUnchecked(task, progressCb, signal, throttle, fileProbe, context);
|
||||
let confirmed;
|
||||
@@ -1317,6 +1467,40 @@ class UploadManager extends EventEmitter {
|
||||
return confirmed;
|
||||
}
|
||||
|
||||
async _awaitUploadResult(resultPromise, signal) {
|
||||
if (!signal) return resultPromise;
|
||||
const createAbortUncertainty = () => {
|
||||
const error = new Error('Abgebrochen; Remote-Status konnte nicht bestätigt werden');
|
||||
error.name = 'AbortError';
|
||||
error.remoteCommitUncertain = true;
|
||||
error.cancelledAfterUploadStart = true;
|
||||
return error;
|
||||
};
|
||||
if (signal.aborted) {
|
||||
void Promise.resolve(resultPromise).catch(() => {});
|
||||
throw createAbortUncertainty();
|
||||
}
|
||||
let onAbort;
|
||||
const abortPromise = new Promise((resolve, reject) => {
|
||||
onAbort = () => {
|
||||
reject(createAbortUncertainty());
|
||||
};
|
||||
signal.addEventListener('abort', onAbort, { once: true });
|
||||
});
|
||||
try {
|
||||
return await Promise.race([resultPromise, abortPromise]);
|
||||
} catch (error) {
|
||||
if (!signal.aborted) throw error;
|
||||
if (error && error.remoteCommitUncertain === true) {
|
||||
error.cancelledAfterUploadStart = true;
|
||||
throw error;
|
||||
}
|
||||
throw createAbortUncertainty();
|
||||
} finally {
|
||||
signal.removeEventListener('abort', onAbort);
|
||||
}
|
||||
}
|
||||
|
||||
async _createRecoveryContext(task) {
|
||||
const fileName = path.basename(task.file);
|
||||
if ((task.hoster === 'vidmoly.me' || task.hoster === 'voe.sx') && task.username) {
|
||||
@@ -1419,7 +1603,7 @@ class UploadManager extends EventEmitter {
|
||||
await dood.login(task.username, task.password);
|
||||
let result;
|
||||
try {
|
||||
result = await dood.upload(task.file, progressCb, signal, throttle);
|
||||
result = await this._awaitUploadResult(dood.upload(task.file, progressCb, signal, throttle), signal);
|
||||
} catch (err) {
|
||||
if (context.recoveryClaim && this._isDoodstreamRemoteCommitUncertain(err)) {
|
||||
throw context.recoveryClaim.markUncertain(err);
|
||||
@@ -1434,19 +1618,19 @@ class UploadManager extends EventEmitter {
|
||||
return result;
|
||||
} else if (task.hoster === 'clouddrop.cc') {
|
||||
const clouddrop = new ClouddropUploader(task.apiKey);
|
||||
return clouddrop.upload(task.file, progressCb, signal, throttle);
|
||||
return this._awaitUploadResult(clouddrop.upload(task.file, progressCb, signal, throttle), signal);
|
||||
} else {
|
||||
if (task.hoster === 'byse.sx' || task.hoster === 'doodstream.com' || task.hoster === 'voe.sx') {
|
||||
return this._executeRecoveryAwareApiUpload(task.hoster, task.file, task.apiKey, progressCb, signal, throttle, fileProbe, context.recoveryClaim);
|
||||
}
|
||||
return uploadFile(task.hoster, task.file, task.apiKey, progressCb, signal, throttle, {});
|
||||
return this._awaitUploadResult(uploadFile(task.hoster, task.file, task.apiKey, progressCb, signal, throttle, {}), signal);
|
||||
}
|
||||
}
|
||||
|
||||
async _executeRecoveryAwareLoginUpload(task, UploaderClass, progressCb, signal, throttle, recoveryClaim) {
|
||||
const uploader = new UploaderClass(recoveryClaim);
|
||||
await uploader.login(task.username, task.password);
|
||||
return uploader.upload(task.file, progressCb, signal, throttle);
|
||||
return this._awaitUploadResult(uploader.upload(task.file, progressCb, signal, throttle), signal);
|
||||
}
|
||||
|
||||
async _executeRecoveryAwareApiUpload(hosterName, filePath, apiKey, progressCb, signal, throttle, fileProbe, recoveryClaim) {
|
||||
@@ -1457,7 +1641,7 @@ class UploadManager extends EventEmitter {
|
||||
} else if (hosterName === 'doodstream.com') {
|
||||
options.doodBaseline = await this._getBaseline(hosterName, apiKey, signal);
|
||||
}
|
||||
return uploadFile(hosterName, filePath, apiKey, progressCb, signal, throttle, options);
|
||||
return this._awaitUploadResult(uploadFile(hosterName, filePath, apiKey, progressCb, signal, throttle, options), signal);
|
||||
}
|
||||
|
||||
_isDoodstreamRemoteCommitUncertain(error) {
|
||||
|
||||
+10
-1
@@ -24,6 +24,7 @@
|
||||
status,
|
||||
error: result.error || null,
|
||||
failureDetails: result.failureDetails || null,
|
||||
...(result.remoteCommitUncertain === true ? { remoteCommitUncertain: true } : {}),
|
||||
result: uploadResult
|
||||
});
|
||||
}
|
||||
@@ -75,7 +76,9 @@
|
||||
status: terminal.status,
|
||||
error: terminal.error || null,
|
||||
failureDetails: terminal.failureDetails || null,
|
||||
...(terminal.remoteCommitUncertain === true ? { remoteCommitUncertain: true } : {}),
|
||||
result: terminal.result || null,
|
||||
...(recovery?.historyPending === true ? { historyPending: true } : {}),
|
||||
interrupted: false
|
||||
};
|
||||
}
|
||||
@@ -83,5 +86,11 @@
|
||||
return { status, interrupted: interruptedIds.has(jobId) && !terminalStatuses.has(status) };
|
||||
}
|
||||
|
||||
return { buildFailedUploadSummary, buildTerminalJobSnapshots, getRecoveryOutcome };
|
||||
function resolveRemoteCommitUncertainty(previous, progress) {
|
||||
if (progress?.remoteCommitUncertain === true) return true;
|
||||
if (progress?.status === 'done') return false;
|
||||
return previous === true;
|
||||
}
|
||||
|
||||
return { buildFailedUploadSummary, buildTerminalJobSnapshots, getRecoveryOutcome, resolveRemoteCommitUncertainty };
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user