完善数据库备份恢复链路
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
import { Readable } from 'node:stream'
|
||||
|
||||
import {
|
||||
manifestKeyFor,
|
||||
parseBackupArgs,
|
||||
pickLatestDumpKey,
|
||||
resolveBackupStorageConfig,
|
||||
runDownload,
|
||||
runLatest,
|
||||
runList,
|
||||
runUpload,
|
||||
} from './db-backup.js'
|
||||
import type { StorageObjectSummary } from './services/file-storage/object-storage.js'
|
||||
|
||||
test('resolveBackupStorageConfig 优先 DB_BACKUP_OSS_* 并按字段回退 STORAGE_OSS_*', () => {
|
||||
const config = resolveBackupStorageConfig({
|
||||
DB_BACKUP_OSS_ENDPOINT: 'https://backup.example.com',
|
||||
STORAGE_OSS_ENDPOINT: 'https://storage.example.com',
|
||||
STORAGE_OSS_BUCKET: 'order-site',
|
||||
STORAGE_OSS_ACCESS_KEY_ID: 'storage-key',
|
||||
DB_BACKUP_OSS_ACCESS_KEY_ID: 'backup-key',
|
||||
STORAGE_OSS_SECRET_ACCESS_KEY: 'storage-secret',
|
||||
})
|
||||
|
||||
assert.equal(config.endpoint, 'https://backup.example.com')
|
||||
assert.equal(config.bucket, 'order-site')
|
||||
assert.equal(config.accessKeyId, 'backup-key')
|
||||
assert.equal(config.secretAccessKey, 'storage-secret')
|
||||
assert.equal(config.region, 'oss-cn-hangzhou')
|
||||
})
|
||||
|
||||
test('resolveBackupStorageConfig 全部未配置时返回空值由调用方校验', () => {
|
||||
const config = resolveBackupStorageConfig({})
|
||||
assert.equal(config.endpoint, '')
|
||||
assert.equal(config.bucket, '')
|
||||
})
|
||||
|
||||
test('parseBackupArgs 解析命令与参数并去掉前缀尾部斜杠', () => {
|
||||
const args = parseBackupArgs([
|
||||
'upload',
|
||||
'--key',
|
||||
'backups/postgres/order_site-20260824-030000.dump',
|
||||
'--prefix',
|
||||
'backups/postgres/',
|
||||
'--kind',
|
||||
'app-configs',
|
||||
'--size',
|
||||
'1234',
|
||||
])
|
||||
|
||||
assert.equal(args.command, 'upload')
|
||||
assert.equal(args.key, 'backups/postgres/order_site-20260824-030000.dump')
|
||||
assert.equal(args.prefix, 'backups/postgres')
|
||||
assert.equal(args.kind, 'app-configs')
|
||||
assert.equal(args.size, 1234)
|
||||
})
|
||||
|
||||
test('parseBackupArgs 无参数时使用默认前缀与类型', () => {
|
||||
const args = parseBackupArgs(['list'])
|
||||
assert.equal(args.command, 'list')
|
||||
assert.equal(args.prefix, 'backups/postgres')
|
||||
assert.equal(args.kind, 'postgres-dump')
|
||||
assert.equal(args.key, '')
|
||||
assert.equal(args.allowUnverified, false)
|
||||
})
|
||||
|
||||
test('parseBackupArgs 支持显式兼容未校验备份', () => {
|
||||
assert.equal(parseBackupArgs(['latest', '--allow-unverified']).allowUnverified, true)
|
||||
})
|
||||
|
||||
test('manifestKeyFor 在备份 key 后追加清单后缀', () => {
|
||||
assert.equal(manifestKeyFor('backups/postgres/a.dump'), 'backups/postgres/a.dump.manifest.json')
|
||||
})
|
||||
|
||||
test('pickLatestDumpKey 只认 dump 并取最近修改的对象', () => {
|
||||
const key = pickLatestDumpKey([
|
||||
{
|
||||
key: 'backups/postgres/a.dump',
|
||||
size: 1,
|
||||
etag: '',
|
||||
lastModified: new Date('2026-08-01T00:00:00Z'),
|
||||
},
|
||||
{
|
||||
key: 'backups/postgres/a.dump.manifest.json',
|
||||
size: 1,
|
||||
etag: '',
|
||||
lastModified: new Date('2026-08-23T00:00:00Z'),
|
||||
},
|
||||
{
|
||||
key: 'backups/configs/a.tar.gz',
|
||||
size: 1,
|
||||
etag: '',
|
||||
lastModified: new Date('2026-08-23T00:00:00Z'),
|
||||
},
|
||||
{
|
||||
key: 'backups/postgres/b.dump',
|
||||
size: 1,
|
||||
etag: '',
|
||||
lastModified: new Date('2026-08-22T00:00:00Z'),
|
||||
},
|
||||
])
|
||||
|
||||
assert.equal(key, 'backups/postgres/b.dump')
|
||||
})
|
||||
|
||||
test('pickLatestDumpKey 无备份时返回 null,无时间时按 key 兜底', () => {
|
||||
assert.equal(pickLatestDumpKey([]), null)
|
||||
assert.equal(
|
||||
pickLatestDumpKey([
|
||||
{ key: 'backups/postgres/order_site-1.dump', size: 1, etag: '' },
|
||||
{ key: 'backups/postgres/order_site-2.dump', size: 1, etag: '' },
|
||||
]),
|
||||
'backups/postgres/order_site-2.dump',
|
||||
)
|
||||
})
|
||||
|
||||
test('runUpload 支持流式输入并生成带长度和 sha256 的 manifest', async () => {
|
||||
const uploads: Array<{ key: string; content: Buffer; contentLength?: number }> = []
|
||||
const storage = {
|
||||
putObject: async (input: {
|
||||
key: string
|
||||
content: Buffer | Readable
|
||||
contentLength?: number
|
||||
}) => {
|
||||
const chunks: Buffer[] = []
|
||||
if (Buffer.isBuffer(input.content)) {
|
||||
chunks.push(input.content)
|
||||
} else {
|
||||
for await (const chunk of input.content) chunks.push(Buffer.from(chunk))
|
||||
}
|
||||
uploads.push({
|
||||
key: input.key,
|
||||
content: Buffer.concat(chunks),
|
||||
contentLength: input.contentLength,
|
||||
})
|
||||
},
|
||||
} as never
|
||||
|
||||
await runUpload(
|
||||
storage,
|
||||
parseBackupArgs([
|
||||
'upload',
|
||||
'--key',
|
||||
'backups/configs/a.tar.gz',
|
||||
'--kind',
|
||||
'app-configs',
|
||||
'--size',
|
||||
'5',
|
||||
]),
|
||||
Readable.from([Buffer.from('hello')]),
|
||||
)
|
||||
|
||||
assert.equal(uploads.length, 2)
|
||||
assert.equal(uploads[0]?.content.toString(), 'hello')
|
||||
assert.equal(uploads[0]?.contentLength, 5)
|
||||
assert.match(uploads[1]?.content.toString() || '', /"size": 5/)
|
||||
assert.match(uploads[1]?.content.toString() || '', /sha256/)
|
||||
})
|
||||
|
||||
test('runLatest 跳过没有 manifest 的新对象并选择最近的有效备份', async () => {
|
||||
const objects: StorageObjectSummary[] = [
|
||||
{
|
||||
key: 'backups/postgres/new.dump',
|
||||
size: 5,
|
||||
etag: '',
|
||||
lastModified: new Date('2026-08-23T03:00:00Z'),
|
||||
},
|
||||
{
|
||||
key: 'backups/postgres/valid.dump',
|
||||
size: 5,
|
||||
etag: '',
|
||||
lastModified: new Date('2026-08-22T03:00:00Z'),
|
||||
},
|
||||
]
|
||||
const storage = {
|
||||
listObjects: async () => objects,
|
||||
getObject: async (key: string) => {
|
||||
if (key.endsWith('new.dump.manifest.json')) {
|
||||
const error = Object.assign(new Error('missing'), { name: 'NoSuchKey' })
|
||||
throw error
|
||||
}
|
||||
return {
|
||||
reader: Readable.from([
|
||||
JSON.stringify({
|
||||
key: 'backups/postgres/valid.dump',
|
||||
kind: 'postgres-dump',
|
||||
size: 5,
|
||||
sha256: 'a'.repeat(64),
|
||||
createdAt: '2026-08-22T03:00:00Z',
|
||||
}),
|
||||
]),
|
||||
}
|
||||
},
|
||||
} as never
|
||||
|
||||
assert.equal(
|
||||
await runLatest(storage, parseBackupArgs(['latest', '--prefix', 'backups/postgres'])),
|
||||
'backups/postgres/valid.dump',
|
||||
)
|
||||
})
|
||||
Reference in New Issue
Block a user