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:
+70
-209
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user