Compare commits
No commits in common. "287ebde1f50adfce493b0d7941d14cd00188b9e0" and "d2f903b8ba9af97793bf942f8b3aff7907c44701" have entirely different histories.
287ebde1f5
...
d2f903b8ba
@ -247,10 +247,7 @@ class DoodstreamUploader {
|
|||||||
} else {
|
} else {
|
||||||
_debugLog('upload_server: form action found but no sess_id on page; keeping existing sessId');
|
_debugLog('upload_server: form action found but no sess_id on page; keeping existing sessId');
|
||||||
}
|
}
|
||||||
// Capture the form's real fields so upload() submits exactly what the
|
_debugLog(`upload_server: using form action node=${url} sess=${this.sessId}`);
|
||||||
// browser would (file_title, submit_btn, …) instead of stale hardcoded ones.
|
|
||||||
this._uploadFormFields = this._parseUploadFormFields(html);
|
|
||||||
_debugLog(`upload_server: using form action node=${url} sess=${this.sessId} fields=${Object.keys(this._uploadFormFields).join(',')}`);
|
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -272,35 +269,6 @@ class DoodstreamUploader {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Replicate the non-file fields of doodstream's CURRENT upload form so our
|
|
||||||
* POST matches what the browser actually submits. Doodstream dropped the old
|
|
||||||
* `utype` field and added file_title / fakefilepc / submit_btn; submitting a
|
|
||||||
* stale/incomplete field set can make the node accept the bytes but skip
|
|
||||||
* registration (→ empty result form). We parse the live form rather than
|
|
||||||
* hardcode, so we track whatever fields doodstream uses now. The file input
|
|
||||||
* (type=file) is excluded — the file is streamed separately.
|
|
||||||
*/
|
|
||||||
_parseUploadFormFields(html) {
|
|
||||||
const fields = {};
|
|
||||||
if (!html) return fields;
|
|
||||||
// Narrow to the upload form (its action points at a /upload/ node).
|
|
||||||
const formMatch = html.match(/<form[^>]*\baction=["'][^"']*\/upload\/[^"']*["'][\s\S]*?<\/form>/i);
|
|
||||||
const scope = formMatch ? formMatch[0] : html;
|
|
||||||
const re = /<(?:input|button)\b([^>]*)>/gi;
|
|
||||||
let m;
|
|
||||||
while ((m = re.exec(scope)) !== null) {
|
|
||||||
const attrs = m[1];
|
|
||||||
const typeM = attrs.match(/\btype=["']([^"']*)["']/i);
|
|
||||||
if (typeM && typeM[1].toLowerCase() === 'file') continue;
|
|
||||||
const nameM = attrs.match(/\bname=["']([^"']+)["']/i);
|
|
||||||
if (!nameM) continue;
|
|
||||||
const valM = attrs.match(/\bvalue=["']([^"']*)["']/i);
|
|
||||||
fields[nameM[1]] = valM ? valM[1] : '';
|
|
||||||
}
|
|
||||||
return fields;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Upload file using web session
|
* Upload file using web session
|
||||||
*/
|
*/
|
||||||
@ -317,16 +285,10 @@ class DoodstreamUploader {
|
|||||||
// Build multipart form
|
// Build multipart form
|
||||||
const boundary = `----WebKitFormBoundary${crypto.randomBytes(16).toString('hex')}`;
|
const boundary = `----WebKitFormBoundary${crypto.randomBytes(16).toString('hex')}`;
|
||||||
|
|
||||||
// Build form parts. Submit the live form's fields (parsed in
|
// Build form parts
|
||||||
// _getUploadServer) so our POST matches the browser; merge in sess_id (the
|
|
||||||
// fresh node token) and keep utype=reg as a harmless compatibility extra.
|
|
||||||
// Falls back to the minimal known-good set if the form wasn't parsed.
|
|
||||||
const formFields = { utype: 'reg', ...(this._uploadFormFields || {}) };
|
|
||||||
formFields.sess_id = this.sessId;
|
|
||||||
let preamble = '';
|
let preamble = '';
|
||||||
for (const [name, value] of Object.entries(formFields)) {
|
preamble += `--${boundary}\r\nContent-Disposition: form-data; name="sess_id"\r\n\r\n${this.sessId}\r\n`;
|
||||||
preamble += `--${boundary}\r\nContent-Disposition: form-data; name="${name}"\r\n\r\n${value}\r\n`;
|
preamble += `--${boundary}\r\nContent-Disposition: form-data; name="utype"\r\n\r\nreg\r\n`;
|
||||||
}
|
|
||||||
const safeFileName = fileName.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
const safeFileName = fileName.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||||||
preamble += `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${safeFileName}"\r\nContent-Type: application/octet-stream\r\n\r\n`;
|
preamble += `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${safeFileName}"\r\nContent-Type: application/octet-stream\r\n\r\n`;
|
||||||
|
|
||||||
|
|||||||
@ -468,8 +468,6 @@ async function _fetchDoodstreamFileList(apiKey, signal) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const DOODSTREAM_POLL = { attempts: 12, delayMs: 2500 }; // test-tunable via __test
|
|
||||||
|
|
||||||
async function _resolveDoodstreamUploadByName(apiKey, fileName, baselineCodes, signal) {
|
async function _resolveDoodstreamUploadByName(apiKey, fileName, baselineCodes, signal) {
|
||||||
// Same recovery byse uses: the upload POST returned no filecode, but the file
|
// Same recovery byse uses: the upload POST returned no filecode, but the file
|
||||||
// may register in the account a little later. Poll the list for a NEW file
|
// may register in the account a little later. Poll the list for a NEW file
|
||||||
@ -477,8 +475,8 @@ async function _resolveDoodstreamUploadByName(apiKey, fileName, baselineCodes, s
|
|||||||
// (never "take the only new one") so parallel doodstream uploads can't claim
|
// (never "take the only new one") so parallel doodstream uploads can't claim
|
||||||
// each other's files.
|
// each other's files.
|
||||||
const expected = _normalizeFileTitle(fileName);
|
const expected = _normalizeFileTitle(fileName);
|
||||||
const POLL_ATTEMPTS = DOODSTREAM_POLL.attempts;
|
const POLL_ATTEMPTS = 12;
|
||||||
const POLL_DELAY_MS = DOODSTREAM_POLL.delayMs;
|
const POLL_DELAY_MS = 2500;
|
||||||
for (let i = 0; i < POLL_ATTEMPTS; i++) {
|
for (let i = 0; i < POLL_ATTEMPTS; i++) {
|
||||||
if (signal && signal.aborted) return null;
|
if (signal && signal.aborted) return null;
|
||||||
const list = await _fetchDoodstreamFileList(apiKey, signal);
|
const list = await _fetchDoodstreamFileList(apiKey, signal);
|
||||||
@ -640,7 +638,6 @@ module.exports = {
|
|||||||
extractUploadServerUrl,
|
extractUploadServerUrl,
|
||||||
parseVoeResult,
|
parseVoeResult,
|
||||||
parseDoodstreamResult,
|
parseDoodstreamResult,
|
||||||
parseByseResult,
|
parseByseResult
|
||||||
DOODSTREAM_POLL
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "multi-hoster-uploader",
|
"name": "multi-hoster-uploader",
|
||||||
"version": "3.3.33",
|
"version": "3.3.32",
|
||||||
"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": {
|
||||||
|
|||||||
@ -1,105 +0,0 @@
|
|||||||
const { test, before, after } = require('node:test');
|
|
||||||
const assert = require('node:assert');
|
|
||||||
const fs = require('fs');
|
|
||||||
const os = require('os');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
// Mock the undici transport BEFORE requiring hosters so the destructured
|
|
||||||
// `request` picks up our stub. apiGet (getUploadServer) uses global fetch, which
|
|
||||||
// we override per-test. This exercises the FULL doodstream API upload + recovery
|
|
||||||
// orchestration against the doc-verified response shapes — the gap between the
|
|
||||||
// already-tested parseDoodstreamResult helper and the real uploadFile path.
|
|
||||||
// (mock.module needs an experimental flag npm test doesn't pass, so we reassign
|
|
||||||
// undici.request on the module object and refresh the hosters cache instead.)
|
|
||||||
let requestRouter = async () => ({ statusCode: 200, headers: {}, body: { text: async () => '{}' } });
|
|
||||||
const undici = require('undici');
|
|
||||||
const _origUndiciRequest = undici.request;
|
|
||||||
undici.request = (...a) => requestRouter(...a);
|
|
||||||
delete require.cache[require.resolve('../lib/hosters')];
|
|
||||||
const hostersMod = require('../lib/hosters');
|
|
||||||
const { uploadFile } = hostersMod;
|
|
||||||
|
|
||||||
let tmpFile;
|
|
||||||
let origFetch;
|
|
||||||
before(() => {
|
|
||||||
tmpFile = path.join(os.tmpdir(), `dood-itest-${process.pid}.mkv`);
|
|
||||||
fs.writeFileSync(tmpFile, Buffer.alloc(2048, 7));
|
|
||||||
origFetch = global.fetch;
|
|
||||||
// Keep the "never appears" recovery test fast (real default is 12 × 2.5 s).
|
|
||||||
hostersMod.__test.DOODSTREAM_POLL.attempts = 3;
|
|
||||||
hostersMod.__test.DOODSTREAM_POLL.delayMs = 5;
|
|
||||||
});
|
|
||||||
after(() => {
|
|
||||||
global.fetch = origFetch;
|
|
||||||
undici.request = _origUndiciRequest; // restore real transport for other test files
|
|
||||||
delete require.cache[require.resolve('../lib/hosters')];
|
|
||||||
try { fs.unlinkSync(tmpFile); } catch {}
|
|
||||||
});
|
|
||||||
|
|
||||||
// getUploadServer hits /api/upload/server via global fetch.
|
|
||||||
function stubUploadServer() {
|
|
||||||
global.fetch = async (url) => {
|
|
||||||
if (/upload\/server/.test(String(url))) {
|
|
||||||
return { status: 200, text: async () => JSON.stringify({ status: 200, result: 'https://node1.cloudatacdn.com/upload/01' }) };
|
|
||||||
}
|
|
||||||
return { status: 200, text: async () => '{"status":200}' };
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build an undici-style router. uploadBody is the POST result; listBodies is a
|
|
||||||
// queue consumed by successive /api/file/list calls (baseline, then polls).
|
|
||||||
function routeWith(uploadBody, listBodies = []) {
|
|
||||||
return async (url, opts) => {
|
|
||||||
const u = String(url);
|
|
||||||
if (/\/api\/file\/list/.test(u)) {
|
|
||||||
const body = listBodies.length ? listBodies.shift() : '{"status":200,"result":{"files":[]}}';
|
|
||||||
return { statusCode: 200, headers: {}, body: { text: async () => body } };
|
|
||||||
}
|
|
||||||
// Upload POST: drain the streamed body so the file handle closes.
|
|
||||||
if (opts && opts.body && typeof opts.body[Symbol.asyncIterator] === 'function') {
|
|
||||||
for await (const chunk of opts.body) { if (chunk && chunk.length === -1) break; }
|
|
||||||
}
|
|
||||||
return { statusCode: uploadBody.status, headers: { 'content-type': 'application/json' }, body: { text: async () => uploadBody.body } };
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
test('doodstream API upload: filecode returned directly is used', async () => {
|
|
||||||
stubUploadServer();
|
|
||||||
requestRouter = routeWith({
|
|
||||||
status: 200,
|
|
||||||
body: JSON.stringify({ status: 200, result: [{ filecode: 'DOODCODE1234', download_url: 'https://doodstream.com/d/DOODCODE1234', protected_embed: 'https://doodstream.com/e/DOODCODE1234' }] })
|
|
||||||
});
|
|
||||||
const res = await uploadFile('doodstream.com', tmpFile, 'VALIDKEY', null, null, null);
|
|
||||||
assert.equal(res.file_code, 'DOODCODE1234');
|
|
||||||
assert.equal(res.download_url, 'https://doodstream.com/d/DOODCODE1234');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('doodstream API upload: codeless result recovered via file-list name match', async () => {
|
|
||||||
stubUploadServer();
|
|
||||||
const fileName = path.basename(tmpFile).replace(/\.[^.]+$/, ''); // title doodstream stores
|
|
||||||
requestRouter = routeWith(
|
|
||||||
{ status: 200, body: JSON.stringify({ status: 200, msg: 'OK' }) }, // codeless upload
|
|
||||||
[
|
|
||||||
'{"status":200,"result":{"files":[]}}', // baseline (pre-upload)
|
|
||||||
`{"status":200,"result":{"files":[{"file_code":"RECOVER9999","title":"${fileName}"}]}}` // poll finds it
|
|
||||||
]
|
|
||||||
);
|
|
||||||
const res = await uploadFile('doodstream.com', tmpFile, 'VALIDKEY', null, null, null);
|
|
||||||
assert.equal(res.file_code, 'RECOVER9999');
|
|
||||||
assert.equal(res.download_url, 'https://doodstream.com/d/RECOVER9999');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('doodstream API upload: codeless + file never appears → throws hosterTransient (no account poison)', async () => {
|
|
||||||
stubUploadServer();
|
|
||||||
requestRouter = routeWith(
|
|
||||||
{ status: 200, body: JSON.stringify({ status: 200, msg: 'OK' }) },
|
|
||||||
[] // every file/list returns empty
|
|
||||||
);
|
|
||||||
await assert.rejects(
|
|
||||||
() => uploadFile('doodstream.com', tmpFile, 'VALIDKEY', null, null, null),
|
|
||||||
(err) => {
|
|
||||||
assert.equal(err.hosterTransient, true, 'codeless result must be tagged hosterTransient');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@ -64,31 +64,6 @@ test('happy path: link in result page wins', async () => {
|
|||||||
assert.equal(res.file_code, 'jjsuhr931ds9');
|
assert.equal(res.file_code, 'jjsuhr931ds9');
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- _parseUploadFormFields: replicate the current upload form faithfully ---
|
|
||||||
test('_parseUploadFormFields extracts the real form fields and excludes the file input', () => {
|
|
||||||
const up = new DoodstreamUploader();
|
|
||||||
const html = `
|
|
||||||
<form name="file" enctype="multipart/form-data" action="https://uxg.cloudatacdn.com/upload/01?TOK" method="post">
|
|
||||||
<input type="hidden" name="sess_id" value="TOK">
|
|
||||||
<input name="file" type="file" size="30" id="filepc">
|
|
||||||
<input name="fakefilepc" class="d-none" type="text" id="fakefilepc">
|
|
||||||
<input type="text" name="file_title" class="form-control">
|
|
||||||
<button type="submit" name="submit_btn" class="btn">Upload</button>
|
|
||||||
</form>`;
|
|
||||||
const f = up._parseUploadFormFields(html);
|
|
||||||
assert.equal(f.sess_id, 'TOK');
|
|
||||||
assert.equal(f.fakefilepc, '');
|
|
||||||
assert.equal(f.file_title, '');
|
|
||||||
assert.ok('submit_btn' in f);
|
|
||||||
assert.ok(!('file' in f), 'the file input must be excluded (streamed separately)');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('_parseUploadFormFields returns {} for markup without a form', () => {
|
|
||||||
const up = new DoodstreamUploader();
|
|
||||||
assert.deepEqual(up._parseUploadFormFields('<div>no form here</div>'), {});
|
|
||||||
assert.deepEqual(up._parseUploadFormFields(''), {});
|
|
||||||
});
|
|
||||||
|
|
||||||
// --- deriveApiKey: pull + validate the account API key from the web session ---
|
// --- deriveApiKey: pull + validate the account API key from the web session ---
|
||||||
test('_extractApiKeyCandidates finds the key in an input value and ranks api-context first', () => {
|
test('_extractApiKeyCandidates finds the key in an input value and ranks api-context first', () => {
|
||||||
const up = new DoodstreamUploader();
|
const up = new DoodstreamUploader();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user