From 545fa8073473a905526c670772fcbc18e148d46b Mon Sep 17 00:00:00 2001 From: Eduard Prigoana Date: Sat, 21 Feb 2026 19:19:21 +0200 Subject: [PATCH] Update upload.js to allow chunked uploads --- functions/upload.js | 349 +++++++++++++++++++++++++------------------- 1 file changed, 200 insertions(+), 149 deletions(-) diff --git a/functions/upload.js b/functions/upload.js index 70d2f22..83eb78e 100644 --- a/functions/upload.js +++ b/functions/upload.js @@ -1,198 +1,249 @@ -const API_BASE = 'https://temp.imgur.gg/api/upload'; -const PING_URL = 'https://temp.imgur.gg/api/ping'; +const API_BASE = "https://temp.imgur.gg/api/upload"; +const COMPLETE_URL = "https://temp.imgur.gg/api/upload/complete"; +const PING_URL = "https://temp.imgur.gg/api/ping"; export async function onRequest(context) { const { request } = context; - if (request.method === 'OPTIONS') { + if (request.method === "OPTIONS") { return new Response(null, { status: 204, - headers: { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'POST, OPTIONS', - 'Access-Control-Allow-Headers': '*', - }, + headers: corsHeaders() }); } - if (request.method !== 'POST') { - return new Response(JSON.stringify({ error: 'Method not allowed' }), { - status: 405, - headers: { - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*', - }, - }); + if (request.method !== "POST") { + return jsonError("Method not allowed", 405); } try { - const contentType = request.headers.get('content-type') || ''; + const contentType = request.headers.get("content-type") || ""; let file; let fileName; let fileType; - if (contentType.includes('application/json')) { + /* ========================== + HANDLE FILE INPUT + ========================== */ + + if (contentType.includes("application/json")) { const body = await request.json(); + if (!body.fileUrl) { - return new Response(JSON.stringify({ error: 'No fileUrl provided' }), { - status: 400, - headers: { - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*', - }, - }); + return jsonError("No fileUrl provided", 400); } - const fileResponse = await fetch(body.fileUrl); - if (!fileResponse.ok) { - throw new Error('Failed to fetch remote file'); - } + const res = await fetch(body.fileUrl); + if (!res.ok) throw new Error("Failed to fetch remote file"); - const buffer = await fileResponse.arrayBuffer(); - file = buffer; - fileName = body.fileName || body.fileUrl.split('/').pop() || 'file'; - fileType = fileResponse.headers.get('content-type') || 'application/octet-stream'; + file = await res.arrayBuffer(); + fileName = body.fileName || body.fileUrl.split("/").pop(); + fileType = res.headers.get("content-type") || "application/octet-stream"; } else { - const formData = await request.formData(); - const uploadedFile = formData.get('file'); + const form = await request.formData(); + const uploaded = form.get("file"); - if (!uploadedFile) { - return new Response(JSON.stringify({ error: 'No file provided' }), { - status: 400, - headers: { - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*', - }, - }); + if (!uploaded) return jsonError("No file provided", 400); + + if (uploaded.size > 500 * 1024 * 1024) { + return jsonError("File exceeds 500MB", 400); } - if (uploadedFile.size > 500 * 1024 * 1024) { - return new Response( - JSON.stringify({ - error: 'File size exceeds 500MB limit', - size: uploadedFile.size, - }), - { - status: 400, - headers: { - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*', - }, - } - ); - } - - file = await uploadedFile.arrayBuffer(); - fileName = uploadedFile.name; - fileType = uploadedFile.type || 'application/octet-stream'; + file = await uploaded.arrayBuffer(); + fileName = uploaded.name; + fileType = uploaded.type || "application/octet-stream"; } - const pingResponse = await fetch(PING_URL, { - method: 'GET', - headers: { - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - }, + /* ========================== + GET UPLOAD METADATA + ========================== */ + + const ping = await fetch(PING_URL, { + method: "GET", + headers: userAgentHeaders() }); - const setCookie = pingResponse.headers.get('set-cookie') || ''; - const sessionCookie = setCookie.split(';').find((c) => c.trim().startsWith('_s=')) || ''; + const cookieHeader = ping.headers.get("set-cookie") || ""; + const sessionCookie = + cookieHeader.split(";").find(c => c.trim().startsWith("_s=")) || ""; - const metadataPayload = { - files: [ - { - fileName, - fileType, - fileSize: file.byteLength || 0, - }, - ], - }; - - const metadataResponse = await fetch(API_BASE, { - method: 'POST', + const metadataResp = await fetch(API_BASE, { + method: "POST", headers: { - Accept: '*/*', - 'Accept-Language': 'en-US,en;q=0.9', - 'Cache-Control': 'no-cache', - 'Content-Type': 'application/json', - Origin: 'https://temp.imgur.gg', - Pragma: 'no-cache', - Referer: 'https://temp.imgur.gg/', - 'Sec-Ch-Ua': '"Not A(Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"', - 'Sec-Ch-Ua-Mobile': '?0', - 'Sec-Ch-Ua-Platform': '"Windows"', - 'Sec-Fetch-Dest': 'empty', - 'Sec-Fetch-Mode': 'cors', - 'Sec-Fetch-Site': 'same-origin', - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - Cookie: sessionCookie, + ...userAgentHeaders(), + "Content-Type": "application/json", + Cookie: sessionCookie }, - body: JSON.stringify(metadataPayload), + body: JSON.stringify({ + files: [ + { + fileName, + fileType, + fileSize: file.byteLength + } + ] + }) }); - const metadataText = await metadataResponse.text(); + const metadataText = await metadataResp.text(); - if (!metadataResponse.ok) { - throw new Error(`Metadata request failed: ${metadataResponse.status} - ${metadataText}`); + if (!metadataResp.ok) { + throw new Error(`Metadata failed: ${metadataText}`); } const metadata = JSON.parse(metadataText); + const fileInfo = metadata.files?.[0]; - if (!metadata.success || !metadata.files || !metadata.files[0]) { - throw new Error('Metadata missing required fields'); + if (!fileInfo || !fileInfo.success) { + throw new Error("Invalid metadata response"); } - const fileInfo = metadata.files[0]; - const uploadUrl = fileInfo.uploadUrl; + /* ========================== + HANDLE UPLOAD TYPE + ========================== */ - if (!uploadUrl) { - throw new Error('No uploadUrl returned from metadata'); - } + let finalData; - const uploadResponse = await fetch(uploadUrl, { - method: 'PUT', - body: file, - headers: { - 'Content-Type': fileType, - }, - }); - - if (!uploadResponse.ok) { - const uploadText = await uploadResponse.text(); - throw new Error(`File upload failed: ${uploadResponse.status} - ${uploadText}`); + if (fileInfo.isMultipart) { + finalData = await handleMultipart(fileInfo, file); + } else { + finalData = await handleSingle(fileInfo.uploadUrl, file, fileType); } const publicUrl = `https://i.imgur.gg/${fileInfo.fileId}-${fileInfo.fileName}`; - return new Response( - JSON.stringify({ - success: true, - url: publicUrl, - fileId: fileInfo.fileId, - fileName: fileInfo.fileName, - }), - { - status: 200, - headers: { - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*', - }, - } - ); - } catch (error) { - return new Response( - JSON.stringify({ - error: 'Upload failed', - message: error.message, - }), - { - status: 500, - headers: { - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*', - }, - } - ); + return jsonResponse({ + success: true, + url: publicUrl, + fileId: fileInfo.fileId, + fileName: fileInfo.fileName + }); + + } catch (err) { + return jsonError(err.message, 500); } } + +/* ===================================================== */ +/* ================= SINGLE UPLOAD ===================== */ +/* ===================================================== */ + +async function handleSingle(uploadUrl, fileBuffer, fileType) { + if (!uploadUrl) { + throw new Error("Missing uploadUrl for single upload"); + } + + const res = await fetch(uploadUrl, { + method: "PUT", + body: fileBuffer, + headers: { + "Content-Type": fileType + } + }); + + if (!res.ok) { + const txt = await res.text(); + throw new Error(`Single upload failed: ${txt}`); + } + + return true; +} + +/* ===================================================== */ +/* ================= MULTIPART UPLOAD =================== */ +/* ===================================================== */ + +async function handleMultipart(fileInfo, fileBuffer) { + const { partUrls, partSize, uploadId, fileId } = fileInfo; + + if (!partUrls || !uploadId) { + throw new Error("Invalid multipart metadata"); + } + + const parts = []; + + for (let i = 0; i < partUrls.length; i++) { + const start = i * partSize; + const end = start + partSize; + const chunk = fileBuffer.slice(start, end); + + const uploadRes = await fetch(partUrls[i].url, { + method: "PUT", + body: chunk + }); + + if (!uploadRes.ok) { + const txt = await uploadRes.text(); + throw new Error(`Multipart part ${i + 1} failed: ${txt}`); + } + + let etag = uploadRes.headers.get("etag") || ""; + etag = etag.replace(/"/g, ""); // clean quotes + + parts.push({ + PartNumber: i + 1, + ETag: `"${etag}"` + }); + } + + /* ===== FINALIZE MULTIPART ===== */ + + const complete = await fetch(COMPLETE_URL, { + method: "PUT", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + fileId, + uploadId, + parts + }) + }); + + const completeText = await complete.text(); + + if (!complete.ok) { + throw new Error(`Multipart complete failed: ${completeText}`); + } + + const completeData = JSON.parse(completeText); + + if (!completeData.success) { + throw new Error("Multipart finalize returned failure"); + } + + return completeData; +} + +/* ===================================================== */ +/* ================= UTIL FUNCTIONS ===================== */ +/* ===================================================== */ + +function jsonResponse(obj, status = 200) { + return new Response(JSON.stringify(obj), { + status, + headers: { + "Content-Type": "application/json", + ...corsHeaders() + } + }); +} + +function jsonError(message, status) { + return jsonResponse({ success: false, error: message }, status); +} + +function corsHeaders() { + return { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "POST, OPTIONS", + "Access-Control-Allow-Headers": "*" + }; +} + +function userAgentHeaders() { + return { + "User-Agent": + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0" + }; +}