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

173 lines
11 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { discoverStorageConfig } from '../lib/auto-storage.mjs';
const database = 'local_app';
const server = 'test-server-uuid';
const input = () => ({ mysql: { database, user: 'local-test', password: 'db-test-only' }, projectProfile: 'local', _projectDownloadBaseUrl: 'http://localhost:48080/admin-api/infra/file' });
const source = value => ({ id: value, storage: 1, master: Buffer.from([0]), config: JSON.stringify({ domain: 'https://old.example' }) });
const target = (value = '30', master = 1) => ({ id: value, storage: 20, master: Buffer.from([master]), config: JSON.stringify({ endpoint: 'oss-cn-hangzhou.aliyuncs.com', bucket: 'migration-test', accessKey: 'oss-test-access', accessSecret: 'oss-secret-must-not-escape', domain: 'https://media.example' }) });
function binding(overrides = {}) {
return { server, schema: database, sourceConfigIds: ['5'], targetConfigId: '30', sourceMode: 'database', downloadBaseUrl: 'https://old.example/admin-api/infra/file', oldDomains: ['https://historic.example'], objectPrefix: 'migration/original', imagesOnly: true, referenceSchemas: [database, 'chat'], excludeReferenceTables: ['audit'], targetUrlMode: 'direct', publicDomain: 'https://original-media.example', ...overrides };
}
const run = (runId = 'original-run', status = 'partial', overrides = {}) => ({ run_id: runId, status, settings: JSON.stringify({ binding: binding(overrides), maxId: '9007199254740993' }) });
class FakeDb {
constructor(options = {}) {
this.server = server;
this.configs = [source('5'), target()];
this.sourceIds = ['5'];
this.runs = [];
this.domains = ['https://old.example'];
this.maximum = '9007199254740993';
this.calls = [];
Object.assign(this, options);
}
async execute(sql, params = []) {
assert.match(sql.trim(), /^SELECT\b/i, 'discovery must only issue SELECT');
this.calls.push({ sql, params });
if (sql.includes('@@server_uuid')) return [[{ server_uuid: this.server }]];
if (sql.includes('information_schema.TABLES')) {
assert.deepEqual(params, [database, 'infra_file_oss_migration_runs']);
return [this.runs.length ? [{ present: 1 }] : []];
}
if (sql.includes('infra_file_oss_migration_runs')) return [this.runs];
if (sql.includes('infra_file_config')) return [this.configs];
if (sql.includes('MAX(id)')) return [[{ max_id: this.maximum }]];
if (sql.includes('infra_file_content')) {
assert.match(sql, /deleted=0/);
return [this.sourceIds.filter(value => params.includes(value)).map(config_id => ({ config_id }))];
}
if (sql.includes('SUBSTRING_INDEX')) return [this.domains.map(domain => ({ domain }))];
throw new Error(`Unexpected test SQL: ${sql}`);
}
}
test('discovers source BLOB configurations and the unique OSS default using SELECT only', async () => {
const db = new FakeDb({ configs: [source('9007199254740995'), target(), source('12'), source('2'), source('999')], sourceIds: ['12', '9007199254740995', '2', '2'] });
const result = await discoverStorageConfig(db, input());
assert.deepEqual(result.sourceConfigIds, ['2', '12', '9007199254740995']);
assert.equal(result.targetConfigId, '30');
assert.equal(result.sourceMode, 'database');
assert.equal(result.downloadBaseUrl, input()._projectDownloadBaseUrl);
assert.equal(result.projectProfile, 'local');
assert.equal(result._autoDiscovery, true);
assert.deepEqual(result._databaseIdentity, { server, schema: database });
assert.match(result.runId, /^db-images-[a-f0-9]{16}$/);
assert.equal(JSON.stringify(result).includes('oss-secret-must-not-escape'), false);
assert.ok(db.calls.some(call => call.sql.includes('SELECT DISTINCT config_id FROM')));
});
test('generated runId is independent of source ordering, credentials, domains and remaining file metadata', async () => {
const first = await discoverStorageConfig(new FakeDb({ configs: [source('7'), target(), source('5')], sourceIds: ['7', '5'] }), input());
const secondInput = input();
secondInput.mysql.password = 'rotated-password';
const second = await discoverStorageConfig(new FakeDb({ configs: [source('5'), source('7'), target()], sourceIds: ['5', '7'], domains: [] }), secondInput);
assert.equal(second.runId, first.runId);
assert.deepEqual(second.sourceConfigIds, first.sourceConfigIds);
});
test('only valid HTTP base domains without credentials are inferred and sorted', async () => {
const result = await discoverStorageConfig(new FakeDb({ domains: ['https://z.example/', 'https://a.example/prefix', 'https://z.example', 'https://user:secret@example.com', 'ftp://file.example', 'https://x.example?secret=1', 'not a URL'] }), input());
assert.deepEqual(result.oldDomains, ['https://a.example/prefix', 'https://z.example']);
});
test('requires a unique default and refuses non-Aliyun defaults instead of selecting a different OSS', async () => {
await assert.rejects(discoverStorageConfig(new FakeDb({ configs: [source('5'), target('30', 0)] }), input()), /默认.*唯一/);
await assert.rejects(discoverStorageConfig(new FakeDb({ configs: [source('5'), target(), target('31')] }), input()), /默认.*唯一/);
const invalidDefault = target();
invalidDefault.config = JSON.stringify({ endpoint: 'minio.example', accessSecret: 'dont-leak' });
await assert.rejects(discoverStorageConfig(new FakeDb({ configs: [source('5'), invalidDefault, target('31', 0)] }), input()), error => /阿里云/.test(error.message) && !error.message.includes('dont-leak'));
const malformedDefault = target();
malformedDefault.config = '{"secret":"dont-leak", broken';
await assert.rejects(discoverStorageConfig(new FakeDb({ configs: [source('5'), malformedDefault] }), input()), error => !error.message.includes('dont-leak'));
});
test('restores an unfinished run before current metadata can shrink its original scope', async () => {
const original = run('original-run', 'partial', { sourceConfigIds: ['7', '5'] });
const db = new FakeDb({ runs: [original], configs: [source('5'), source('7'), target()], sourceIds: [], domains: [] });
const result = await discoverStorageConfig(db, input(), { command: 'apply' });
assert.equal(result.runId, 'original-run');
assert.deepEqual(result.sourceConfigIds, ['7', '5'], 'historical order is part of the existing fingerprint');
assert.deepEqual(result.oldDomains, ['https://historic.example']);
assert.equal(result.objectPrefix, 'migration/original');
assert.equal(result.downloadBaseUrl, 'https://old.example/admin-api/infra/file');
assert.equal(result.targetPublicDomain, 'https://original-media.example');
assert.deepEqual(result.referenceSchemas, [database, 'chat']);
assert.ok(!db.calls.some(call => call.sql.includes('SELECT DISTINCT config_id')));
assert.ok(!db.calls.some(call => call.sql.includes('SUBSTRING_INDEX')));
});
test('explicit input fields override restored fields for downstream fingerprint validation', async () => {
const result = await discoverStorageConfig(new FakeDb({ runs: [run()] }), {
...input(), oldDomains: [], objectPrefix: 'migration/explicit', imagesOnly: false,
targetPublicDomain: 'https://explicit.example', sourceMode: 'http', downloadBaseUrl: 'https://explicit.example/admin-api/infra/file'
});
assert.deepEqual(result.oldDomains, []);
assert.equal(result.objectPrefix, 'migration/explicit');
assert.equal(result.imagesOnly, false);
assert.equal(result.targetPublicDomain, 'https://explicit.example');
assert.equal(result.sourceMode, 'http');
});
test('automatic resumed inputs and historical runs reject a changed default target', async () => {
const options = { configs: [source('5'), target('30', 0), target('31')] };
await assert.rejects(discoverStorageConfig(new FakeDb(options), { ...input(), targetConfigId: '30', _autoDiscovery: true }), /原迁移目标不同/);
await assert.rejects(discoverStorageConfig(new FakeDb({ ...options, runs: [run()] }), input()), /原迁移目标不同/);
});
test('rejects cross-server/schema inputs and restored history before accessing storage credentials', async () => {
for (const wrong of [{ server: 'other', schema: database }, { server, schema: 'other' }]) {
const db = new FakeDb();
await assert.rejects(discoverStorageConfig(db, { ...input(), _databaseIdentity: wrong }), /身份.*不一致/);
assert.equal(db.calls.length, 1);
}
const db = new FakeDb({ runs: [run('foreign', 'partial', { server: 'other' })] });
await assert.rejects(discoverStorageConfig(db, input()), /身份.*不一致/);
assert.ok(!db.calls.some(call => call.sql.includes('infra_file_config')));
});
test('historical selection refuses ambiguity but honors an explicit existing or new runId', async () => {
const unfinished = [run('one'), run('two')];
await assert.rejects(discoverStorageConfig(new FakeDb({ runs: unfinished }), input()), /多个未完成.*--run-id/);
const chosen = await discoverStorageConfig(new FakeDb({ runs: unfinished }), { ...input(), runId: 'two' });
assert.equal(chosen.runId, 'two');
const fresh = await discoverStorageConfig(new FakeDb({ runs: unfinished }), { ...input(), runId: 'new-run' });
assert.equal(fresh.runId, 'new-run');
assert.equal(fresh.objectPrefix, 'migration/db-images');
await assert.rejects(discoverStorageConfig(new FakeDb({ runs: [run('one', 'complete'), run('two', 'rolled_back')] }), input()), /多个历史.*--run-id/);
const single = await discoverStorageConfig(new FakeDb({ runs: [run('complete-run', 'complete')] }), input());
assert.equal(single.runId, 'complete-run');
const active = await discoverStorageConfig(new FakeDb({ runs: [run('old', 'complete'), run('active', 'needs-review')] }), input());
assert.equal(active.runId, 'active');
});
test('apply cannot resume a rollback while rollback does not require current storage configs or secrets', async () => {
for (const status of ['rollback', 'rollback_partial', 'rolled_back']) {
await assert.rejects(discoverStorageConfig(new FakeDb({ runs: [run('old', status)] }), input(), { command: 'apply' }), /回滚.*--run-id/);
}
const db = new FakeDb({ runs: [run('old', 'rollback_partial')], configs: [] });
const result = await discoverStorageConfig(db, input(), { command: 'rollback' });
assert.equal(result.runId, 'old');
assert.equal(result.targetConfigId, '30');
assert.ok(!db.calls.some(call => call.sql.includes('infra_file_config')));
await assert.rejects(discoverStorageConfig(new FakeDb(), input(), { command: 'rollback' }), /未找到可回滚/);
});
test('new content beyond a non-inventory run boundary blocks apply, including bigint boundaries', async () => {
const options = { runs: [run()], maximum: '9007199254740994' };
await assert.rejects(discoverStorageConfig(new FakeDb(options), input(), { command: 'apply' }), /新增内容/);
const planned = await discoverStorageConfig(new FakeDb(options), input(), { command: 'plan' });
assert.equal(planned.runId, 'original-run');
const inventoryDb = new FakeDb({ ...options, runs: [run('still-building', 'inventory')] });
const inventory = await discoverStorageConfig(inventoryDb, input(), { command: 'apply' });
assert.equal(inventory.runId, 'still-building');
assert.ok(!inventoryDb.calls.some(call => call.sql.includes('MAX(id)')));
});
test('missing source contents produce a clear no-migration result without creating a run', async () => {
const db = new FakeDb({ sourceIds: [] });
await assert.rejects(discoverStorageConfig(db, input()), /无需迁移/);
assert.ok(db.calls.every(call => /^SELECT\b/i.test(call.sql.trim())));
});