Files
dusuniot/scripts/sync-missing-uploads.ts

80 lines
3.0 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* # 脚本 补齐缺失的上传图片(从原站 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/<path> 下载补齐;
* 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<string>();
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<string, unknown>)) 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);