assets.js 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. "use strict";
  2. var __importDefault = (this && this.__importDefault) || function (mod) {
  3. return (mod && mod.__esModule) ? mod : { "default": mod };
  4. };
  5. Object.defineProperty(exports, "__esModule", { value: true });
  6. exports.assetsRouter = assetsRouter;
  7. const express_1 = require("express");
  8. const multer_1 = __importDefault(require("multer"));
  9. const adm_zip_1 = __importDefault(require("adm-zip"));
  10. const path_1 = __importDefault(require("path"));
  11. const fs_1 = __importDefault(require("fs"));
  12. const storageService_1 = require("../services/storageService");
  13. const upload = (0, multer_1.default)({
  14. storage: multer_1.default.memoryStorage(),
  15. limits: { fileSize: 50 * 1024 * 1024 },
  16. fileFilter: (_req, file, cb) => {
  17. if (file.mimetype === "application/zip" ||
  18. file.mimetype === "application/x-zip-compressed" ||
  19. file.originalname.endsWith(".zip")) {
  20. cb(null, true);
  21. }
  22. else {
  23. cb(new Error("Only .zip files are allowed"));
  24. }
  25. },
  26. });
  27. /**
  28. * 从 zip buffer 中提取素材文件、校验、写入磁盘、更新 DB。
  29. * 文件上传和 URL 导入共用此逻辑。
  30. */
  31. function extractAndSave(zipBuffer, manifest, assetsDir, db, creativeId) {
  32. const requiredAssets = manifest.assets?.required ?? [];
  33. const optionalAssets = manifest.assets?.optional ?? [];
  34. const allAssetDefs = [...requiredAssets, ...optionalAssets];
  35. // 清理并重建素材目录
  36. if (fs_1.default.existsSync(assetsDir)) {
  37. fs_1.default.rmSync(assetsDir, { recursive: true, force: true });
  38. }
  39. fs_1.default.mkdirSync(assetsDir, { recursive: true });
  40. const zip = new adm_zip_1.default(zipBuffer);
  41. const zipEntries = zip.getEntries();
  42. const extractedFiles = [];
  43. const warnings = [];
  44. // 清空旧素材记录
  45. db.prepare("DELETE FROM creative_assets WHERE creative_id = ?").run(creativeId);
  46. // 匹配并解压文件
  47. for (const def of allAssetDefs) {
  48. const entry = zipEntries.find((e) => {
  49. const entryName = path_1.default.basename(e.entryName).toLowerCase();
  50. const expectedName = def.file.toLowerCase();
  51. return entryName === expectedName;
  52. });
  53. if (entry) {
  54. const fileName = def.file;
  55. const filePath = path_1.default.join(assetsDir, fileName);
  56. fs_1.default.writeFileSync(filePath, entry.getData());
  57. const stat = fs_1.default.statSync(filePath);
  58. const isRequired = requiredAssets.some((r) => r.key === def.key);
  59. 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);
  60. extractedFiles.push({ key: def.key, fileName, fileSize: stat.size, valid: true });
  61. }
  62. else if (requiredAssets.some((r) => r.key === def.key)) {
  63. warnings.push(`Required file '${def.file}' is missing from uploaded zip`);
  64. extractedFiles.push({ key: def.key, fileName: def.file, fileSize: 0, valid: false });
  65. }
  66. }
  67. // 检查未知文件
  68. for (const entry of zipEntries) {
  69. if (entry.isDirectory)
  70. continue;
  71. const entryName = path_1.default.basename(entry.entryName).toLowerCase();
  72. const known = allAssetDefs.some((d) => d.file.toLowerCase() === entryName);
  73. if (!known) {
  74. warnings.push(`Unknown file '${entry.entryName}' ignored`);
  75. }
  76. }
  77. return { files: extractedFiles, warnings };
  78. }
  79. function assetsRouter(db, storageDir) {
  80. const router = (0, express_1.Router)();
  81. // POST /api/v1/creatives/:id/assets/upload
  82. // 支持两种方式:
  83. // - multipart/form-data 上传 .zip 文件(file 字段)
  84. // - application/json 提供素材 URL({ url: "https://..." })
  85. router.post("/creatives/:id/assets/upload", upload.single("file"), async (req, res) => {
  86. try {
  87. const creativeId = req.params.id;
  88. // 校验创意存在
  89. const creative = db
  90. .prepare("SELECT c.*, t.manifest FROM creatives c JOIN templates t ON c.template_id = t.id WHERE c.id = ?")
  91. .get(creativeId);
  92. if (!creative) {
  93. res.status(404).json({ error: { message: "Creative not found" } });
  94. return;
  95. }
  96. const manifest = JSON.parse(creative.manifest);
  97. const assetsDir = path_1.default.join(storageDir, "creatives", creativeId, "assets");
  98. let zipBuffer = null;
  99. // 方式 1:文件上传
  100. if (req.file) {
  101. zipBuffer = req.file.buffer;
  102. }
  103. // 方式 2:URL 导入
  104. else if (req.body?.url) {
  105. const parsed = (0, storageService_1.parseDetailUrl)(req.body.url);
  106. if (!parsed) {
  107. res.status(400).json({
  108. error: { message: "无法解析素材 URL,请确认格式正确" },
  109. });
  110. return;
  111. }
  112. console.log(`[assets] Downloading encrypted zip: ${parsed.zipUrl}`);
  113. const encrypted = await (0, storageService_1.downloadFile)(parsed.zipUrl);
  114. console.log(`[assets] Decrypting with key: ${parsed.id}`);
  115. zipBuffer = (0, storageService_1.xorDecryptBuffer)(encrypted, parsed.id);
  116. }
  117. else {
  118. res.status(400).json({
  119. error: { message: "请上传素材 zip 文件或提供素材 URL" },
  120. });
  121. return;
  122. }
  123. // 共用解压 & 校验逻辑
  124. const { files, warnings } = extractAndSave(zipBuffer, manifest, assetsDir, db, creativeId);
  125. // 更新创意状态
  126. const missingRequired = files.some((f) => !f.valid);
  127. db.prepare("UPDATE creatives SET status = ?, updated_at = datetime('now') WHERE id = ?").run(missingRequired ? "draft" : "assets_ready", creativeId);
  128. res.json({ data: { files, warnings } });
  129. }
  130. catch (err) {
  131. console.error("[assets] Upload error:", err.message);
  132. res.status(500).json({ error: { message: err.message } });
  133. }
  134. });
  135. // GET /api/v1/creatives/:id/assets/:key — 获取单个素材文件(用于缩略图展示)
  136. router.get("/creatives/:id/assets/:key", (req, res) => {
  137. const creativeId = req.params.id;
  138. const fileKey = req.params.key;
  139. const asset = db
  140. .prepare("SELECT file_path, file_name FROM creative_assets WHERE creative_id = ? AND file_key = ?")
  141. .get(creativeId, fileKey);
  142. if (!asset) {
  143. res.status(404).json({ error: { message: "Asset not found" } });
  144. return;
  145. }
  146. if (!fs_1.default.existsSync(asset.file_path)) {
  147. res.status(404).json({ error: { message: "File not found on disk" } });
  148. return;
  149. }
  150. const ext = path_1.default.extname(asset.file_name).toLowerCase();
  151. const mimeTypes = {
  152. ".png": "image/png",
  153. ".jpg": "image/jpeg",
  154. ".jpeg": "image/jpeg",
  155. ".gif": "image/gif",
  156. ".webp": "image/webp",
  157. ".svg": "image/svg+xml",
  158. };
  159. const contentType = mimeTypes[ext] || "application/octet-stream";
  160. res.setHeader("Content-Type", contentType);
  161. res.setHeader("Cache-Control", "public, max-age=3600");
  162. fs_1.default.createReadStream(asset.file_path).pipe(res);
  163. });
  164. // DELETE /api/v1/creatives/:id/assets — 清除素材
  165. router.delete("/creatives/:id/assets", (req, res) => {
  166. const creativeId = req.params.id;
  167. const assetsDir = path_1.default.join(storageDir, "creatives", creativeId, "assets");
  168. if (fs_1.default.existsSync(assetsDir)) {
  169. fs_1.default.rmSync(assetsDir, { recursive: true, force: true });
  170. }
  171. db.prepare("DELETE FROM creative_assets WHERE creative_id = ?").run(creativeId);
  172. db.prepare("UPDATE creatives SET status = 'draft', updated_at = datetime('now') WHERE id = ?").run(creativeId);
  173. res.json({ data: { id: creativeId, cleared: true } });
  174. });
  175. return router;
  176. }
  177. //# sourceMappingURL=assets.js.map