const express = require('express'); const bodyParser = require('body-parser'); const axios = require('axios'); const { execSync } = require('child_process'); const path = require('path'); const fs = require('fs'); const app = express(); app.use(bodyParser.json({ limit: '100mb' })); // File Dummy lokal sebagai cadangan jika dummy_url tidak dikirim const DUMMY_ZIP = 'dummy_lite.zepeto.zip'; if (!fs.existsSync('uploads')) fs.mkdirSync('uploads', { recursive: true }); function getTS() { return new Date().toLocaleTimeString(); } // Fungsi sakti untuk mencari file/folder di kedalaman mana pun function findPath(base, targetName, isFolder = false) { if (!fs.existsSync(base)) return null; const items = fs.readdirSync(base); for (const item of items) { const fullPath = path.join(base, item); const stat = fs.lstatSync(fullPath); if (isFolder && stat.isDirectory() && item.toLowerCase() === targetName.toLowerCase()) return fullPath; if (!isFolder && stat.isFile() && item.toLowerCase().endsWith(targetName.toLowerCase())) return fullPath; if (stat.isDirectory()) { const found = findPath(fullPath, targetName, isFolder); if (found) return found; } } return null; } app.post('/convert', async (req, res) => { const targetFilename = req.body.filename || 'result.zepeto'; // KUNCI UTAMA: dummy_url wajib dari draft yang sama agar KTP GUID cocok! const hpUrl = req.body.file_url; const dummyUrl = req.body.dummy_url; console.log(`\n[${getTS()}] 🔥 REQUEST MASUK: Eksekusi ICU Clone (Native Packing) -> ${targetFilename}`); const timeId = Date.now(); const tempHP = path.join(__dirname, 'uploads', `hp_${timeId}.zepeto`); const tempDummy = path.join(__dirname, 'uploads', `dummy_${timeId}.zepeto`); const extHP = path.join(__dirname, 'uploads', `ext_hp_${timeId}`); const extDummy = path.join(__dirname, 'uploads', `ext_dummy_${timeId}`); const stagePath = path.join(__dirname, 'uploads', `stage_${timeId}`); const finalOut = path.join(__dirname, 'uploads', `final_${timeId}.zepeto`); try { console.log(`[${getTS()}] [1] Mendownload Baju Raksasa...`); const resHP = await axios({ method: 'GET', url: hpUrl, responseType: 'stream' }); const writerHP = fs.createWriteStream(tempHP); resHP.data.pipe(writerHP); await new Promise(resolve => writerHP.on('finish', resolve)); console.log(`[${getTS()}] [2] Menyiapkan File Dummy...`); if (dummyUrl) { const resDummy = await axios({ method: 'GET', url: dummyUrl, responseType: 'stream' }); const writerDummy = fs.createWriteStream(tempDummy); resDummy.data.pipe(writerDummy); await new Promise(resolve => writerDummy.on('finish', resolve)); } else { // Gunakan dummy lokal jika tidak dikirim dari CURL fs.copyFileSync(DUMMY_ZIP, tempDummy); } console.log(`[${getTS()}] [3] Mengekstrak File...`); fs.mkdirSync(extHP, { recursive: true }); fs.mkdirSync(extDummy, { recursive: true }); fs.mkdirSync(stagePath, { recursive: true }); // Ruang Operasi Bersih execSync(`unzip -q -o "${tempHP}" -d "${extHP}"`); execSync(`unzip -q -o "${tempDummy}" -d "${extDummy}"`); console.log(`[${getTS()}] [4] Eksekusi Transplantasi Murni...`); // 1. AMBIL KTP & BLUEPRINT DARI DUMMY const dummyMetaPath = findPath(extDummy, '.prefab.meta'); const dummyPrefabPath = findPath(extDummy, '.prefab'); if (!dummyMetaPath || !dummyPrefabPath) throw new Error("File .prefab/.meta Dummy hilang!"); fs.copyFileSync(dummyPrefabPath, path.join(stagePath, 'untitled.prefab')); fs.copyFileSync(dummyMetaPath, path.join(stagePath, 'untitled.prefab.meta')); console.log(`[${getTS()}] -> Kertas Blueprint & KTP Dummy terpasang bersih.`); // 2. AMBIL BRANKAS BINER DARI RAKSASA const hpBundleDir = findPath(extHP, 'assetbundles', true); if (!hpBundleDir) throw new Error("Folder assetbundles Raksasa tidak ditemukan!"); const stageBundles = path.join(stagePath, 'assetbundles'); fs.mkdirSync(stageBundles, { recursive: true }); let fileCount = 0; fs.readdirSync(hpBundleDir).forEach(p => { const src = path.join(hpBundleDir, p); if (fs.lstatSync(src).isFile()) { fs.copyFileSync(src, path.join(stageBundles, p)); fileCount++; } }); console.log(`[${getTS()}] -> ${fileCount} File Biner Raksasa berhasil diselundupkan utuh.`); // 3. PEMBUNGKUSAN NATIVE LINUX (SOLUSI MUTLAK) console.log(`[${getTS()}] [5] Membungkus dengan Algoritma ZIP Native (Level 0 / Store)...`); // Memastikan sistem Linux memiliki package zip terinstal try { execSync('zip -v', { stdio: 'ignore' }); } catch (e) { console.log(`[${getTS()}] -> Menginstal library zip native...`); execSync('apt-get update && apt-get install -y zip', { stdio: 'ignore' }); } // Mengeksekusi perintah packing asli sistem operasi! // -r = Rekursif ke dalam folder // -0 = STORE ONLY (Dilarang keras melakukan kompresi DEFLATE agar Zepeto tidak error) execSync(`cd "${stagePath}" && zip -r -0 "${finalOut}" ./*`); console.log(`[${getTS()}] ✅ SELESAI! Packing ZIP 100% Identik dengan Web ICU.`); res.download(finalOut, targetFilename, () => { try { fs.rmSync(extHP, { recursive: true, force: true }); fs.rmSync(extDummy, { recursive: true, force: true }); fs.rmSync(stagePath, { recursive: true, force: true }); fs.unlinkSync(tempHP); fs.unlinkSync(tempDummy); fs.unlinkSync(finalOut); } catch(e) {} }); } catch (err) { console.error(`[FATAL ERROR]`, err); res.status(500).json({ error: err.message }); } }); const PORT = 7860; app.listen(PORT, () => console.log(`[${getTS()}] 🚀 Server Native Packing (ICU Clone) Aktif di Port ${PORT}`));