80 lines
3.0 KiB
TypeScript
80 lines
3.0 KiB
TypeScript
/**
|
||
* # 脚本 补齐缺失的上传图片(从原站 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);
|