/** * # 脚本 补齐缺失的上传图片(从原站 dusuniot.com 拉取) * 背景:导入另抓的 html/meta 引用了 uploads 里的图片,但部分文件未同步到 * public/uploads(如 landing-pages 分类卡片 2026/11 目录)。本脚本: * 1. 扫库(contents.html/meta/excerpt、products 文本列、media.url)收集全部 uploads 引用路径; * 2. 本地缺失的文件从 https://www.dusuniot.com/wp-content/uploads/ 下载补齐; * 3. 输出成功/失败清单。可重复执行(幂等,跳过已存在文件)。 */ import { mkdirSync, existsSync, writeFileSync } from "node:fs"; import { join, dirname } from "node:path"; import { initDb } from "~/db"; import { db } from "~/db"; import { contents, products, media } from "~/db/schema"; const PUBLIC_ROOT = join(process.cwd(), "public", "uploads"); const ORIGIN = "https://www.dusuniot.com/wp-content/uploads/"; await initDb(); /** # 成员变量 引用路径去重集合(含 html/meta/excerpt/products/media 全部来源) **/ const paths = new Set(); const addFromText = (s: unknown) => { if (s == null || typeof s !== "string") return; for (const m of s.matchAll(/(?:https?:\/\/(?:www\.)?dusuniot\.com)?\/wp-content\/uploads\/([\w\-./%~()+!@^[\]!'*,]+?\.(?:png|jpe?g|webp|gif|svg|ico|mp4|pdf))/gi)) { paths.add(m[1]); } for (const m of s.matchAll(/\/uploads\/((?:\d{4}\/\d{2}\/)?[\w\-./%~()+!@^[\]!'*,]+?\.(?:png|jpe?g|webp|gif|svg|ico|mp4|pdf))/gi)) { paths.add(m[1]); } }; // contents:html/meta/excerpt for (const row of await db.select({ html: contents.html, meta: contents.meta, excerpt: contents.excerpt, seoTitle: contents.seoTitle }).from(contents)) { addFromText(row.html); addFromText(row.excerpt); addFromText(row.seoTitle); addFromText(row.meta); } // products:常见文本/jsonb 列 for (const row of await db.select().from(products)) { for (const v of Object.values(row as Record)) addFromText(v); } // media 表登记的 URL for (const row of await db.select({ url: media.url }).from(media)) addFromText(row.url); console.log("引用路径总数:", paths.size); let ok = 0; let okExisting = 0; const failed: string[] = []; for (const p of [...paths].sort()) { // 解码 WP URL 编码与 query const clean = p.split("?")[0]; const local = join(PUBLIC_ROOT, decodeURIComponent(clean)); if (existsSync(local)) { okExisting++; continue; } const url = ORIGIN + clean; try { const res = await fetch(url); if (!res.ok) { failed.push(`${res.status} ${clean}`); continue; } const buf = new Uint8Array(await res.arrayBuffer()); if (buf.byteLength < 100) { failed.push(`DOCSIZE ${clean}`); continue; } mkdirSync(dirname(local), { recursive: true }); writeFileSync(local, buf); ok++; } catch (e) { failed.push(`ERR ${clean}: ${(e as Error).message.slice(0, 80)}`); } } console.log(`已存在 ${okExisting},新下载 ${ok},失败 ${failed.length}`); for (const f of failed.slice(0, 40)) console.log(" fail:", f);