Merge pull request #561 from Kayphoon/codex/s3-integrated-backup

This commit is contained in:
fawney19
2026-05-27 00:48:59 +08:00
31 changed files with 3698 additions and 100 deletions
+77 -1
View File
@@ -735,6 +735,7 @@ const DEFAULT_BARK_API_BASE: &str = "https://api.day.app";
const SENSITIVE_SYSTEM_CONFIG_KEYS: &[&str] = &[
"smtp_password",
"turnstile_secret_key",
"backup_s3_secret_access_key",
"module.server_chan_push.send_key",
"module.important_notification.server_chan_send_key",
"module.bark_push.device_key",
@@ -1291,6 +1292,7 @@ pub fn build_admin_module_validation_result(
important_notification_configured: bool,
server_chan_push_configured: bool,
bark_push_configured: bool,
s3_backup_configured: bool,
) -> (bool, Option<String>) {
match module_name {
"oauth" => {
@@ -1382,6 +1384,13 @@ pub fn build_admin_module_validation_result(
(false, Some("请先配置 Bark Device Key".to_string()))
}
}
"s3_backup" => {
if s3_backup_configured {
(true, None)
} else {
(false, Some("请先完成 S3 备份配置".to_string()))
}
}
"gemini_files" => {
if gemini_files_has_capable_key {
(true, None)
@@ -1407,7 +1416,8 @@ pub fn build_admin_module_health(
| "proxy_nodes"
| "important_notification"
| "bark_push"
| "server_chan_push" => "healthy",
| "server_chan_push"
| "s3_backup" => "healthy",
"gemini_files" => {
if gemini_files_has_capable_key {
"healthy"
@@ -1671,6 +1681,24 @@ pub fn admin_system_config_default_value(key: &str) -> Option<serde_json::Value>
"turnstile_site_key" => Some(serde_json::Value::Null),
"turnstile_secret_key" => Some(serde_json::Value::Null),
"turnstile_allowed_hostnames" => Some(json!([])),
"backup_s3_enabled" => Some(json!(false)),
"backup_s3_scope" => Some(json!("data")),
"backup_s3_endpoint" => Some(serde_json::Value::Null),
"backup_s3_region" => Some(json!("auto")),
"backup_s3_bucket" => Some(serde_json::Value::Null),
"backup_s3_prefix" => Some(json!("aether/backups/")),
"backup_s3_access_key_id" => Some(serde_json::Value::Null),
"backup_s3_secret_access_key" => Some(serde_json::Value::Null),
"backup_s3_path_style" => Some(json!(true)),
"backup_s3_compression" => Some(json!("zstd")),
"backup_s3_schedule_unit" => Some(json!("days")),
"backup_s3_schedule_interval" => Some(json!(1)),
"backup_s3_schedule_minute" => Some(json!(0)),
"backup_s3_schedule_hour" => Some(json!(3)),
"backup_s3_schedule_weekday" => Some(json!(1)),
"backup_s3_schedule_month_day" => Some(json!(1)),
"backup_s3_retention_count" => Some(json!(7)),
"backup_s3_last_slot" => Some(serde_json::Value::Null),
"email_suffix_mode" => Some(json!("none")),
"email_suffix_list" => Some(json!([])),
"enable_format_conversion" => Some(json!(false)),
@@ -3330,6 +3358,41 @@ mod tests {
assert!(!is_sensitive_admin_system_config_key("site_name"));
}
#[test]
fn s3_backup_secret_access_key_is_sensitive() {
assert!(is_sensitive_admin_system_config_key(
"backup_s3_secret_access_key"
));
assert!(is_sensitive_admin_system_config_key(
"BACKUP_S3_SECRET_ACCESS_KEY"
));
assert!(!is_sensitive_admin_system_config_key("backup_s3_bucket"));
}
#[test]
fn s3_backup_defaults_match_admin_ui_contract() {
assert_eq!(
admin_system_config_default_value("backup_s3_scope"),
Some(json!("data"))
);
assert_eq!(
admin_system_config_default_value("backup_s3_schedule_unit"),
Some(json!("days"))
);
assert_eq!(
admin_system_config_default_value("backup_s3_schedule_interval"),
Some(json!(1))
);
assert_eq!(
admin_system_config_default_value("backup_s3_retention_count"),
Some(json!(7))
);
assert_eq!(
admin_system_config_default_value("backup_s3_path_style"),
Some(json!(true))
);
}
#[test]
fn legacy_notification_email_config_key_normalizes_to_important_notification() {
assert_eq!(
@@ -3345,6 +3408,19 @@ mod tests {
);
}
#[test]
fn s3_backup_secret_detail_is_write_only() {
let payload = build_admin_system_config_detail_payload(
"backup_s3_secret_access_key",
Some(json!("encrypted-secret")),
)
.expect("sensitive backup key should render");
assert_eq!(payload["key"], json!("backup_s3_secret_access_key"));
assert_eq!(payload["value"], serde_json::Value::Null);
assert_eq!(payload["is_set"], json!(true));
}
#[test]
fn legacy_server_chan_config_keys_normalize_to_push_module() {
assert_eq!(
@@ -244,7 +244,7 @@ impl MysqlUsageReadRepository {
r#"
SELECT
DATE_FORMAT(FROM_UNIXTIME(created_at_unix_ms), '%Y-%m-%d') AS date,
COUNT(*) AS requests,
CAST(COUNT(*) AS SIGNED) AS requests,
CAST(COALESCE(SUM(
GREATEST(COALESCE(input_tokens, 0), 0)
+ GREATEST(COALESCE(output_tokens, 0), 0)
@@ -641,7 +641,7 @@ WHERE user_id IN (
r#"
SELECT
`usage`.user_id,
COUNT(*) AS request_count,
CAST(COUNT(*) AS SIGNED) AS request_count,
CAST(COALESCE(SUM(GREATEST(COALESCE(`usage`.total_tokens, 0), 0)), 0) AS SIGNED) AS total_tokens
FROM `usage`
JOIN (
@@ -1510,6 +1510,7 @@ mod tests {
assert!(source.contains("FROM stats_daily"));
assert!(source.contains("FROM stats_user_daily"));
assert!(source.contains("AS SIGNED) AS total_tokens"));
assert!(source.contains("CAST(COUNT(*) AS SIGNED) AS requests"));
assert!(source.contains("summaries.entry(item.date.clone()).or_insert(item)"));
}
@@ -1520,6 +1521,9 @@ mod tests {
assert!(source.contains("FROM stats_user_daily"));
assert!(source.contains("MAX(`date`) AS latest_date"));
assert!(source.contains("AS SIGNED) AS request_count"));
assert!(
source.contains("CAST(COALESCE(SUM(total_requests), 0) AS SIGNED) AS request_count")
);
assert!(source.contains("requested.cutoff_unix_secs"));
}
@@ -1532,6 +1536,10 @@ mod tests {
assert!(source.contains("FROM stats_user_daily"));
assert!(source.contains("'aggregate' AS model"));
assert!(source.contains("AS SIGNED) AS total_requests"));
assert!(
source.contains("CAST(COALESCE(SUM(total_requests), 0) AS SIGNED) AS total_requests")
);
assert!(source.contains("CAST(COALESCE(SUM(total_requests), 0) AS SIGNED) AS requests"));
}
#[tokio::test]