feat(admin): configure S3 backup User-Agent

This commit is contained in:
Kayphoon
2026-07-16 08:56:56 +00:00
parent 9ea84f9748
commit 6b707f29a2
8 changed files with 46 additions and 1 deletions
+5
View File
@@ -11,6 +11,7 @@ pub(crate) struct S3BackupConfig {
pub(crate) scope: BackupScope,
pub(crate) endpoint: String,
pub(crate) region: String,
pub(crate) user_agent: String,
pub(crate) bucket: String,
pub(crate) prefix: String,
pub(crate) access_key_id: String,
@@ -104,6 +105,8 @@ impl S3BackupConfig {
endpoint,
region: optional_string(entries, "backup_s3_region")?
.unwrap_or_else(|| "auto".to_string()),
user_agent: optional_string(entries, "backup_s3_user_agent")?
.unwrap_or_else(|| "rclone/v1.68.0".to_string()),
bucket,
prefix: optional_string(entries, "backup_s3_prefix")?
.unwrap_or_else(|| "aether/backups/".to_string()),
@@ -179,6 +182,7 @@ fn config_label(key: &str) -> &str {
match key {
"backup_s3_endpoint" => "EndpointS3 地址)",
"backup_s3_region" => "RegionS3 区域)",
"backup_s3_user_agent" => "User-Agent(请求头标识)",
"backup_s3_bucket" => "Bucket(存储桶)",
"backup_s3_prefix" => "Prefix(备份前缀)",
"backup_s3_access_key_id" => "Access Key ID(访问密钥 ID",
@@ -384,6 +388,7 @@ mod tests {
assert_eq!(config.scope, BackupScope::Data);
assert_eq!(config.region, "auto");
assert_eq!(config.user_agent, "rclone/v1.68.0");
assert_eq!(config.prefix, "aether/backups/");
assert_eq!(config.path_style, true);
assert_eq!(config.compression, "zstd");
@@ -132,6 +132,7 @@ mod tests {
scope,
endpoint: "https://example.com".to_string(),
region: "auto".to_string(),
user_agent: "rclone/v1.68.0".to_string(),
bucket: "aether-backups".to_string(),
prefix: "prod/".to_string(),
access_key_id: "test-access-key".to_string(),
+14 -1
View File
@@ -6,7 +6,8 @@ use bytes::Bytes;
use futures_util::TryStreamExt;
use object_store::aws::AmazonS3Builder;
use object_store::path::Path;
use object_store::ObjectStore;
use object_store::{ClientOptions, ObjectStore};
use reqwest::header::HeaderValue;
use tokio::sync::RwLock;
use super::config::S3BackupConfig;
@@ -84,7 +85,19 @@ pub(crate) struct ObjectStoreS3BackupStore {
impl ObjectStoreS3BackupStore {
pub(crate) fn from_config(config: &S3BackupConfig) -> Result<Self, BackupStoreError> {
// 部分 S3 兼容网关(如中国科技云 s3.cstcloud.cn)按 User-Agent 放行请求,
// object_store 默认 UA 会被拒,因此允许自定义 User-Agent。
let client_options = if config.user_agent.trim().is_empty() {
ClientOptions::new()
} else {
ClientOptions::new().with_user_agent(
HeaderValue::from_str(config.user_agent.trim()).map_err(|error| {
BackupStoreError::new(format!("S3 备份 User-Agent 配置无效: {error}"))
})?,
)
};
let store = AmazonS3Builder::new()
.with_client_options(client_options)
.with_endpoint(config.endpoint.clone())
.with_region(config.region.clone())
.with_bucket_name(config.bucket.clone())
+1
View File
@@ -31,6 +31,7 @@ const S3_BACKUP_CONFIG_KEYS: &[&str] = &[
"backup_s3_scope",
"backup_s3_endpoint",
"backup_s3_region",
"backup_s3_user_agent",
"backup_s3_bucket",
"backup_s3_prefix",
"backup_s3_access_key_id",
+5
View File
@@ -1724,6 +1724,7 @@ pub fn admin_system_config_default_value(key: &str) -> Option<serde_json::Value>
"backup_s3_scope" => Some(json!("data")),
"backup_s3_endpoint" => Some(serde_json::Value::Null),
"backup_s3_region" => Some(json!("auto")),
"backup_s3_user_agent" => Some(json!("rclone/v1.68.0")),
"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),
@@ -3378,6 +3379,10 @@ mod tests {
admin_system_config_default_value("backup_s3_path_style"),
Some(json!(true))
);
assert_eq!(
admin_system_config_default_value("backup_s3_user_agent"),
Some(json!("rclone/v1.68.0"))
);
}
#[test]
+1
View File
@@ -964,6 +964,7 @@ export const MOCK_SYSTEM_CONFIGS: Array<{ key: string; value: unknown; descripti
{ key: 'backup_s3_scope', value: 'data', description: 'S3 备份范围' },
{ key: 'backup_s3_endpoint', value: null, description: 'S3 Endpoint' },
{ key: 'backup_s3_region', value: 'auto', description: 'S3 Region' },
{ key: 'backup_s3_user_agent', value: 'rclone/v1.68.0', description: 'S3 User-Agent' },
{ key: 'backup_s3_bucket', value: null, description: 'S3 Bucket' },
{ key: 'backup_s3_prefix', value: 'aether/backups/', description: 'S3 备份前缀' },
{ key: 'backup_s3_access_key_id', value: null, description: 'S3 Access Key ID' },
@@ -275,6 +275,21 @@
@update:model-value="backup.config.value.region = String($event)"
/>
</div>
<div>
<Label
for="backup-user-agent"
class="block text-sm font-medium"
>
User-Agent
</Label>
<Input
id="backup-user-agent"
:model-value="backup.config.value.userAgent"
class="mt-1"
placeholder="rclone/v1.68.0"
@update:model-value="backup.config.value.userAgent = String($event)"
/>
</div>
<div>
<Label
for="backup-prefix"
@@ -11,6 +11,7 @@ export interface S3BackupConfig {
scope: S3BackupScope
endpoint: string
region: string
userAgent: string
bucket: string
prefix: string
accessKeyId: string
@@ -34,6 +35,7 @@ const CONFIG_KEY_BY_FIELD: Record<ConfigField, string> = {
scope: 'backup_s3_scope',
endpoint: 'backup_s3_endpoint',
region: 'backup_s3_region',
userAgent: 'backup_s3_user_agent',
bucket: 'backup_s3_bucket',
prefix: 'backup_s3_prefix',
accessKeyId: 'backup_s3_access_key_id',
@@ -63,6 +65,7 @@ function defaultS3BackupConfig(): S3BackupConfig {
scope: 'data',
endpoint: '',
region: 'auto',
userAgent: 'rclone/v1.68.0',
bucket: '',
prefix: 'aether/backups/',
accessKeyId: '',
@@ -192,6 +195,7 @@ export function useS3BackupConfig() {
{ key: 'backup_s3_scope', value: config.value.scope, description: 'S3 备份范围' },
{ key: 'backup_s3_endpoint', value: config.value.endpoint.trim() || null, description: 'S3 Endpoint' },
{ key: 'backup_s3_region', value: config.value.region.trim() || 'auto', description: 'S3 Region' },
{ key: 'backup_s3_user_agent', value: config.value.userAgent.trim() || 'rclone/v1.68.0', description: 'S3 User-Agent' },
{ key: 'backup_s3_bucket', value: config.value.bucket.trim() || null, description: 'S3 Bucket' },
{ key: 'backup_s3_prefix', value: config.value.prefix.trim() || 'aether/backups/', description: 'S3 备份前缀' },
{ key: 'backup_s3_access_key_id', value: config.value.accessKeyId.trim() || null, description: 'S3 Access Key ID' },