feat(admin): 添加 S3 备份列表查看、下载和删除功能

- 在数据管理页面添加“查看备份”按钮,用于打开备份列表弹窗
- 新增 S3BackupModal 组件,展示备份文件列表,支持下载和删除操作
- 扩展 S3 工具类,支持列出、获取和删除 S3 对象
- 新增后端 API 接口:获取备份列表、删除备份、下载备份文件
- 为所有支持的语言添加相应的国际化文本
This commit is contained in:
anghunk
2026-02-12 16:26:50 +08:00
parent 1c416ce93e
commit 3cfa5f1f6a
21 changed files with 737 additions and 29 deletions

View File

@@ -5,13 +5,27 @@ import { S3Client } from '../../utils/s3';
import { getConfigs } from './exportConfig';
import { getStatsData } from './exportStats';
async function getS3Client(c: Context<{ Bindings: Bindings }>): Promise<{ s3: S3Client; settings: any } | Response> {
const settings = await loadS3Settings(c.env);
if (!settings.endpoint || !settings.bucket || !settings.accessKeyId || !settings.secretAccessKey) {
return c.json({ message: 'S3 配置不完整,请先配置 S3 信息' }, 400);
}
const s3 = new S3Client({
endpoint: settings.endpoint,
accessKeyId: settings.accessKeyId,
secretAccessKey: settings.secretAccessKey,
bucket: settings.bucket,
region: settings.region,
});
return { s3, settings };
}
export async function triggerS3Backup(c: Context<{ Bindings: Bindings }>) {
try {
// 1. Load S3 Settings
const settings = await loadS3Settings(c.env);
if (!settings.endpoint || !settings.bucket || !settings.accessKeyId || !settings.secretAccessKey) {
return c.json({ message: 'S3 配置不完整,请先配置 S3 信息' }, 400);
}
const result = await getS3Client(c);
if (result instanceof Response) return result;
const { s3 } = result;
// 2. Gather Backup Data (Logic from exportBackup.ts)
const { results: comments } = await c.env.CWD_DB.prepare('SELECT * FROM Comment ORDER BY priority DESC, created DESC').all();
@@ -33,14 +47,6 @@ export async function triggerS3Backup(c: Context<{ Bindings: Bindings }>) {
const fileName = `cwd-backup-${dateStr}-${Date.now()}.json`;
// 3. Upload to S3
const s3 = new S3Client({
endpoint: settings.endpoint,
accessKeyId: settings.accessKeyId,
secretAccessKey: settings.secretAccessKey,
bucket: settings.bucket,
region: settings.region,
});
await s3.putObject(fileName, jsonString);
return c.json({
@@ -52,3 +58,62 @@ export async function triggerS3Backup(c: Context<{ Bindings: Bindings }>) {
return c.json({ message: e.message || 'S3 备份失败' }, 500);
}
}
export async function listS3Backups(c: Context<{ Bindings: Bindings }>) {
try {
const result = await getS3Client(c);
if (result instanceof Response) return result;
const { s3 } = result;
const files = await s3.listObjects('cwd-backup-');
return c.json({ files });
} catch (e: any) {
console.error('S3 List Backups Error:', e);
return c.json({ message: e.message || '获取备份列表失败' }, 500);
}
}
export async function deleteS3BackupHandler(c: Context<{ Bindings: Bindings }>) {
try {
const key = c.req.query('key');
if (!key) {
return c.json({ message: '缺少 key 参数' }, 400);
}
const result = await getS3Client(c);
if (result instanceof Response) return result;
const { s3 } = result;
await s3.deleteObject(key);
return c.json({ message: '删除成功' });
} catch (e: any) {
console.error('S3 Delete Backup Error:', e);
return c.json({ message: e.message || '删除备份失败' }, 500);
}
}
export async function downloadS3BackupHandler(c: Context<{ Bindings: Bindings }>) {
try {
const key = c.req.query('key');
if (!key) {
return c.json({ message: '缺少 key 参数' }, 400);
}
const result = await getS3Client(c);
if (result instanceof Response) return result;
const { s3 } = result;
const s3Response = await s3.getObject(key);
const body = await s3Response.arrayBuffer();
// Set headers for file download
c.header('Content-Type', 'application/json');
c.header('Content-Disposition', `attachment; filename="${key}"`);
c.header('Content-Length', String(body.byteLength));
return c.body(body);
} catch (e: any) {
console.error('S3 Download Backup Error:', e);
return c.json({ message: e.message || '下载备份失败' }, 500);
}
}