Files
admin 0593ff7e62
nginx serve /uploads direct (audio speed) / patch-nginx (push) Failing after 1m34s
数字我 Digital Me:全量入库(大文件走 Git LFS)
2026-10-10 16:24:43 +08:00

125 lines
5.8 KiB
JavaScript
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.
// 存量媒体迁移 → MinIO(老板 2026-08-05:文件存储改用 MinIO)。
//
// 把库里指向【本地 /uploads】(可选:也含【阿里云 OSS】)的媒体 URL,下载字节 → 传到 MinIO → 回写库里的 URL。
// 深度遍历每条文档的所有字符串字段(自动覆盖 media/cover/videoUrl/bundle.* 等嵌套,无需逐字段列举)。
//
// 前提:
// ① 在【服务器后端目录】跑(本地 /uploads 文件在这台机上才读得到)。
// ② .env 已配好 MINIO_*(minioConfigured 必须为真,否则拒绝运行)。
//
// 用法:
// node scripts/migrate-media-to-minio.mjs # 【干跑】只统计不写库(强烈建议先跑这个)
// node scripts/migrate-media-to-minio.mjs --commit # 真迁移(下载→传 MinIO→回写库 URL)
// node scripts/migrate-media-to-minio.mjs --commit --include-oss # 连 OSS 上的也迁到 MinIO
// node scripts/migrate-media-to-minio.mjs --commit creations create_jobs # 只迁指定集合
//
// 幂等:MinIO key = 原文件名(basename,已带时间戳/随机段);重复跑同一文件覆盖同 key,库里 URL 已是 MinIO 的会跳过。
// 说明:只迁【http(s) 公开媒体 URL】;母带(存的是 {key,driver} 对象,非 URL)不在此列,如需迁移母带另说。
import 'dotenv/config';
import fs from 'node:fs/promises';
import path from 'node:path';
import { connectDb, getDb, closeDb } from '../src/db.js';
import { config } from '../src/config.js';
import { saveMedia, minioConfigured } from '../src/services/storage.js';
const COMMIT = process.argv.includes('--commit');
const INCLUDE_OSS = process.argv.includes('--include-oss');
const COLL_ARGS = process.argv.slice(2).filter((a) => !a.startsWith('--'));
// 默认扫这些含媒体 URL 的集合(可用命令行参数覆盖)。
const DEFAULT_COLLS = ['creations', 'create_jobs', 'users', 'twins', 'messages', 'memories', 'masters', 'social_messages', 'shares', 'knowledge'];
const COLLS = COLL_ARGS.length ? COLL_ARGS : DEFAULT_COLLS;
const localPrefix = `${config.publicUrl}/uploads/`;
const localDir = config.storage.localDir;
const minioBase = config.storage.minio.publicBase;
const minioBucket = config.storage.minio.bucket;
const ossBase = config.storage.oss.publicBase;
function isLocal(u) { return u.startsWith(localPrefix) || u.startsWith('/uploads/'); }
function isMinio(u) { return (minioBase && u.startsWith(minioBase)) || (minioBucket && u.includes(`/${minioBucket}/`)); }
function isOss(u) { return /\.aliyuncs\.com\//.test(u) || (ossBase && u.startsWith(ossBase)); }
// 只处理我们自己托管的公开媒体 URL;第三方外链、data: 一律不动。
function migratable(u) {
if (!/^(https?:)?\/\//.test(u) && !u.startsWith('/uploads/')) return false;
if (isMinio(u)) return false; // 已在 MinIO
return isLocal(u) || (INCLUDE_OSS && isOss(u));
}
async function bytesFor(u) {
if (isLocal(u)) {
const base = path.basename(u.split('?')[0]);
return fs.readFile(path.join(localDir, base)); // 本地盘直接读
}
const r = await fetch(u); // OSS/远程:HTTP 拉
if (!r.ok) throw new Error(`fetch ${r.status}`);
return Buffer.from(await r.arrayBuffer());
}
const cache = new Map(); // 旧 URL → 新 URL(同一文件只迁一次)
let migrated = 0, failed = 0;
async function migrateUrl(u) {
if (cache.has(u)) return cache.get(u);
try {
const buf = await bytesFor(u);
const base = path.basename(u.split('?')[0]) || 'file';
const newUrl = COMMIT ? await saveMedia(buf, base) : `MINIO:${base}`; // 干跑不真传
cache.set(u, newUrl);
migrated++;
if (migrated % 20 === 0) console.log(` …已处理 ${migrated} 个文件`);
return newUrl;
} catch (e) {
failed++;
console.warn(` ✗ 跳过 ${u} —— ${e.message}`);
cache.set(u, null);
return null;
}
}
// BSON 特殊类型(ObjectId/Date/Binary 等)不递归。
function isPlainContainer(o) {
return o && typeof o === 'object' && !o._bsontype && !(o instanceof Date) && !Buffer.isBuffer(o);
}
async function walk(node) {
let changed = false;
const entries = Array.isArray(node) ? node.map((v, i) => [i, v]) : Object.entries(node);
for (const [k, v] of entries) {
if (typeof v === 'string') {
if (migratable(v)) { const nu = await migrateUrl(v); if (nu) { node[k] = nu; changed = true; } }
} else if (isPlainContainer(v)) {
if (await walk(v)) changed = true;
}
}
return changed;
}
async function run() {
if (!minioConfigured) {
console.error('❌ 未配置 MinIO(MINIO_ENDPOINT/BUCKET/ACCESS_KEY/SECRET_KEY)。先在 .env 配好再跑。');
process.exit(1);
}
console.log(`模式: ${COMMIT ? '★ COMMIT(真写库)' : '干跑(只统计,不改库/不传文件)'} | 含OSS: ${INCLUDE_OSS ? '是' : '否'} | 集合: ${COLLS.join(', ')}`);
await connectDb();
const db = getDb();
let totalChanged = 0;
for (const coll of COLLS) {
let scanned = 0, changedDocs = 0;
try {
for await (const doc of db.collection(coll).find({})) {
scanned++;
if (await walk(doc)) {
changedDocs++;
if (COMMIT) await db.collection(coll).replaceOne({ _id: doc._id }, doc);
}
}
} catch (e) { console.warn(`[${coll}] 扫描出错(可能无此集合): ${e.message}`); continue; }
totalChanged += changedDocs;
console.log(`[${coll}] 扫描 ${scanned} 文档,需迁移 ${changedDocs} 条`);
}
console.log('──────────────────────────');
console.log(`唯一文件: ${cache.size} 迁移成功: ${migrated} 失败: ${failed} 受影响文档: ${totalChanged}`);
console.log(COMMIT ? '✅ 已写库。' : 'ℹ️ 干跑结束——加 --commit 才会真迁移。');
await closeDb();
process.exit(0);
}
run().catch((e) => { console.error('迁移失败:', e); process.exit(1); });