Restore the v2.1.19 application baseline and retain only the focused import preflight summary with duplicate, unavailable, destination, job, and size-limit visibility.
This commit is contained in:
+82
-225
@@ -2,8 +2,6 @@ const fs = require('fs');
|
||||
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';
|
||||
@@ -16,9 +14,8 @@ const RESULT_POLL_DELAY_MS = 2000;
|
||||
* Fallback when API-based upload fails or is unavailable.
|
||||
*/
|
||||
class VoeUploader {
|
||||
constructor(recoveryClaim = null) {
|
||||
constructor() {
|
||||
this.cookies = new Map();
|
||||
this.recoveryClaim = recoveryClaim;
|
||||
}
|
||||
|
||||
_cookieHeader() {
|
||||
@@ -163,58 +160,21 @@ class VoeUploader {
|
||||
* Returns { server: "https://cdn-xxx.edgeon-bandwidth.com/node/u/01", session_id: "..." }
|
||||
*/
|
||||
async _getDeliveryNode(csrfToken) {
|
||||
const endpoint = `${BASE_URL}/engine/delivery-node`;
|
||||
let res;
|
||||
try {
|
||||
res = await this._fetch(endpoint, {
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': csrfToken,
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
throw createTransportError('VOE: Upload-Server konnte nicht geladen werden', {
|
||||
phase: 'upload-server',
|
||||
endpoint,
|
||||
retryable: true,
|
||||
transientNetwork: true
|
||||
});
|
||||
}
|
||||
const res = await this._fetch(`${BASE_URL}/engine/delivery-node`, {
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': csrfToken,
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
const body = await res.text();
|
||||
const contentType = res.headers && typeof res.headers.get === 'function'
|
||||
? res.headers.get('content-type')
|
||||
: null;
|
||||
if (res.status < 200 || res.status >= 300) {
|
||||
throw createTransportError('VOE: Upload-Server konnte nicht geladen werden', {
|
||||
phase: 'upload-server',
|
||||
endpoint,
|
||||
httpStatus: res.status,
|
||||
contentType,
|
||||
body,
|
||||
retryable: res.status === 429 || res.status >= 500,
|
||||
transientNetwork: res.status >= 500
|
||||
});
|
||||
}
|
||||
let data;
|
||||
try { data = JSON.parse(body); } catch {
|
||||
throw createTransportError('VOE: Upload-Server Antwort war kein JSON', {
|
||||
phase: 'upload-server',
|
||||
endpoint,
|
||||
httpStatus: res.status,
|
||||
contentType,
|
||||
body
|
||||
});
|
||||
throw new Error(`VOE: Upload-Server Antwort war kein JSON: ${body.slice(0, 200)}`);
|
||||
}
|
||||
|
||||
if (!data || !data.success || !data.server) {
|
||||
throw createTransportError('VOE: Kein Upload-Server erhalten von delivery-node', {
|
||||
phase: 'upload-server',
|
||||
endpoint,
|
||||
httpStatus: res.status,
|
||||
contentType,
|
||||
body
|
||||
});
|
||||
throw new Error('VOE: Kein Upload-Server erhalten von delivery-node');
|
||||
}
|
||||
|
||||
return { uploadServer: data.server, sessionId: data.session_id || '' };
|
||||
@@ -223,54 +183,26 @@ class VoeUploader {
|
||||
/**
|
||||
* List current files via VOE API (for result polling fallback)
|
||||
*/
|
||||
async _fetchFileList(phase = 'recovery-poll') {
|
||||
const endpoint = `${BASE_URL}/api2/my-files?sort=date&order=dsc&page=1&per_page=50`;
|
||||
let res;
|
||||
async _fetchFileList() {
|
||||
try {
|
||||
res = await this._fetch(endpoint);
|
||||
const res = await this._fetch(`${BASE_URL}/api2/my-files?sort=date&order=dsc&page=1&per_page=50`);
|
||||
const body = await res.text();
|
||||
const data = JSON.parse(body);
|
||||
if (data && Array.isArray(data.data)) return data.data;
|
||||
if (data && Array.isArray(data.files)) return data.files;
|
||||
return [];
|
||||
} catch {
|
||||
throw createTransportError('VOE: Dateiliste konnte nicht geladen werden', {
|
||||
phase,
|
||||
endpoint,
|
||||
retryable: true,
|
||||
transientNetwork: true
|
||||
});
|
||||
return [];
|
||||
}
|
||||
const body = await res.text();
|
||||
const contentType = res.headers && typeof res.headers.get === 'function'
|
||||
? res.headers.get('content-type')
|
||||
: null;
|
||||
if (res.status < 200 || res.status >= 300) {
|
||||
throw createTransportError('VOE: Dateiliste konnte nicht geladen werden', {
|
||||
phase,
|
||||
endpoint,
|
||||
httpStatus: res.status,
|
||||
contentType,
|
||||
body,
|
||||
retryable: res.status === 429 || res.status >= 500,
|
||||
transientNetwork: res.status >= 500
|
||||
});
|
||||
}
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(body);
|
||||
} catch {
|
||||
throw createTransportError('VOE: Dateiliste war kein JSON', {
|
||||
phase,
|
||||
endpoint,
|
||||
httpStatus: res.status,
|
||||
contentType,
|
||||
body
|
||||
});
|
||||
}
|
||||
if (data && Array.isArray(data.data)) return data.data;
|
||||
if (data && Array.isArray(data.files)) return data.files;
|
||||
return [];
|
||||
}
|
||||
|
||||
async _captureFileCodes() {
|
||||
const files = await this._fetchFileList('recovery-baseline');
|
||||
return new Set(files.map(f => String(f.file_code || f.slug || '').trim()).filter(Boolean));
|
||||
try {
|
||||
const files = await this._fetchFileList();
|
||||
return new Set(files.map(f => String(f.file_code || f.slug || '').trim()).filter(Boolean));
|
||||
} catch {
|
||||
return new Set();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -280,14 +212,7 @@ class VoeUploader {
|
||||
async upload(filePath, onProgress, signal, throttle) {
|
||||
const fileName = path.basename(filePath);
|
||||
const fileSize = fs.statSync(filePath).size;
|
||||
let baselineCodes = null;
|
||||
let baselineError = null;
|
||||
try {
|
||||
baselineCodes = await this._captureFileCodes();
|
||||
} catch (err) {
|
||||
if (signal && signal.aborted) throw err;
|
||||
baselineError = err;
|
||||
}
|
||||
const baselineCodes = await this._captureFileCodes();
|
||||
|
||||
// Step 1: Get CSRF token from upload page
|
||||
const { csrfToken } = await this._getUploadParams();
|
||||
@@ -333,57 +258,27 @@ class VoeUploader {
|
||||
}
|
||||
|
||||
// Step 3: POST file to CDN upload server
|
||||
let uploadResponse;
|
||||
try {
|
||||
uploadResponse = await request(uploadServer, {
|
||||
method: 'POST',
|
||||
body: generate(),
|
||||
signal,
|
||||
headers: {
|
||||
'User-Agent': USER_AGENT,
|
||||
'Cookie': this._cookieHeader(),
|
||||
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
||||
'Content-Length': String(totalSize),
|
||||
'X-CSRF-TOKEN': csrfToken,
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Referer': `${BASE_URL}/file-upload`,
|
||||
'Origin': BASE_URL
|
||||
},
|
||||
headersTimeout: UPLOAD_TIMEOUT,
|
||||
bodyTimeout: UPLOAD_TIMEOUT
|
||||
});
|
||||
} catch (err) {
|
||||
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;
|
||||
const { body, headers } = await request(uploadServer, {
|
||||
method: 'POST',
|
||||
body: generate(),
|
||||
signal,
|
||||
headers: {
|
||||
'User-Agent': USER_AGENT,
|
||||
'Cookie': this._cookieHeader(),
|
||||
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
||||
'Content-Length': String(totalSize),
|
||||
'X-CSRF-TOKEN': csrfToken,
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Referer': `${BASE_URL}/file-upload`,
|
||||
'Origin': BASE_URL
|
||||
},
|
||||
headersTimeout: UPLOAD_TIMEOUT,
|
||||
bodyTimeout: UPLOAD_TIMEOUT
|
||||
});
|
||||
|
||||
this._parseCookiesFromHeaders(headers || {});
|
||||
|
||||
let rawBody;
|
||||
try {
|
||||
rawBody = await body.text();
|
||||
} catch (err) {
|
||||
throw this._markRemoteCommitUncertain(err);
|
||||
}
|
||||
if (statusCode < 200 || statusCode >= 300) {
|
||||
const error = createTransportError('VOE Upload fehlgeschlagen', {
|
||||
phase: 'upload-response',
|
||||
endpoint: uploadServer,
|
||||
httpStatus: statusCode,
|
||||
contentType: headers && headers['content-type'],
|
||||
body: rawBody,
|
||||
retryable: statusCode === 429 || statusCode >= 500,
|
||||
transientNetwork: statusCode >= 500
|
||||
});
|
||||
throw statusCode >= 500 ? this._markRemoteCommitUncertain(error) : error;
|
||||
}
|
||||
const rawBody = await body.text();
|
||||
|
||||
// Try JSON response
|
||||
try {
|
||||
@@ -400,48 +295,22 @@ class VoeUploader {
|
||||
|
||||
// Check for error
|
||||
if (json.error || json.message) {
|
||||
throw createTransportError(`VOE Upload-Fehler: ${sanitizeRemoteText(json.error || json.message)}`, {
|
||||
phase: 'upload-result',
|
||||
endpoint: uploadServer,
|
||||
contentType: 'application/json',
|
||||
body: rawBody
|
||||
});
|
||||
throw new Error(`VOE Upload-Fehler: ${json.error || json.message}`);
|
||||
}
|
||||
} catch (parseErr) {
|
||||
if (parseErr && parseErr.diagnostic) throw parseErr;
|
||||
if (parseErr.message.startsWith('VOE Upload-Fehler')) throw parseErr;
|
||||
// Not JSON - might be a redirect or HTML response
|
||||
}
|
||||
|
||||
// Fallback: poll the file list to find the newly uploaded file
|
||||
if (baselineCodes) {
|
||||
try {
|
||||
const result = await this._resolveUploadedFile(fileName, baselineCodes, signal);
|
||||
if (result) return result;
|
||||
} catch (err) {
|
||||
throw this._markRemoteCommitUncertain(err);
|
||||
}
|
||||
}
|
||||
const result = await this._resolveUploadedFile(fileName, baselineCodes, signal);
|
||||
if (result) return result;
|
||||
|
||||
if (baselineError) {
|
||||
baselineError.hosterTransient = true;
|
||||
throw this._markRemoteCommitUncertain(baselineError);
|
||||
}
|
||||
|
||||
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
|
||||
}));
|
||||
throw new Error('VOE Upload: Kein file_code in der Antwort gefunden');
|
||||
}
|
||||
|
||||
async _resolveUploadedFile(fileName, baselineCodes, signal) {
|
||||
if (!(baselineCodes instanceof Set)) return null;
|
||||
const expectedTitle = this._normalizeTitle(path.parse(fileName).name);
|
||||
let lastPollError = null;
|
||||
let successfulPoll = false;
|
||||
|
||||
for (let attempt = 0; attempt < RESULT_POLL_ATTEMPTS; attempt++) {
|
||||
if (signal && signal.aborted) {
|
||||
@@ -452,31 +321,29 @@ class VoeUploader {
|
||||
|
||||
let files = [];
|
||||
try {
|
||||
files = await this._fetchFileList('recovery-poll');
|
||||
successfulPoll = true;
|
||||
} catch (err) {
|
||||
if (err && err.name === 'AbortError') throw err;
|
||||
lastPollError = err;
|
||||
}
|
||||
files = await this._fetchFileList();
|
||||
} catch { files = []; }
|
||||
|
||||
const withCode = files.filter(f => f && (f.file_code || f.slug));
|
||||
const newFiles = withCode.filter(f => !baselineCodes.has(String(f.file_code || f.slug || '').trim()));
|
||||
const matches = newFiles
|
||||
.filter(file => {
|
||||
const title = this._normalizeTitle(file.title || file.name || '');
|
||||
return expectedTitle && title === expectedTitle;
|
||||
})
|
||||
.filter(file => {
|
||||
const code = String(file.file_code || file.slug || '').trim();
|
||||
return !this.recoveryClaim
|
||||
|| typeof this.recoveryClaim.has !== 'function'
|
||||
|| !this.recoveryClaim.has(code);
|
||||
});
|
||||
|
||||
if (matches.length > 1) return null;
|
||||
if (matches.length === 1) {
|
||||
const code = matches[0].file_code || matches[0].slug;
|
||||
return this._buildUrls(code, 'recovery-poll');
|
||||
if (newFiles.length > 0) {
|
||||
// Try to match by title
|
||||
let best = null;
|
||||
let bestScore = -1;
|
||||
|
||||
for (const file of newFiles) {
|
||||
const score = this._scoreCandidate(file, expectedTitle);
|
||||
if (score > bestScore) {
|
||||
bestScore = score;
|
||||
best = file;
|
||||
}
|
||||
}
|
||||
|
||||
if (best && (bestScore > 0 || newFiles.length === 1)) {
|
||||
const code = best.file_code || best.slug;
|
||||
return this._buildUrls(code);
|
||||
}
|
||||
}
|
||||
|
||||
if (attempt < RESULT_POLL_ATTEMPTS - 1) {
|
||||
@@ -484,41 +351,31 @@ class VoeUploader {
|
||||
}
|
||||
}
|
||||
|
||||
if (!successfulPoll && lastPollError) throw lastPollError;
|
||||
return null;
|
||||
}
|
||||
|
||||
_normalizeTitle(value) {
|
||||
return normalizeRecoveryTitle(value);
|
||||
return String(value || '')
|
||||
.toLowerCase()
|
||||
.normalize('NFKD')
|
||||
.replace(/[^a-z0-9]+/g, '');
|
||||
}
|
||||
|
||||
_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;
|
||||
_scoreCandidate(file, expectedTitle) {
|
||||
if (!file || !(file.file_code || file.slug)) return -1;
|
||||
if (!expectedTitle) return 0;
|
||||
|
||||
const title = this._normalizeTitle(file.title || file.name || '');
|
||||
if (!title) return -1;
|
||||
if (title === expectedTitle) return 120;
|
||||
if (title.startsWith(expectedTitle) || expectedTitle.startsWith(title)) return 90;
|
||||
if (title.includes(expectedTitle) || expectedTitle.includes(title)) return 70;
|
||||
return 0;
|
||||
}
|
||||
|
||||
_buildUrls(fileCode, phase = 'upload-result') {
|
||||
_buildUrls(fileCode) {
|
||||
const code = String(fileCode || '').trim();
|
||||
if (!code) return null;
|
||||
if (this.recoveryClaim
|
||||
&& typeof this.recoveryClaim.reserve === 'function'
|
||||
&& !this.recoveryClaim.reserve(code)) {
|
||||
const error = createTransportError('VOE Upload-Ergebnis ist bereits einem anderen Upload zugeordnet', {
|
||||
phase,
|
||||
endpoint: BASE_URL,
|
||||
retryable: true,
|
||||
hosterTransient: true
|
||||
});
|
||||
error.remoteIdentityClaimed = true;
|
||||
throw this._markRemoteCommitUncertain(error);
|
||||
}
|
||||
return {
|
||||
download_url: `${BASE_URL}/${code}`,
|
||||
embed_url: `${BASE_URL}/e/${code}`,
|
||||
|
||||
Reference in New Issue
Block a user