"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.assetsRouter = assetsRouter; const express_1 = require("express"); const multer_1 = __importDefault(require("multer")); const adm_zip_1 = __importDefault(require("adm-zip")); const path_1 = __importDefault(require("path")); const fs_1 = __importDefault(require("fs")); const storageService_1 = require("../services/storageService"); const upload = (0, multer_1.default)({ storage: multer_1.default.memoryStorage(), limits: { fileSize: 50 * 1024 * 1024 }, fileFilter: (_req, file, cb) => { if (file.mimetype === "application/zip" || file.mimetype === "application/x-zip-compressed" || file.originalname.endsWith(".zip")) { cb(null, true); } else { cb(new Error("Only .zip files are allowed")); } }, }); // 单图片上传(logo/slogon 等手动替换) const uploadImage = (0, multer_1.default)({ storage: multer_1.default.memoryStorage(), limits: { fileSize: 5 * 1024 * 1024 }, fileFilter: (_req, file, cb) => { if (file.mimetype.startsWith("image/")) { cb(null, true); } else { cb(new Error("Only image files are allowed")); } }, }); /** * 从 zip buffer 中提取素材文件、校验、写入磁盘、更新 DB。 * 文件上传和 URL 导入共用此逻辑。 */ function extractAndSave(zipBuffer, manifest, assetsDir, db, creativeId) { const requiredAssets = manifest.assets?.required ?? []; const optionalAssets = manifest.assets?.optional ?? []; const allAssetDefs = [...requiredAssets, ...optionalAssets]; // 清理并重建素材目录 if (fs_1.default.existsSync(assetsDir)) { fs_1.default.rmSync(assetsDir, { recursive: true, force: true }); } fs_1.default.mkdirSync(assetsDir, { recursive: true }); const zip = new adm_zip_1.default(zipBuffer); const zipEntries = zip.getEntries(); const extractedFiles = []; const warnings = []; // 清空旧素材记录 db.prepare("DELETE FROM creative_assets WHERE creative_id = ?").run(creativeId); // 匹配并解压文件 for (const def of allAssetDefs) { const entry = zipEntries.find((e) => { const entryName = path_1.default.basename(e.entryName).toLowerCase(); const expectedName = def.file.toLowerCase(); return entryName === expectedName; }); if (entry) { const fileName = def.file; const filePath = path_1.default.join(assetsDir, fileName); fs_1.default.writeFileSync(filePath, entry.getData()); const stat = fs_1.default.statSync(filePath); const isRequired = requiredAssets.some((r) => r.key === def.key); db.prepare("INSERT INTO creative_assets (creative_id, file_key, file_name, file_path, file_size, is_required) VALUES (?, ?, ?, ?, ?, ?)").run(creativeId, def.key, fileName, filePath, stat.size, isRequired ? 1 : 0); extractedFiles.push({ key: def.key, fileName, fileSize: stat.size, valid: true }); } else if (requiredAssets.some((r) => r.key === def.key)) { warnings.push(`Required file '${def.file}' is missing from uploaded zip`); extractedFiles.push({ key: def.key, fileName: def.file, fileSize: 0, valid: false }); } } // 检查未知文件 for (const entry of zipEntries) { if (entry.isDirectory) continue; const entryName = path_1.default.basename(entry.entryName).toLowerCase(); const known = allAssetDefs.some((d) => d.file.toLowerCase() === entryName); if (!known) { warnings.push(`Unknown file '${entry.entryName}' ignored`); } } return { files: extractedFiles, warnings }; } function assetsRouter(db, storageDir) { const router = (0, express_1.Router)(); // POST /api/v1/creatives/:id/assets/upload // 支持两种方式: // - multipart/form-data 上传 .zip 文件(file 字段) // - application/json 提供素材 URL({ url: "https://..." }) router.post("/creatives/:id/assets/upload", upload.single("file"), async (req, res) => { try { const creativeId = req.params.id; // 校验创意存在 const creative = db .prepare("SELECT c.*, t.manifest FROM creatives c JOIN templates t ON c.template_id = t.id WHERE c.id = ?") .get(creativeId); if (!creative) { res.status(404).json({ error: { message: "Creative not found" } }); return; } const manifest = JSON.parse(creative.manifest); const assetsDir = path_1.default.join(storageDir, "creatives", creativeId, "assets"); let zipBuffer = null; // 方式 1:文件上传 if (req.file) { zipBuffer = req.file.buffer; } // 方式 2:URL 导入 else if (req.body?.url) { const parsed = (0, storageService_1.parseDetailUrl)(req.body.url); if (!parsed) { res.status(400).json({ error: { message: "无法解析素材 URL,请确认格式正确" }, }); return; } console.log(`[assets] Downloading encrypted zip: ${parsed.zipUrl}`); const encrypted = await (0, storageService_1.downloadFile)(parsed.zipUrl); console.log(`[assets] Decrypting with key: ${parsed.id}`); zipBuffer = (0, storageService_1.xorDecryptBuffer)(encrypted, parsed.id); } else { res.status(400).json({ error: { message: "请上传素材 zip 文件或提供素材 URL" }, }); return; } // 共用解压 & 校验逻辑 const { files, warnings } = extractAndSave(zipBuffer, manifest, assetsDir, db, creativeId); // 更新创意状态 const missingRequired = files.some((f) => !f.valid); db.prepare("UPDATE creatives SET status = ?, updated_at = datetime('now') WHERE id = ?").run(missingRequired ? "draft" : "assets_ready", creativeId); res.json({ data: { files, warnings } }); } catch (err) { console.error("[assets] Upload error:", err.message); res.status(500).json({ error: { message: err.message } }); } }); // GET /api/v1/creatives/:id/assets/:key — 获取单个素材文件(用于缩略图展示) router.get("/creatives/:id/assets/:key", (req, res) => { const creativeId = req.params.id; const fileKey = req.params.key; // 1. 先查用户上传的素材 const asset = db .prepare("SELECT file_path, file_name FROM creative_assets WHERE creative_id = ? AND file_key = ?") .get(creativeId, fileKey); let filePath; let fileName; if (asset && fs_1.default.existsSync(asset.file_path)) { filePath = asset.file_path; fileName = asset.file_name; } else { // 2. 未上传 → 尝试回退到模板默认图 const TEMPLATE_IMG_DIR = path_1.default.resolve(__dirname, "../../../../templates/coloring/assets/img"); const defaultMap = { logo: "logo.png", logoTxt: "logo-txt.png", slogon: "slogon.png", coloringPages: "coloring-pages.png", }; const defaultFile = defaultMap[fileKey]; if (!defaultFile) { res.status(404).json({ error: { message: "Asset not found" } }); return; } filePath = path_1.default.join(TEMPLATE_IMG_DIR, defaultFile); fileName = defaultFile; if (!fs_1.default.existsSync(filePath)) { res.status(404).json({ error: { message: "Default asset not found" } }); return; } } const ext = path_1.default.extname(fileName).toLowerCase(); const mimeTypes = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp", ".svg": "image/svg+xml", }; const contentType = mimeTypes[ext] || "application/octet-stream"; res.setHeader("Content-Type", contentType); res.setHeader("Cache-Control", "public, max-age=3600"); fs_1.default.createReadStream(filePath).pipe(res); }); // POST /api/v1/creatives/:id/assets/:key — 上传单个素材文件(手动替换) router.post("/creatives/:id/assets/:key", uploadImage.single("file"), (req, res) => { try { const creativeId = req.params.id; const fileKey = req.params.key; const creative = db .prepare("SELECT c.*, t.manifest FROM creatives c JOIN templates t ON c.template_id = t.id WHERE c.id = ?") .get(creativeId); if (!creative) { res.status(404).json({ error: { message: "Creative not found" } }); return; } const manifest = JSON.parse(creative.manifest); const requiredAssets = manifest.assets?.required ?? []; const optionalAssets = manifest.assets?.optional ?? []; const def = [...requiredAssets, ...optionalAssets].find((d) => d.key === fileKey); if (!def) { res.status(400).json({ error: { message: `Unknown asset key: ${fileKey}` } }); return; } if (!req.file) { res.status(400).json({ error: { message: "请选择文件" } }); return; } const assetsDir = path_1.default.join(storageDir, "creatives", creativeId, "assets"); if (!fs_1.default.existsSync(assetsDir)) fs_1.default.mkdirSync(assetsDir, { recursive: true }); // 删除同 key 的旧文件 const oldAsset = db.prepare("SELECT file_path FROM creative_assets WHERE creative_id = ? AND file_key = ?").get(creativeId, fileKey); if (oldAsset && fs_1.default.existsSync(oldAsset.file_path)) fs_1.default.unlinkSync(oldAsset.file_path); const filePath = path_1.default.join(assetsDir, def.file); fs_1.default.writeFileSync(filePath, req.file.buffer); const stat = fs_1.default.statSync(filePath); db.prepare("DELETE FROM creative_assets WHERE creative_id = ? AND file_key = ?").run(creativeId, fileKey); db.prepare("INSERT INTO creative_assets (creative_id, file_key, file_name, file_path, file_size, is_required) VALUES (?, ?, ?, ?, ?, ?)").run(creativeId, fileKey, def.file, filePath, stat.size, requiredAssets.some((r) => r.key === fileKey) ? 1 : 0); console.log(`[assets] Single upload: ${fileKey} -> ${filePath} (${stat.size} bytes)`); res.json({ data: { key: fileKey, fileName: def.file, fileSize: stat.size, valid: true } }); } catch (err) { console.error("[assets] Single upload error:", err.message); res.status(500).json({ error: { message: err.message } }); } }); // DELETE /api/v1/creatives/:id/assets — 清除素材 router.delete("/creatives/:id/assets", (req, res) => { const creativeId = req.params.id; const assetsDir = path_1.default.join(storageDir, "creatives", creativeId, "assets"); if (fs_1.default.existsSync(assetsDir)) { fs_1.default.rmSync(assetsDir, { recursive: true, force: true }); } db.prepare("DELETE FROM creative_assets WHERE creative_id = ?").run(creativeId); db.prepare("UPDATE creatives SET status = 'draft', updated_at = datetime('now') WHERE id = ?").run(creativeId); res.json({ data: { id: creativeId, cleared: true } }); }); return router; } //# sourceMappingURL=assets.js.map