133 lines
5.3 KiB
JavaScript
133 lines
5.3 KiB
JavaScript
import 'reflect-metadata';
|
|
import { createHash, randomUUID } from 'node:crypto';
|
|
import { readFile } from 'node:fs/promises';
|
|
import { createRequire } from 'node:module';
|
|
import { parseArgs, parseEnv } from 'node:util';
|
|
import {
|
|
loadConfig,
|
|
validateApplyConfig,
|
|
} from '../../../packages/asset-sync/src/config.mjs';
|
|
import { readSource } from '../../../packages/asset-sync/src/files.mjs';
|
|
import { createQiniuClient } from '../../../packages/asset-sync/src/qiniu-client.mjs';
|
|
import { buildAssetMappings } from './asset-mapping.mjs';
|
|
|
|
const require = createRequire(import.meta.url);
|
|
const { PrismaService } = require('../dist/database/prisma.service.js');
|
|
const { getServerConfig } = require('../dist/config/server-config.js');
|
|
let database;
|
|
try {
|
|
const { values } = parseArgs({
|
|
options: {
|
|
report: { type: 'string' },
|
|
apply: { type: 'boolean', default: false },
|
|
},
|
|
});
|
|
if (!values.report) throw new Error('REPORT_REQUIRED');
|
|
const local = parseEnv(
|
|
await readFile(new URL('../.env', import.meta.url), 'utf8'),
|
|
);
|
|
const server = getServerConfig({ ...local, ...process.env });
|
|
const url = new URL(server.databaseUrl);
|
|
// 本次数据迁移仅授权本机开发库;部署环境需独立迁移流程。
|
|
if (
|
|
!['localhost', '127.0.0.1'].includes(url.hostname) ||
|
|
url.pathname !== '/doodle_dev'
|
|
)
|
|
throw new Error('LOCAL_DATABASE_REQUIRED');
|
|
const config = await loadConfig();
|
|
if (
|
|
config.publicBucket !== server.qiniuBucket ||
|
|
config.prefix !== server.qiniuKeyPrefix ||
|
|
config.publicDomain !== server.qiniuPublicDomain
|
|
)
|
|
throw new Error('SERVER_SYNC_CONFIG_MISMATCH');
|
|
const raw = await readFile(values.report, 'utf8');
|
|
database = new PrismaService(server);
|
|
const assets = await database.client.asset.findMany();
|
|
const mappings = buildAssetMappings(JSON.parse(raw), assets, config);
|
|
validateApplyConfig(
|
|
config,
|
|
mappings.map((item) => item.file),
|
|
);
|
|
const client = createQiniuClient(config);
|
|
if ((await client.bucketInfo(config.publicBucket)).private !== 0)
|
|
throw new Error('BUCKET_VISIBILITY_MISMATCH');
|
|
// 数据库事务外完成全部本地和远端核对,不将网络等待放进事务。
|
|
for (const { file } of mappings) {
|
|
const source = await readSource(config, file.relativePath);
|
|
const remote = await client.stat(file.bucket, file.key);
|
|
if (
|
|
source.sha256 !== file.sha256 ||
|
|
source.etag !== file.etag ||
|
|
source.size !== file.size ||
|
|
remote?.hash !== file.etag ||
|
|
remote.fsize !== file.size ||
|
|
remote.mimeType !== file.mimeType
|
|
)
|
|
throw new Error('ASSET_VERIFICATION_FAILED');
|
|
}
|
|
const requestId = `asset-mapping-${randomUUID()}`;
|
|
if (values.apply) {
|
|
await database.client.$transaction(
|
|
async (tx) => {
|
|
for (const { asset, data, unchanged } of mappings) {
|
|
if (unchanged) continue;
|
|
// 乐观并发检查与整批事务保证不会覆盖回填期间的其他编辑。
|
|
const result = await tx.asset.updateMany({
|
|
where: {
|
|
id: asset.id,
|
|
updatedAt: asset.updatedAt,
|
|
status: asset.status,
|
|
},
|
|
data,
|
|
});
|
|
if (result.count !== 1)
|
|
throw new Error('CONCURRENT_ASSET_CHANGE');
|
|
await tx.adminAuditLog.create({
|
|
data: {
|
|
id: randomUUID(),
|
|
actorType: 'migration',
|
|
action: 'asset.map-qiniu',
|
|
resourceType: 'assets',
|
|
resourceId: asset.id,
|
|
requestId,
|
|
beforeData: {
|
|
status: asset.status,
|
|
bucket: asset.bucket,
|
|
objectKey: asset.objectKey,
|
|
},
|
|
afterData: {
|
|
status: data.status,
|
|
bucket: data.bucket,
|
|
objectKey: data.objectKey,
|
|
checksum: data.checksum,
|
|
},
|
|
reason: `sync-report sha256=${createHash('sha256').update(raw).digest('hex')}`,
|
|
},
|
|
});
|
|
}
|
|
},
|
|
{ timeout: 30000 },
|
|
);
|
|
}
|
|
console.log(
|
|
JSON.stringify({
|
|
dryRun: !values.apply,
|
|
requestId,
|
|
matched: mappings.length,
|
|
updated: mappings.filter((item) => !item.unchanged).length,
|
|
unchanged: mappings.filter((item) => item.unchanged).length,
|
|
}),
|
|
);
|
|
} catch (error) {
|
|
// 不输出数据库连接、SDK 原始异常或密钥;错误保留有限自定义代码。
|
|
console.error(
|
|
/^[A-Z_]+$/.test(error.message ?? '')
|
|
? error.message
|
|
: 'ASSET_MAPPING_FAILED',
|
|
);
|
|
process.exitCode = 1;
|
|
} finally {
|
|
await database?.onModuleDestroy();
|
|
}
|