diff --git a/lib/doodstream-upload.js b/lib/doodstream-upload.js index a111ac1..525fa5d 100644 --- a/lib/doodstream-upload.js +++ b/lib/doodstream-upload.js @@ -7,6 +7,13 @@ 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'; const UPLOAD_TIMEOUT = 1800000; // 30 min +function _debugLog(msg) { + try { + const ts = new Date().toISOString(); + fs.appendFileSync(path.join(process.cwd(), 'doodstream-debug.log'), `[${ts}] ${msg}\n`); + } catch {} +} + class DoodstreamUploader { constructor() { this.cookies = new Map(); @@ -193,73 +200,34 @@ class DoodstreamUploader { // Build multipart form const boundary = `----WebKitFormBoundary${crypto.randomBytes(16).toString('hex')}`; - const fileStream = fs.createReadStream(filePath); // Build form parts - const fields = { - sess_id: this.sessId, - utype: 'reg' - }; + let preamble = ''; + 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="utype"\r\n\r\nreg\r\n`; + preamble += `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${fileName}"\r\nContent-Type: application/octet-stream\r\n\r\n`; - const preamble = []; - for (const [key, val] of Object.entries(fields)) { - preamble.push( - `--${boundary}\r\nContent-Disposition: form-data; name="${key}"\r\n\r\n${val}\r\n` - ); + const epilogue = `\r\n--${boundary}--\r\n`; + const preambleBuf = Buffer.from(preamble, 'utf-8'); + const epilogueBuf = Buffer.from(epilogue, 'utf-8'); + const totalSize = preambleBuf.length + fileSize + epilogueBuf.length; + + const CHUNK_SIZE = 256 * 1024; + let bytesRead = 0; + + const self = this; + async function* generate() { + yield preambleBuf; + const fileStream = fs.createReadStream(filePath, { highWaterMark: CHUNK_SIZE }); + for await (const chunk of fileStream) { + if (signal && signal.aborted) throw new Error('Aborted'); + if (throttle) await throttle.consume(chunk.length, signal); + bytesRead += chunk.length; + yield chunk; + if (progressCb) progressCb(bytesRead, fileSize); + } + yield epilogueBuf; } - preamble.push( - `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${fileName}"\r\nContent-Type: application/octet-stream\r\n\r\n` - ); - - const preambleBuffer = Buffer.from(preamble.join('')); - const epilogue = Buffer.from(`\r\n--${boundary}--\r\n`); - const totalSize = preambleBuffer.length + fileSize + epilogue.length; - - // Assemble body - const { Readable } = require('stream'); - let bytesSent = 0; - - const bodyStream = new Readable({ - read() {} - }); - - // Push preamble - bodyStream.push(preambleBuffer); - bytesSent += preambleBuffer.length; - - // Pipe file with throttle support - fileStream.on('data', (chunk) => { - if (signal && signal.aborted) { - fileStream.destroy(); - bodyStream.destroy(); - return; - } - if (throttle) { - fileStream.pause(); - throttle.consume(chunk.length, signal).then(() => { - bodyStream.push(chunk); - bytesSent += chunk.length; - if (progressCb) progressCb(Math.max(0, bytesSent - preambleBuffer.length), fileSize); - fileStream.resume(); - }).catch(() => { - fileStream.destroy(); - bodyStream.destroy(); - }); - } else { - bodyStream.push(chunk); - bytesSent += chunk.length; - if (progressCb) progressCb(Math.max(0, bytesSent - preambleBuffer.length), fileSize); - } - }); - - fileStream.on('end', () => { - bodyStream.push(epilogue); - bodyStream.push(null); - }); - - fileStream.on('error', (err) => { - bodyStream.destroy(err); - }); const uploadRes = await request(uploadUrl, { method: 'POST', @@ -269,83 +237,190 @@ class DoodstreamUploader { 'User-Agent': USER_AGENT, 'Cookie': this._cookieHeader() }, - body: bodyStream, + body: generate(), signal, bodyTimeout: UPLOAD_TIMEOUT, headersTimeout: 60000 }); const statusCode = uploadRes.statusCode; + _debugLog(`Upload response status: ${statusCode}`); + + // Handle redirects from upload server (undici doesn't follow them) + if ([301, 302, 303, 307, 308].includes(statusCode)) { + const location = uploadRes.headers['location']; + try { await uploadRes.body.text(); } catch {} + _debugLog(`Upload redirect to: ${location}`); + if (location) { + return this._handleUploadResult(location); + } + } + const resText = await uploadRes.body.text(); - let payload; - try { payload = JSON.parse(resText); } catch {} + _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 ? payload.msg : resText.slice(0, 200); throw new Error(`Doodstream Upload HTTP ${statusCode}: ${msg}`); } - if (!payload) { - // Try to extract filecode directly from HTML - const codeMatch = resText.match(/filecode['":\s]+['"]([a-zA-Z0-9]+)['"]/i); - if (codeMatch) { - return { - download_url: `https://doodstream.com/d/${codeMatch[1]}`, - embed_url: `https://doodstream.com/e/${codeMatch[1]}`, - file_code: codeMatch[1] - }; - } + return this._parseUploadResponse(resText); + } - // Follow HTML form redirect (two-step upload) - const formAction = resText.match(/