import { promises as fs } from "fs"; import db from '../../sql.mjs'; import lib from '../../lib.mjs'; import cfg from '../../config.mjs'; import queue from '../../queue.mjs'; import path from "path"; // Native multipart form data parser const parseMultipart = (buffer, boundary) => { const parts = {}; const boundaryBuffer = Buffer.from(`--${boundary}`); const segments = []; let start = 0; let idx; while ((idx = buffer.indexOf(boundaryBuffer, start)) !== -1) { if (start !== 0) { segments.push(buffer.slice(start, idx - 2)); // -2 for \r\n before boundary } start = idx + boundaryBuffer.length + 2; // +2 for \r\n after boundary } for (const segment of segments) { const headerEnd = segment.indexOf('\r\n\r\n'); if (headerEnd === -1) continue; const headers = segment.slice(0, headerEnd).toString(); const body = segment.slice(headerEnd + 4); const nameMatch = headers.match(/name="([^"]+)"/); const filenameMatch = headers.match(/filename="([^"]+)"/); const contentTypeMatch = headers.match(/Content-Type:\s*([^\r\n]+)/i); if (nameMatch) { const name = nameMatch[1]; if (filenameMatch) { parts[name] = { filename: filenameMatch[1], contentType: contentTypeMatch ? contentTypeMatch[1] : 'application/octet-stream', data: body }; } else { parts[name] = body.toString().trim(); } } } return parts; }; // Collect request body as buffer with debug logging const collectBody = (req) => { return new Promise((resolve, reject) => { console.log('[UPLOAD DEBUG] collectBody started'); const chunks = []; req.on('data', chunk => { // console.log(`[UPLOAD DEBUG] chunk received: ${chunk.length} bytes`); chunks.push(chunk); }); req.on('end', () => { console.log(`[UPLOAD DEBUG] Stream ended. Total size: ${chunks.reduce((acc, c) => acc + c.length, 0)}`); resolve(Buffer.concat(chunks)); }); req.on('error', err => { console.error('[UPLOAD DEBUG] Stream error:', err); reject(err); }); // Ensure stream is flowing if (req.isPaused()) { console.log('[UPLOAD DEBUG] Stream was paused, resuming...'); req.resume(); } }); }; export default router => { router.group(/^\/api\/v2/, group => { group.post(/\/upload$/, lib.loggedin, async (req, res) => { try { console.log('[UPLOAD DEBUG] Request received'); // Use stored content type if available (from middleware bypass), otherwise use header const contentType = req._multipartContentType || req.headers['content-type'] || ''; const boundaryMatch = contentType.match(/boundary=(.+)$/); if (!boundaryMatch) { console.log('[UPLOAD DEBUG] No boundary found'); return res.json({ success: false, msg: 'Invalid content type' }, 400); } let body; if (req.bodyPromise) { console.log('[UPLOAD DEBUG] Waiting for buffered body from middleware promise...'); body = await req.bodyPromise; console.log('[UPLOAD DEBUG] Received body from promise'); } else if (req.rawBody) { console.log('[UPLOAD DEBUG] Using buffered body from middleware'); body = req.rawBody; } else { console.log('[UPLOAD DEBUG] Collecting body via collectBody...'); body = await collectBody(req); } if (!body) { return res.json({ success: false, msg: 'Failed to receive file body' }, 400); } console.log('[UPLOAD DEBUG] Body size:', body.length); const parts = parseMultipart(body, boundaryMatch[1]); console.log('[UPLOAD DEBUG] Parsed parts:', Object.keys(parts)); // Validate required fields const file = parts.file; const rating = parts.rating; // 'sfw' or 'nsfw' const tagsRaw = parts.tags; // comma-separated tags if (!file || !file.data) { return res.json({ success: false, msg: 'No file provided' }, 400); } if (!rating || !['sfw', 'nsfw'].includes(rating)) { return res.json({ success: false, msg: 'Rating (sfw/nsfw) is required' }, 400); } const tags = tagsRaw ? tagsRaw.split(',').map(t => t.trim()).filter(t => t.length > 0) : []; if (tags.length < 3) { return res.json({ success: false, msg: 'At least 3 tags are required' }, 400); } // Validate MIME type const allowedMimes = ['video/mp4', 'video/webm']; let mime = file.contentType; if (!allowedMimes.includes(mime)) { return res.json({ success: false, msg: `Invalid file type. Only mp4 and webm allowed. Got: ${mime}` }, 400); } // Validate file size const maxfilesize = cfg.main.maxfilesize; const size = file.data.length; if (size > maxfilesize) { return res.json({ success: false, msg: `File too large. Max: ${lib.formatSize(maxfilesize)}, Got: ${lib.formatSize(size)}` }, 400); } // Generate UUID for filename const uuid = await queue.genuuid(); const ext = mime === 'video/mp4' ? 'mp4' : 'webm'; const filename = `${uuid}.${ext}`; const tmpPath = `./tmp/${filename}`; const destPath = `./public/b/${filename}`; // Save file temporarily await fs.writeFile(tmpPath, file.data); // Verify MIME with file command const actualMime = (await queue.exec(`file --mime-type -b ${tmpPath}`)).stdout.trim(); if (!allowedMimes.includes(actualMime)) { await fs.unlink(tmpPath).catch(() => { }); return res.json({ success: false, msg: `Invalid file type detected: ${actualMime}` }, 400); } // Generate checksum const checksum = (await queue.exec(`sha256sum ${tmpPath}`)).stdout.trim().split(" ")[0]; // Check for repost const repost = await queue.checkrepostsum(checksum); if (repost) { await fs.unlink(tmpPath).catch(() => { }); return res.json({ success: false, msg: `This file already exists`, repost: repost }, 409); } // Move to public folder await fs.copyFile(tmpPath, destPath); await fs.unlink(tmpPath).catch(() => { }); // Insert into database (active=false for admin approval) await db` insert into items ${db({ src: '', dest: filename, mime: actualMime, size: size, checksum: checksum, username: req.session.user, userchannel: 'web', usernetwork: 'web', stamp: ~~(Date.now() / 1000), active: false }, 'src', 'dest', 'mime', 'size', 'checksum', 'username', 'userchannel', 'usernetwork', 'stamp', 'active') } `; // Get the new item ID const itemid = await queue.getItemID(filename); // Generate thumbnail try { await queue.genThumbnail(filename, actualMime, itemid, ''); } catch (err) { await queue.exec(`magick ./mugge.png ./public/t/${itemid}.webp`); } // Assign rating tag (sfw=1, nsfw=2) const ratingTagId = rating === 'sfw' ? 1 : 2; await db` insert into tags_assign ${db({ item_id: itemid, tag_id: ratingTagId, user_id: req.session.id })} `; // Assign user tags for (const tagName of tags) { // Check if tag exists, create if not let tagRow = await db` select id from tags where normalized = slugify(${tagName}) limit 1 `; let tagId; if (tagRow.length === 0) { // Create new tag await db` insert into tags ${db({ tag: tagName }, 'tag')} `; tagRow = await db` select id from tags where normalized = slugify(${tagName}) limit 1 `; } tagId = tagRow[0].id; // Assign tag to item await db` insert into tags_assign ${db({ item_id: itemid, tag_id: tagId, user_id: req.session.id })} on conflict do nothing `; } return res.json({ success: true, msg: 'Upload successful! Your upload is pending admin approval.', itemid: itemid }); } catch (err) { console.error('[UPLOAD ERROR]', err); return res.json({ success: false, msg: 'Upload failed: ' + err.message }, 500); } }); }); return router; };