Compare commits

..

No commits in common. "7267adfd03d92856b6f968d3cf6e4441cee03431" and "74d7f8ce5a2913473c31fd1f384b6eead7aee094" have entirely different histories.

4 changed files with 6 additions and 83 deletions

View File

@ -160,9 +160,7 @@ function sleep(ms, signal) {
// Doodstream: { result: [{ download_url, protected_embed, filecode, protected_dl }] } // Doodstream: { result: [{ download_url, protected_embed, filecode, protected_dl }] }
function parseDoodstreamResult(payload) { function parseDoodstreamResult(payload) {
let item = {}; let item = {};
// Defensive: also handle direct callers that bypass uploadFile's payload const result = payload.result;
// normalisation (e.g. unit tests, future callers).
const result = payload && payload.result;
if (Array.isArray(result) && result.length > 0) { if (Array.isArray(result) && result.length > 0) {
item = result[0]; item = result[0];
} else if (result && typeof result === 'object') { } else if (result && typeof result === 'object') {
@ -197,20 +195,18 @@ function parseVoeResult(payload) {
// Byse: { files: [{ filecode, filename, status }] } // Byse: { files: [{ filecode, filename, status }] }
function parseByseResult(payload) { function parseByseResult(payload) {
// Defensive: bypass-callers may pass null/non-object directly.
if (!payload || typeof payload !== 'object') payload = {};
let file_code = null; let file_code = null;
let perFileError = null; let perFileError = null;
// Primary: files array (per official Byse API docs) // Primary: files array (per official Byse API docs)
if (Array.isArray(payload.files) && payload.files.length > 0) { if (Array.isArray(payload.files) && payload.files.length > 0) {
const f = payload.files[0]; const f = payload.files[0];
file_code = f && (f.filecode || f.file_code) || null; file_code = f.filecode || f.file_code || null;
// Byse returns HTTP 200 + msg=OK even when a specific file was rejected // Byse returns HTTP 200 + msg=OK even when a specific file was rejected
// ("Not video file format", "Duplicate", "File too small", ...). When // ("Not video file format", "Duplicate", "File too small", ...). When
// filecode is empty and status carries a non-OK message, that IS the // filecode is empty and status carries a non-OK message, that IS the
// actual per-file error, not a server problem. // actual per-file error, not a server problem.
if (!file_code && f && f.status && !/^(ok|success|done)$/i.test(String(f.status))) { if (!file_code && f.status && !/^(ok|success|done)$/i.test(String(f.status))) {
perFileError = String(f.status).trim(); perFileError = String(f.status).trim();
} }
} }
@ -483,16 +479,6 @@ async function uploadFile(hosterName, filePath, apiKey, onProgress, signal, thro
`Upload-Antwort von ${hosterName} war kein JSON (HTTP ${statusCode}${snippet ? `): ${snippet}` : ')'}` `Upload-Antwort von ${hosterName} war kein JSON (HTTP ${statusCode}${snippet ? `): ${snippet}` : ')'}`
); );
} }
// Normalize valid-but-not-object JSON (JSON.parse('null') → null;
// JSON.parse('"foo"') → string; JSON.parse('[1]') → array). Without this
// the downstream `payload.msg` / `payload.status` / parseResult(payload)
// calls crash with a confusing TypeError instead of letting the existing
// fallback defaults kick in. Arrays from servers that return a top-level
// list (rare but seen in the wild) are kept addressable as `payload.X`
// → undefined, which the parsers already handle.
if (payload === null || typeof payload !== 'object') {
payload = {};
}
if (statusCode < 200 || statusCode >= 300) { if (statusCode < 200 || statusCode >= 300) {
throw new Error( throw new Error(
@ -554,8 +540,6 @@ module.exports = {
HOSTER_CONFIGS, HOSTER_CONFIGS,
__test: { __test: {
extractUploadServerUrl, extractUploadServerUrl,
parseVoeResult, parseVoeResult
parseDoodstreamResult,
parseByseResult
} }
}; };

View File

@ -1,6 +1,6 @@
{ {
"name": "multi-hoster-uploader", "name": "multi-hoster-uploader",
"version": "3.3.14", "version": "3.3.13",
"description": "Upload files to doodstream, voe, vidmoly, byse simultaneously", "description": "Upload files to doodstream, voe, vidmoly, byse simultaneously",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {

View File

@ -15,11 +15,11 @@
- ✅ 3.3.11 — Patch-Bumps `eslint 10.1→10.2`, `undici 7.24→7.25`, `ws 8.19→8.20` (semver-compatible) - ✅ 3.3.11 — Patch-Bumps `eslint 10.1→10.2`, `undici 7.24→7.25`, `ws 8.19→8.20` (semver-compatible)
- ✅ 3.3.12 — Race condition fix: `uploadManager = null` in batch-done clobberte einen frisch gespawnten Manager wenn user mid-await neuen batch startete (deep-audit finding HIGH-1) - ✅ 3.3.12 — Race condition fix: `uploadManager = null` in batch-done clobberte einen frisch gespawnten Manager wenn user mid-await neuen batch startete (deep-audit finding HIGH-1)
- ✅ 3.3.13 — `save-global-settings-sync` reportet jetzt `returnValue=false` bei Fehlern + debugLog statt silent swallow; TOCTOU bei .bak-Refresh in beiden Pfaden (main.js + lib/config-store.js _atomicWrite) entkoppelt: bak-Read-Failure failt nicht mehr den ganzen Save (deep-audit findings HIGH-2 + MED-4) - ✅ 3.3.13 — `save-global-settings-sync` reportet jetzt `returnValue=false` bei Fehlern + debugLog statt silent swallow; TOCTOU bei .bak-Refresh in beiden Pfaden (main.js + lib/config-store.js _atomicWrite) entkoppelt: bak-Read-Failure failt nicht mehr den ganzen Save (deep-audit findings HIGH-2 + MED-4)
- ✅ 3.3.14 — Parser-null-payload guard: `uploadFile` normalisiert payload zu `{}` falls `JSON.parse('null')` o.ä.; `parseDoodstreamResult` + `parseByseResult` haben defensive guards für direct callers + 7 neue Unit-Tests (null/non-object, malformed entries, fileRejected/accountError flips, valid filecode happy path)
## Open items (priorisiert) ## Open items (priorisiert)
### Stabilität (neu aus deep-audit) ### Stabilität (neu aus deep-audit)
- [ ] `parseByseResult` / `parseDoodstreamResult` crash on null payload (lib/hosters.js:202, 214, 163, 491) — `JSON.parse('null')` returns null, then `payload.files` throws TypeError. Guard `if (!payload || typeof payload !== 'object') throw...` after JSON.parse.
- [ ] Cancellation latency in retry-loop's account-rotation block (lib/upload-manager.js:680-792) — re-check signal.aborted after each await. - [ ] Cancellation latency in retry-loop's account-rotation block (lib/upload-manager.js:680-792) — re-check signal.aborted after each await.
### Code-Qualität (deferred) ### Code-Qualität (deferred)

View File

@ -31,65 +31,4 @@ describe('hosters helpers', () => {
assert.equal(url, 'https://delivery-hydra.voe-network.net/upload/01'); assert.equal(url, 'https://delivery-hydra.voe-network.net/upload/01');
}); });
it('parseDoodstreamResult tolerates null/non-object payload without throwing', () => {
// Direct callers may bypass uploadFile's normalisation. The parser must
// never throw on bad input — empty fields are the contract.
for (const bad of [null, undefined, 'string', 42, true]) {
const r = __test.parseDoodstreamResult(bad);
assert.equal(r.file_code, null);
assert.equal(r.download_url, null);
assert.equal(r.embed_url, null);
}
});
it('parseDoodstreamResult handles result-as-array and result-as-object', () => {
const arr = __test.parseDoodstreamResult({ result: [{ filecode: 'AB1', protected_dl: 'https://x/1', protected_embed: 'https://x/e/1' }] });
assert.equal(arr.file_code, 'AB1');
assert.equal(arr.download_url, 'https://x/1');
assert.equal(arr.embed_url, 'https://x/e/1');
const obj = __test.parseDoodstreamResult({ result: { filecode: 'OBJ1', download_url: 'https://x/2' } });
assert.equal(obj.file_code, 'OBJ1');
assert.equal(obj.download_url, 'https://x/2');
});
it('parseByseResult tolerates null/non-object payload without throwing', () => {
for (const bad of [null, undefined, 'string', 42, []]) {
const r = __test.parseByseResult(bad);
assert.equal(r.file_code, null);
assert.equal(r.download_url, null);
assert.equal(r.embed_url, null);
}
});
it('parseByseResult handles malformed files entries (null, missing fields)', () => {
// Files array with a null first element (server returned [null])
const a = __test.parseByseResult({ files: [null] });
assert.equal(a.file_code, null);
// Files array with object missing both filecode and status
const b = __test.parseByseResult({ files: [{}] });
assert.equal(b.file_code, null);
});
it('parseByseResult throws fileRejected for non-OK status with empty filecode', () => {
assert.throws(
() => __test.parseByseResult({ files: [{ status: 'Not video file format' }] }),
(err) => err.fileRejected === true && /Not video file format/i.test(err.message)
);
});
it('parseByseResult flips to accountError for storage-exhausted phrasing', () => {
assert.throws(
() => __test.parseByseResult({ files: [{ status: 'not enough disk space on your account' }] }),
(err) => err.accountError === true
);
});
it('parseByseResult succeeds with valid filecode in files[0]', () => {
const r = __test.parseByseResult({ files: [{ filecode: 'GOOD123', status: 'OK' }] });
assert.equal(r.file_code, 'GOOD123');
assert.equal(r.download_url, 'https://byse.sx/d/GOOD123');
assert.equal(r.embed_url, 'https://byse.sx/e/GOOD123');
});
}); });