release: restore v2.1.19 baseline for v2.1.24
CI / verify (push) Has been cancelled

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:
Sucukdeluxe
2026-08-17 04:25:22 +02:00
parent 9a213a7395
commit d7c9f287e4
87 changed files with 2185 additions and 16125 deletions
+70 -209
View File
@@ -2,12 +2,6 @@ const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const { request } = require('undici');
const {
createTransportError,
safeEndpoint,
sanitizeRemoteText,
summarizeResponse
} = require('./hoster-transport-error');
const BASE_URL = 'https://doodstream.com';
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';
@@ -103,15 +97,8 @@ class DoodstreamUploader {
break;
} catch (err) {
if (opts.signal && opts.signal.aborted) throw err; // caller abort: don't retry
if (attempt >= 3) {
throw createTransportError('Doodstream: Webanfrage fehlgeschlagen', {
phase: 'web-request',
endpoint: url,
retryable: true,
transientNetwork: true
});
}
_debugLog(`_fetch transient (${attempt}/3) ${safeEndpoint(url) || 'unknown endpoint'}: ${err && err.name ? err.name : 'network error'}; retry`);
if (attempt >= 3) throw err;
_debugLog(`_fetch transient (${attempt}/3) ${url}: ${err && err.message}; retry`);
await new Promise(r => setTimeout(r, 400 * attempt));
}
}
@@ -180,35 +167,16 @@ class DoodstreamUploader {
// Explicit success response
} else if (json && json.message && /otp/i.test(json.message)) {
// OTP required — signal caller to collect OTP from user
const err = createTransportError(`Doodstream Login: ${sanitizeRemoteText(json.message)}`, {
phase: 'login',
endpoint: BASE_URL,
httpStatus: res.status,
contentType: res.headers && res.headers.get ? res.headers.get('content-type') : null,
body
});
const err = new Error(`Doodstream Login: ${json.message}`);
err.otpRequired = true;
throw err;
} else if (json && json.status === 'fail') {
throw createTransportError(`Doodstream Login: ${sanitizeRemoteText(json.message) || 'Login fehlgeschlagen'}`, {
phase: 'login',
endpoint: BASE_URL,
httpStatus: res.status,
contentType: res.headers && res.headers.get ? res.headers.get('content-type') : null,
body,
accountError: true
});
throw new Error(`Doodstream Login: ${json.message || 'Login fehlgeschlagen'}`);
} else if (body.includes('Dashboard')) {
// Got dashboard HTML directly — login worked
} else {
const msg = sanitizeRemoteText(json && json.message) || 'Login fehlgeschlagen';
throw createTransportError(`Doodstream Login: ${msg}`, {
phase: 'login',
endpoint: BASE_URL,
httpStatus: res.status,
contentType: res.headers && res.headers.get ? res.headers.get('content-type') : null,
body
});
const msg = (json && json.message) || 'Login fehlgeschlagen';
throw new Error(`Doodstream Login: ${msg}`);
}
}
@@ -252,7 +220,7 @@ class DoodstreamUploader {
const res = await this._fetch(BASE_URL + '/?op=upload_server');
const text = await res.text();
const ctype = (res.headers && res.headers.get) ? (res.headers.get('content-type') || '') : '';
_debugLog(`upload_server: status=${res.status} ctype=${ctype} response=${summarizeResponse(text, ctype)}`);
_debugLog(`upload_server: status=${res.status} ctype=${ctype} body(800)=${(text || '').slice(0, 800)}`);
let json;
try { json = JSON.parse(text); } catch { json = null; }
@@ -286,7 +254,7 @@ class DoodstreamUploader {
// Capture the form's real fields so upload() submits exactly what the
// browser would (file_title, submit_btn, …) instead of stale hardcoded ones.
this._uploadFormFields = this._parseUploadFormFields(html);
_debugLog(`upload_server: using form action node=${safeEndpoint(url)} sessLength=${this.sessId.length} fields=${Object.keys(this._uploadFormFields).join(',')}`);
_debugLog(`upload_server: using form action node=${url} sess=${this.sessId} fields=${Object.keys(this._uploadFormFields).join(',')}`);
return url;
}
@@ -297,19 +265,15 @@ class DoodstreamUploader {
// No upload server could be extracted. We MUST NOT silently fall back to a
// hardcoded node: that node is stale and accepts the bytes but returns an
// empty form (no filecode) — so the user wastes ~90s uploading 95 MB into a
// dead end and gets a cryptic "kein Filecode" 90s later. Fail fast with
// safe structured diagnostics.
_debugLog(`upload_server: no server response=${summarizeResponse(text, ctype)} page=${summarizeResponse(html, pageRes.headers && pageRes.headers.get ? pageRes.headers.get('content-type') : '')}`);
throw createTransportError('Doodstream: konnte Upload-Server nicht ermitteln', {
phase: 'upload-server',
endpoint: BASE_URL + '/?op=upload_server',
httpStatus: res.status,
contentType: ctype,
body: text,
retryable: res.status >= 500,
transientNetwork: res.status >= 500,
hosterTransient: res.status >= 500
});
// dead end and gets a cryptic "kein Filecode" 90s later. Fail fast and put
// the raw responses in the error so the real format change is diagnosable.
const urlHints = (html.match(/https?:\/\/[^'">\s]+/g) || []).slice(0, 4).join(' , ');
_debugLog(`upload_server: NO SERVER. upload-page html(2000)=${(html || '').slice(0, 2000)}`);
throw new Error(
`Doodstream: konnte Upload-Server nicht ermitteln (Endpoint geändert?). ` +
`op=upload_server status=${res.status} ctype=${ctype} body=${(text || '').slice(0, 300)} ` +
`| upload-page URL-Treffer: ${urlHints || 'keine'}`
);
}
/**
@@ -393,7 +357,7 @@ class DoodstreamUploader {
let uploadRes;
try {
uploadRes = await this._requestUpload(uploadUrl, {
uploadRes = await request(uploadUrl, {
method: 'POST',
headers: {
'Content-Type': `multipart/form-data; boundary=${boundary}`,
@@ -406,15 +370,15 @@ class DoodstreamUploader {
bodyTimeout: UPLOAD_TIMEOUT,
headersTimeout: 60000
});
} catch {
} catch (err) {
// Label which phase failed so a future "fetch failed"/"terminated" is
// attributable to the big upload POST vs the small bookend requests. The
// original message is preserved as a substring so upload-manager's
// transient classification still matches. NOTE: undici may surface
// "terminated"/"other side closed", which are not yet in that transient
// list — revisit if logs show them.
const mb = Math.round(bytesRead / 1048576);
throw createTransportError(`Doodstream Upload-POST nach ${mb} MB fehlgeschlagen`, {
phase: 'upload-request',
endpoint: uploadUrl,
retryable: true,
transientNetwork: true,
remoteCommitUncertain: true
});
throw new Error(`Doodstream Upload-POST (${mb} MB an ${uploadUrl}): ${err && err.message ? err.message : err}`);
}
const statusCode = uploadRes.statusCode;
@@ -430,69 +394,27 @@ class DoodstreamUploader {
}
}
let resText;
try {
resText = await uploadRes.body.text();
} catch {
throw createTransportError('Doodstream Upload-Antwort konnte nicht gelesen werden', {
phase: 'upload-response-read',
endpoint: uploadUrl,
retryable: true,
transientNetwork: true,
remoteCommitUncertain: true
});
}
const uploadContentType = uploadRes.headers && uploadRes.headers['content-type'];
_debugLog(`Upload response: ${summarizeResponse(resText, uploadContentType)}`);
const resText = await uploadRes.body.text();
_debugLog(`Upload response body (first 500): ${resText.slice(0, 500)}`);
if (statusCode >= 400) {
let payload;
try { payload = JSON.parse(resText); } catch {}
const msg = payload && payload.msg ? sanitizeRemoteText(payload.msg) : '';
throw createTransportError(`Doodstream Upload fehlgeschlagen${msg ? `: ${msg}` : ''}`, {
phase: 'upload-response',
endpoint: uploadUrl,
httpStatus: statusCode,
contentType: uploadContentType,
body: resText,
retryable: statusCode === 429 || statusCode >= 500,
transientNetwork: statusCode >= 500
});
const msg = payload && payload.msg ? payload.msg : resText.slice(0, 200);
throw new Error(`Doodstream Upload HTTP ${statusCode}: ${msg}`);
}
return this._parseUploadResponse(resText);
}
_requestUpload(url, options) {
return request(url, options);
}
/**
* Follow a redirect URL from upload server and extract filecode
*/
async _handleUploadResult(url) {
_debugLog(`Following upload result URL: ${safeEndpoint(url) || 'unknown endpoint'}`);
let res;
try {
res = await this._fetch(url);
} catch (error) {
if (error && typeof error === 'object') error.remoteCommitUncertain = true;
throw error;
}
let html;
try {
html = await res.text();
} catch {
throw createTransportError('Doodstream Ergebnis-Antwort konnte nicht gelesen werden', {
phase: 'upload-response-read',
endpoint: url,
retryable: true,
transientNetwork: true,
remoteCommitUncertain: true
});
}
const contentType = res.headers && typeof res.headers.get === 'function' ? res.headers.get('content-type') : '';
_debugLog(`Result page: ${summarizeResponse(html, contentType)}`);
_debugLog(`Following upload result URL: ${url}`);
const res = await this._fetch(url);
const html = await res.text();
_debugLog(`Result page (first 500): ${html.slice(0, 500)}`);
return this._parseUploadResponse(html);
}
@@ -536,12 +458,12 @@ class DoodstreamUploader {
// 3. Parse HTML form (XFileSharing two-step upload)
const hiddenFields = this._extractHiddenFields(resText);
_debugLog(`Hidden fields: ${Object.keys(hiddenFields).join(',')}`);
_debugLog(`Hidden fields: ${JSON.stringify(hiddenFields)}`);
// Check if filecode is already in hidden fields
const fnCode = hiddenFields.fn || hiddenFields.filecode || hiddenFields.file_code;
if (fnCode && fnCode.length >= 8) {
_debugLog(`Filecode from hidden field 'fn': length ${fnCode.length}`);
_debugLog(`Filecode from hidden field 'fn': ${fnCode}`);
// We still need to submit the form so doodstream registers the file
// But the filecode is the 'fn' value
}
@@ -552,7 +474,7 @@ class DoodstreamUploader {
// Ensure op=upload_result is set
if (!hiddenFields.op) hiddenFields.op = 'upload_result';
_debugLog(`Submitting upload_result fields: ${Object.keys(hiddenFields).join(',')}`);
_debugLog(`Submitting upload_result to ${BASE_URL}/ with fields: ${JSON.stringify(hiddenFields)}`);
const formData = new URLSearchParams(hiddenFields);
let followText = '';
try {
@@ -565,23 +487,18 @@ class DoodstreamUploader {
body: formData.toString()
});
followText = await followRes.text();
} catch {
} catch (err) {
// The file already uploaded to the CDN; this POST only registers it on
// doodstream's side. If it fails transiently (even after _fetch's own
// retries) but we already hold the filecode, the upload succeeded from
// the user's view — return it rather than discarding a done upload.
if (fnCode && fnCode.length >= 8) {
_debugLog(`upload_result submit failed; using existing filecode length ${fnCode.length}`);
_debugLog(`upload_result submit failed (${err && err.message}); using fn ${fnCode}`);
return this._buildResult(fnCode);
}
throw createTransportError('Doodstream Upload: Ergebnis konnte nicht registriert werden', {
phase: 'upload-result-submit',
endpoint: BASE_URL,
retryable: true,
transientNetwork: true
});
throw err;
}
_debugLog(`upload_result response: ${summarizeResponse(followText, '')}`);
_debugLog(`upload_result response (first 500): ${followText.slice(0, 500)}`);
// Try to find filecode in result page
const resultCode = this._findFilecodeInHtml(followText);
@@ -606,17 +523,11 @@ class DoodstreamUploader {
// download link being empty while the page structure is unchanged points
// at doodstream's backend, not at a parsing bug on our side.
const st = hiddenFields.st || '';
const safeStatus = sanitizeRemoteText(st, 100);
const fnInfo = fnCode ? `vorhanden(len ${fnCode.length})` : 'fehlt/leer';
const node = safeEndpoint(this._lastUploadUrl) || 'unbekannt';
_debugLog(`No filecode. st=${safeStatus || '?'} fn=${fnInfo} node=${node} response=${summarizeResponse(resText, 'text/html')}`);
const fnInfo = fnCode ? `"${fnCode}"(len ${fnCode.length})` : 'fehlt/leer';
const node = this._lastUploadUrl || '?';
_debugLog(`No filecode. st=${st} fn=${fnInfo} node=${node} CDN-body=${(resText || '').slice(0, 400)}`);
if (st && st !== 'OK') {
throw createTransportError(`Doodstream lehnt Datei ab (Server-Status: ${safeStatus || 'unbekannt'})`, {
phase: 'upload-result',
endpoint: this._lastUploadUrl || BASE_URL,
contentType: 'text/html',
body: resText
});
throw new Error(`Doodstream lehnt Datei ab (Server-Status: ${st}). CDN=${node}`);
}
// Empty form (no fn, no st) is a doodstream-side processing flake — same
// account + same file works on a later attempt. Tag it explicitly so the
@@ -625,42 +536,26 @@ class DoodstreamUploader {
// session and later batches hit `pre-job-swap-blocked` for no fault of
// the account). The flag is the primary signal; the message text is a
// belt-and-suspenders regex fallback in the classifier.
throw createTransportError(`Doodstream Upload: kein Filecode (st=${safeStatus || '?'}, fn=${fnInfo}, CDN=${node})`, {
phase: 'upload-result',
endpoint: this._lastUploadUrl || BASE_URL,
contentType: 'text/html',
body: resText,
retryable: true,
hosterTransient: true
});
const emptyLinkErr = new Error(`Doodstream Upload: kein Filecode — Server gab leeren Link zurück (st=${st || '?'}, fn=${fnInfo}, CDN=${node}). CDN-Antwort: ${(resText || '').slice(0, 200)}`);
emptyLinkErr.hosterTransient = true;
throw emptyLinkErr;
}
// 4. Fallback: follow form action as-is (for non-XFS forms)
const formAction = resText.match(/<form[^>]*action=['"]([^'"]+)['"]/i);
if (formAction) {
_debugLog(`Fallback: following form action ${safeEndpoint(formAction[1]) || 'unknown endpoint'}`);
_debugLog(`Fallback: following form action ${formAction[1]}`);
const formData = new URLSearchParams(hiddenFields);
let followText;
try {
const followRes = await this._fetch(formAction[1], {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Referer': BASE_URL + '/'
},
body: formData.toString()
});
followText = await followRes.text();
} catch {
throw createTransportError('Doodstream Upload: Redirect-Antwort konnte nicht gelesen werden', {
phase: 'upload-result-submit',
endpoint: formAction[1],
retryable: true,
transientNetwork: true,
remoteCommitUncertain: true
});
}
_debugLog(`Fallback response: ${summarizeResponse(followText, '')}`);
const followRes = await this._fetch(formAction[1], {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Referer': BASE_URL + '/'
},
body: formData.toString()
});
const followText = await followRes.text();
_debugLog(`Fallback response (first 500): ${followText.slice(0, 500)}`);
const fallbackCode = this._findFilecodeInHtml(followText);
if (fallbackCode) return this._buildResult(fallbackCode);
@@ -668,23 +563,10 @@ class DoodstreamUploader {
// Check if fn was in original hidden fields
if (fnCode && fnCode.length >= 8) return this._buildResult(fnCode);
throw createTransportError('Doodstream Upload: Redirect-Antwort ungültig', {
phase: 'upload-result',
endpoint: formAction[1],
body: followText,
hosterTransient: true,
retryable: true
});
throw new Error(`Doodstream Upload: Redirect-Antwort ungültig (${followText.slice(0, 150)})`);
}
throw createTransportError('Doodstream Upload: Keine gültige Antwort', {
phase: 'upload-result',
endpoint: this._lastUploadUrl || BASE_URL,
contentType: /<\s*(?:!doctype|html|body|form|input)\b/i.test(resText) ? 'text/html' : 'text/plain',
body: resText,
hosterTransient: true,
retryable: true
});
throw new Error(`Doodstream Upload: Keine gültige Antwort (Body: ${resText.slice(0, 150)})`);
}
/**
@@ -708,15 +590,7 @@ class DoodstreamUploader {
*/
_extractFromJson(payload) {
if (payload.status && Number(payload.status) !== 200 && payload.msg) {
throw createTransportError(`Doodstream Upload: ${sanitizeRemoteText(payload.msg) || 'Antwort wurde abgelehnt'}`, {
phase: 'upload-result',
endpoint: this._lastUploadUrl || BASE_URL,
httpStatus: Number(payload.status),
contentType: 'application/json',
body: JSON.stringify(payload),
retryable: Number(payload.status) === 429 || Number(payload.status) >= 500,
transientNetwork: Number(payload.status) >= 500
});
throw new Error(`Doodstream Upload: ${payload.msg}`);
}
let item = null;
@@ -728,28 +602,15 @@ class DoodstreamUploader {
}
if (!item) {
throw createTransportError('Doodstream Upload: Antwort enthielt kein Ergebnis', {
phase: 'upload-result',
endpoint: this._lastUploadUrl || BASE_URL,
contentType: 'application/json',
body: JSON.stringify(payload),
hosterTransient: true,
retryable: true
});
throw new Error(`Doodstream Upload fehlgeschlagen: ${payload.msg || JSON.stringify(payload).slice(0, 150)}`);
}
const fileCode = String(item.filecode || item.file_code || '').trim();
if (!fileCode) {
throw createTransportError('Doodstream Upload: Antwort enthielt keinen Filecode', {
phase: 'upload-result',
endpoint: this._lastUploadUrl || BASE_URL,
contentType: 'application/json',
body: JSON.stringify(payload),
hosterTransient: true,
retryable: true
});
}
return this._buildResult(fileCode);
const fileCode = item.filecode || item.file_code || '';
return {
download_url: item.download_url || item.protected_dl || (fileCode ? `https://doodstream.com/d/${fileCode}` : null),
embed_url: item.protected_embed || (fileCode ? `https://doodstream.com/e/${fileCode}` : null),
file_code: fileCode
};
}
_buildResult(fileCode) {
@@ -835,7 +696,7 @@ class DoodstreamUploader {
return key;
}
}
_debugLog(`api-key derive: ${candidates.length} candidate(s), none validated. response=${summarizeResponse(html, 'text/html')}`);
_debugLog(`api-key derive: ${candidates.length} candidate(s), none validated. settings html(2500)=${(html || '').slice(0, 2500)}`);
return null;
}
}