import * as https from "https"; import * as http from "http"; import { URL } from "url"; const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100 MB const TIMEOUT_MS = 30000; // 30 seconds export async function downloadFile( urlStr: string, options?: { method?: string; headers?: Record; body?: string; }, ): Promise { const url = new URL(urlStr); const protocol = url.protocol === "https:" ? https : http; const method = options?.method ?? "GET"; const headers = options?.headers ?? {}; return new Promise((resolve, reject) => { const req = protocol.request( { hostname: url.hostname, port: url.port, path: url.pathname + url.search, method, headers, timeout: TIMEOUT_MS, }, (res) => { // Follow redirects if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { downloadFile(res.headers.location, options).then(resolve).catch(reject); return; } if (res.statusCode && res.statusCode !== 200) { reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`)); return; } const chunks: Buffer[] = []; let totalSize = 0; res.on("data", (chunk: Buffer) => { totalSize += chunk.length; if (totalSize > MAX_FILE_SIZE) { req.destroy(); reject(new Error(`File exceeds maximum size of ${MAX_FILE_SIZE} bytes`)); return; } chunks.push(chunk); }); res.on("end", () => { resolve(Buffer.concat(chunks)); }); res.on("error", reject); }, ); req.on("timeout", () => { req.destroy(); reject(new Error("Download timeout")); }); req.on("error", reject); if (options?.body && (method === "POST" || method === "PUT")) { req.write(options.body); } req.end(); }); }