import { copyFile, mkdir, readdir, readFile, writeFile } from "node:fs/promises"; import path from "node:path"; const sourceRoot = process.argv[2]; const targetRoot = process.env.UPLOAD_DIR || process.argv[3]; if (!sourceRoot || !targetRoot) { throw new Error( "用法:UPLOAD_DIR=/data/koc/uploads npm run storage:import -- /path/to/r2-export", ); } const contentTypes = { ".avif": "image/avif", ".gif": "image/gif", ".jpeg": "image/jpeg", ".jpg": "image/jpeg", ".png": "image/png", ".webp": "image/webp", }; let copied = 0; async function walk(directory) { const entries = await readdir(directory, { withFileTypes: true }); for (const entry of entries) { const absolute = path.join(directory, entry.name); if (entry.isDirectory()) { await walk(absolute); continue; } if (entry.name.endsWith(".metadata.json")) continue; const relative = path.relative(sourceRoot, absolute); if (relative.startsWith("..") || path.isAbsolute(relative)) { throw new Error("导入目录越界"); } const destination = path.join(targetRoot, relative); await mkdir(path.dirname(destination), { recursive: true }); await copyFile(absolute, destination); const sourceMetadata = `${absolute}.metadata.json`; const metadataDestination = `${destination}.metadata.json`; let metadata; try { metadata = JSON.parse(await readFile(sourceMetadata, "utf8")); } catch { metadata = { contentType: contentTypes[path.extname(entry.name).toLowerCase()] || "application/octet-stream", customMetadata: { source: "r2-export" }, uploadedAt: new Date().toISOString(), }; } await writeFile(metadataDestination, JSON.stringify(metadata), "utf8"); copied += 1; } } await mkdir(targetRoot, { recursive: true }); await walk(sourceRoot); console.info(`imported objects: ${copied}`);