Harden batch-wide recovery identity claims
Scope remote code ownership to normalized hoster and account identities while retaining title-only recovery serialization and canonical Unicode title matching. Mark post-upload ambiguity and duplicate identities as uncertain so retries, account fallback, and later same-title jobs fail closed instead of reporting unsafe success. Acquire recovery title leases before hoster and global semaphores, revalidate failed-account overrides before upload, and clear claim state at batch boundaries. Add deterministic concurrent coverage for same-code rejection, distinct-code parallel success, uncertainty propagation, semaphore fairness, account isolation, Unicode equivalence, and registry lifetime.
This commit is contained in:
+34
-15
@@ -3,6 +3,7 @@ const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const { request } = require('undici');
|
||||
const { createTransportError, sanitizeRemoteText } = require('./hoster-transport-error');
|
||||
const { normalizeRecoveryTitle } = require('./hosters');
|
||||
|
||||
const BASE_URL = 'https://voe.sx';
|
||||
const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
|
||||
@@ -352,22 +353,27 @@ class VoeUploader {
|
||||
bodyTimeout: UPLOAD_TIMEOUT
|
||||
});
|
||||
} catch (err) {
|
||||
if (signal && signal.aborted) throw err;
|
||||
throw createTransportError('VOE Upload konnte nicht übertragen werden', {
|
||||
const error = signal && signal.aborted ? err : createTransportError('VOE Upload konnte nicht übertragen werden', {
|
||||
phase: 'upload-request',
|
||||
endpoint: uploadServer,
|
||||
retryable: true,
|
||||
transientNetwork: true
|
||||
});
|
||||
throw this._markRemoteCommitUncertain(error);
|
||||
}
|
||||
|
||||
const { body, headers, statusCode } = uploadResponse;
|
||||
|
||||
this._parseCookiesFromHeaders(headers || {});
|
||||
|
||||
const rawBody = await body.text();
|
||||
let rawBody;
|
||||
try {
|
||||
rawBody = await body.text();
|
||||
} catch (err) {
|
||||
throw this._markRemoteCommitUncertain(err);
|
||||
}
|
||||
if (statusCode < 200 || statusCode >= 300) {
|
||||
throw createTransportError('VOE Upload fehlgeschlagen', {
|
||||
const error = createTransportError('VOE Upload fehlgeschlagen', {
|
||||
phase: 'upload-response',
|
||||
endpoint: uploadServer,
|
||||
httpStatus: statusCode,
|
||||
@@ -376,6 +382,7 @@ class VoeUploader {
|
||||
retryable: statusCode === 429 || statusCode >= 500,
|
||||
transientNetwork: statusCode >= 500
|
||||
});
|
||||
throw statusCode >= 500 ? this._markRemoteCommitUncertain(error) : error;
|
||||
}
|
||||
|
||||
// Try JSON response
|
||||
@@ -407,23 +414,27 @@ class VoeUploader {
|
||||
|
||||
// Fallback: poll the file list to find the newly uploaded file
|
||||
if (baselineCodes) {
|
||||
const result = await this._resolveUploadedFile(fileName, baselineCodes, signal);
|
||||
if (result) return result;
|
||||
try {
|
||||
const result = await this._resolveUploadedFile(fileName, baselineCodes, signal);
|
||||
if (result) return result;
|
||||
} catch (err) {
|
||||
throw this._markRemoteCommitUncertain(err);
|
||||
}
|
||||
}
|
||||
|
||||
if (baselineError) {
|
||||
baselineError.hosterTransient = true;
|
||||
throw baselineError;
|
||||
throw this._markRemoteCommitUncertain(baselineError);
|
||||
}
|
||||
|
||||
throw createTransportError('VOE Upload: Kein file_code in der Antwort gefunden', {
|
||||
throw this._markRemoteCommitUncertain(createTransportError('VOE Upload: Kein file_code in der Antwort gefunden', {
|
||||
phase: 'upload-result',
|
||||
endpoint: uploadServer,
|
||||
contentType: headers && headers['content-type'],
|
||||
body: rawBody,
|
||||
hosterTransient: true,
|
||||
retryable: true
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
async _resolveUploadedFile(fileName, baselineCodes, signal) {
|
||||
@@ -478,11 +489,19 @@ class VoeUploader {
|
||||
}
|
||||
|
||||
_normalizeTitle(value) {
|
||||
return String(value || '')
|
||||
.toLowerCase()
|
||||
.normalize('NFKD')
|
||||
.replace(/\.[a-z0-9]+$/i, '')
|
||||
.replace(/[^a-z0-9]+/g, '');
|
||||
return normalizeRecoveryTitle(value);
|
||||
}
|
||||
|
||||
_markRemoteCommitUncertain(error) {
|
||||
if (this.recoveryClaim && typeof this.recoveryClaim.markUncertain === 'function') {
|
||||
return this.recoveryClaim.markUncertain(error);
|
||||
}
|
||||
const uncertainError = error && typeof error === 'object'
|
||||
? error
|
||||
: new Error('VOE Upload-Ergebnis ist unsicher');
|
||||
uncertainError.remoteCommitUncertain = true;
|
||||
uncertainError.hosterTransient = true;
|
||||
return uncertainError;
|
||||
}
|
||||
|
||||
_buildUrls(fileCode, phase = 'upload-result') {
|
||||
@@ -498,7 +517,7 @@ class VoeUploader {
|
||||
hosterTransient: true
|
||||
});
|
||||
error.remoteIdentityClaimed = true;
|
||||
throw error;
|
||||
throw this._markRemoteCommitUncertain(error);
|
||||
}
|
||||
return {
|
||||
download_url: `${BASE_URL}/${code}`,
|
||||
|
||||
Reference in New Issue
Block a user