const functions = require("firebase-functions"); const admin = require("firebase-admin"); const archiver = require("archiver"); const fs = require("fs"); const os = require("os"); const path = require("path"); admin.initializeApp(); const bucket = admin.storage().bucket(); exports.generateGroupZip = functions.https.onRequest(async (req, res) => { try { const groupId = req.body.unique_url; // required const folder_prefix = req.body.prefix;// required const zip_prefix = req.body.zip_file_prefix;// required if (!groupId) { return res.status(400).json({ error: "unique_url required" }); } if (!folder_prefix) { return res.status(400).json({ error: "prefix required" }); } if (!zip_prefix) { return res.status(400).json({ error: "zip_file_prefix required" }); } const uniqueId = Date.now() + "_" + Math.floor(Math.random() * 10000); // ✅ SAFE: prefix controlled only by server const prefix = `${folder_prefix}${groupId}/`; // STEP 1: LIST ALL FILES IN FOLDER const [files] = await bucket.getFiles({ prefix: prefix }); if (!files.length) { return res.status(404).json({ error: "No files found" }); } // STEP 2: CREATE ZIP const zipFileName = `${zip_prefix}${groupId}_${uniqueId}.zip`; const tempZipPath = path.join(os.tmpdir(), `${uniqueId}.zip`); const output = fs.createWriteStream(tempZipPath); const archive = archiver("zip", { zlib: { level: 9 } }); archive.pipe(output); // collect temp files for cleanup const tempFiles = []; for (const file of files) { if (!file.name || file.name.endsWith("/")) continue; const tempFilePath = path.join( os.tmpdir(), `${Date.now()}_${path.basename(file.name)}` ); await file.download({ destination: tempFilePath }); tempFiles.push(tempFilePath); archive.file(tempFilePath, { name: file.name.replace(`${folder_prefix}${groupId}/`, "") }); } await archive.finalize(); await new Promise(resolve => output.on("close", resolve)); // STEP 3: UPLOAD ZIP await bucket.upload(tempZipPath, { destination: zipFileName, metadata: { contentType: "application/zip" } }); // STEP 4: GENERATE SIGNED URL const file = bucket.file(zipFileName); // STEP 5: CLEAN TEMP FILES (IMPORTANT) try { fs.unlinkSync(tempZipPath); tempFiles.forEach(f => { if (fs.existsSync(f)) fs.unlinkSync(f); }); } catch (e) { console.log("Cleanup error:", e.message); } return res.json({ success: true, unique_id: uniqueId, zip_path: zipFileName }); } catch (err) { console.error(err); return res.status(500).json({ success: false, error: err.message }); } });