feat(admin): 添加 S3 兼容存储备份功能

- 新增 S3 配置管理页面,支持 AWS S3、Cloudflare R2、MinIO 等兼容存储
- 实现 S3 客户端工具类,使用 aws4fetch 进行签名和上传
- 添加手动触发备份功能,将评论、配置和统计数据打包为 JSON 上传至 S3
- 在管理后台数据管理页面集成 S3 配置表单和备份操作按钮
- 新增相关 API 端点用于获取/保存 S3 配置及触发备份
This commit is contained in:
anghunk
2026-02-12 15:26:58 +08:00
parent ccffd1df67
commit 379de95961
9 changed files with 365 additions and 1 deletions

View File

@@ -0,0 +1,38 @@
import { Context } from 'hono';
import { Bindings } from '../../bindings';
import { loadS3Settings, saveS3Settings } from '../../utils/s3Settings';
export async function getS3Settings(c: Context<{ Bindings: Bindings }>) {
try {
const settings = await loadS3Settings(c.env);
// Hide secret key for security in UI if needed, but usually user needs to edit it.
// For now, return it as is, or maybe mask it partially?
// User usually expects to see the inputs.
return c.json(settings);
} catch (e: any) {
return c.json({ message: e.message || '加载 S3 配置失败' }, 500);
}
}
export async function updateS3Settings(c: Context<{ Bindings: Bindings }>) {
try {
const body = await c.req.json();
const endpoint = typeof body.endpoint === 'string' ? body.endpoint.trim() : '';
const accessKeyId = typeof body.accessKeyId === 'string' ? body.accessKeyId.trim() : '';
const secretAccessKey = typeof body.secretAccessKey === 'string' ? body.secretAccessKey.trim() : '';
const bucket = typeof body.bucket === 'string' ? body.bucket.trim() : '';
const region = typeof body.region === 'string' ? body.region.trim() : 'auto';
await saveS3Settings(c.env, {
endpoint,
accessKeyId,
secretAccessKey,
bucket,
region,
});
return c.json({ message: '保存成功' });
} catch (e: any) {
return c.json({ message: e.message || '保存失败' }, 500);
}
}

View File

@@ -0,0 +1,54 @@
import { Context } from 'hono';
import { Bindings } from '../../bindings';
import { loadS3Settings } from '../../utils/s3Settings';
import { S3Client } from '../../utils/s3';
import { getConfigs } from './exportConfig';
import { getStatsData } from './exportStats';
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);
}
// 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();
const configs = await getConfigs(c.env);
const stats = await getStatsData(c.env);
const backupData = {
version: '1.0',
timestamp: Date.now(),
comments: comments,
settings: configs,
page_stats: stats.page_stats,
page_visit_daily: stats.page_visit_daily,
likes: stats.likes,
};
const jsonString = JSON.stringify(backupData, null, 2);
const dateStr = new Date().toISOString().split('T')[0];
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({
message: '备份成功',
file: fileName,
});
} catch (e: any) {
console.error('S3 Backup Error:', e);
return c.json({ message: e.message || 'S3 备份失败' }, 500);
}
}