Files
dzc f55561c98d feat:上传到阿里云oss
feat:把以前的图片迁移到阿里云oss
2026-11-05 15:22:55 +08:00

89 lines
4.0 KiB
JavaScript

import { performance } from 'node:perf_hooks';
import { qualified, sha256 } from './core.mjs';
import { applyChanges } from './journal.mjs';
import { verifySourceBatch } from './source-batch.mjs';
/**
* Switch a verified batch in one caller-session REPEATABLE READ transaction.
* Source validation must be the first operation after BEGIN: it establishes
* the source range lock before the transaction's consistent-read snapshot.
*/
export async function switchFiles(db, config, items) {
if (!Array.isArray(items)) throw new TypeError('文件切换批次必须为数组');
const timings = { sourceMs: 0, writeMs: 0, statusMs: 0, commitMs: 0 };
if (!items.length) return timings;
const changes = items.flatMap(item => {
if (!Array.isArray(item?.files)) throw new TypeError('文件切换项必须包含 files 数组(无文件记录时使用空数组)');
return item.files.map(file => ({
schema: config.mysql.database, table: 'infra_file', primaryKey: 'id', pkValue: file.id,
before: {
config_id: file.config_id, path: file.path, url: file.url,
type: file.type, size: file.size, deleted: Buffer.from([0])
},
after: {
config_id: config.targetConfigId, path: item.key, url: item.url,
type: item.type, size: item.size
}
}));
});
const keys = items.map(item => sha256(`${item.configId}\0${item.path}`));
if (new Set(keys).size !== keys.length) throw new TypeError('文件切换批次不能包含重复源路径');
if (new Set(changes.map(change => String(change.pkValue))).size !== changes.length) {
throw new TypeError('文件切换批次不能包含重复文件记录');
}
const measure = async (name, action) => {
const started = performance.now();
try { return await action(); }
finally { timings[name] += performance.now() - started; }
};
await db.beginTransaction();
let committing = false;
try {
// Do not insert metadata lookups or other consistent reads before this.
await measure('sourceMs', () => verifySourceBatch(db, config, items));
await measure('writeMs', async () => {
// One source path may have many infra_file rows. Bound prepared-statement
// size while retaining the same source locks and transaction for all rows.
for (let offset = 0; offset < changes.length; offset += 100) {
await applyChanges(db, config.mysql.database, config.runId, changes.slice(offset, offset + 100));
}
});
await measure('statusMs', async () => {
const [result] = await db.execute(
`UPDATE ${qualified(config.mysql.database, 'infra_file_oss_migration_items')}
SET status='switched',error=NULL,updated_at=CURRENT_TIMESTAMP
WHERE run_id=? AND status='verified' AND item_key IN (${keys.map(() => '?').join(',')})`,
[config.runId, ...keys]);
if (result.affectedRows !== items.length) {
throw Object.assign(new Error('部分迁移项已变化或不再处于 verified 状态,必须回滚整批事务'), {
code: 'MIGRATION_CAS_CONFLICT'
});
}
if (result.warningStatus > 0) {
throw Object.assign(new Error('批量迁移状态更新产生数据库警告,必须回滚整批事务'), {
code: 'MIGRATION_SQL_WARNING'
});
}
});
committing = true;
await measure('commitMs', () => db.commit());
return timings;
} catch (error) {
try {
await db.rollback();
} catch (rollbackError) {
throw Object.assign(new Error('事务回滚失败,连接状态不确定,禁止降级重试', { cause: error }), {
code: 'MIGRATION_ROLLBACK_FAILED', fatal: true, noFallback: true, rollbackError
});
}
if (committing) {
// A failed COMMIT acknowledgement does not prove the server rolled back.
throw Object.assign(new Error('事务提交结果不确定,禁止降级重试,请重新核对迁移状态', { cause: error }), {
code: error.code || 'MIGRATION_COMMIT_FAILED', fatal: true, noFallback: true
});
}
throw error;
}
}