diff --git a/docs-site/docs/features/storage-backends.md b/docs-site/docs/features/storage-backends.md index 6e977cb..22a0b11 100644 --- a/docs-site/docs/features/storage-backends.md +++ b/docs-site/docs/features/storage-backends.md @@ -23,6 +23,12 @@ BackupX aims to accept any place you'd want to drop a backup file. New local-disk targets enable **Relay remote backups through Master** by default. This makes the configured path belong to the Master, so a storage server mounted there can collect backups from many source Agents. Turn the switch off when the path intentionally belongs to each Agent. Existing targets retain their previous Agent-local behavior until explicitly changed. +## Storage capacity and backup quotas + +Physical storage usage is shown only when the backend returns total capacity and used bytes. S3-compatible storage, including MinIO, does not provide this through the standard S3 API or rclone About capability. Missing capacity is not treated as zero. + +For these targets, BackupX displays the size tracked by backup records. Set a **backup quota** on the storage target to display a backup quota percentage. This soft quota is enforced by BackupX; it is not the bucket quota or the total disk capacity of the MinIO cluster. The tracked size does not include objects created by other applications or untracked object versions. Use the MinIO console or its monitoring interfaces for physical cluster and bucket capacity. + ## Rclone backends Every [rclone backend](https://rclone.org/overview/) is exposed as a first-class storage type — SFTP, Azure Blob, Dropbox, OneDrive, Backblaze B2, Wasabi, pCloud, HDFS, and many more. diff --git a/docs-site/i18n/zh-CN/docusaurus-plugin-content-docs/current/features/storage-backends.md b/docs-site/i18n/zh-CN/docusaurus-plugin-content-docs/current/features/storage-backends.md index b3f7895..3233117 100644 --- a/docs-site/i18n/zh-CN/docusaurus-plugin-content-docs/current/features/storage-backends.md +++ b/docs-site/i18n/zh-CN/docusaurus-plugin-content-docs/current/features/storage-backends.md @@ -23,6 +23,12 @@ BackupX 的目标是接入任何你想放置备份文件的地方。 新建本地磁盘目标默认开启 **远程备份经 Master 中转**。开启时,配置目录属于 Master,挂载到 Master 的存储服务器可集中接收多台源 Agent 的备份;如果该路径本就属于各 Agent,请关闭此选项。升级前已有目标保持原来的 Agent 本机落盘行为,只有显式开启后才会切换。 +## 存储容量与备份配额 + +只有后端返回总容量和已用字节时,才显示真实存储空间使用率。包括 MinIO 在内的 S3 兼容存储不通过标准 S3 API 或 rclone About 能力提供这些数据;未获取到容量不代表容量为零。 + +此类目标会显示备份记录统计的大小。可在存储目标中设置 **备份配额**,显示备份配额使用率。这是 BackupX 执行的软限额,不是桶配额,也不是 MinIO 集群磁盘总容量;统计不包含其他应用写入的对象或未被记录的对象版本。集群物理容量与桶容量请使用 MinIO 控制台或监控接口查询。 + ## Rclone 后端 每一种 [rclone 后端](https://rclone.org/overview/) 都作为一等公民暴露 — SFTP、Azure Blob、Dropbox、OneDrive、Backblaze B2、Wasabi、pCloud、HDFS 等。 diff --git a/server/internal/backup/helpers.go b/server/internal/backup/helpers.go index c8d7a93..b088c02 100644 --- a/server/internal/backup/helpers.go +++ b/server/internal/backup/helpers.go @@ -6,6 +6,7 @@ import ( "path/filepath" "strings" "time" + "unicode" ) func createTempArtifact(baseDir, taskName string, extension string) (string, string, error) { @@ -29,7 +30,7 @@ func sanitizeFileName(value string) string { builder.WriteRune(char) case char >= 'A' && char <= 'Z': builder.WriteRune(char + ('a' - 'A')) - case char >= '0' && char <= '9': + case unicode.IsLetter(char) || unicode.IsNumber(char): builder.WriteRune(char) case char == '-' || char == '_': builder.WriteRune(char) diff --git a/server/internal/backup/temp_files.go b/server/internal/backup/temp_files.go index cce68c5..0815709 100644 --- a/server/internal/backup/temp_files.go +++ b/server/internal/backup/temp_files.go @@ -9,7 +9,7 @@ import ( "time" ) -var fileNameCleaner = regexp.MustCompile(`[^a-zA-Z0-9._-]+`) +var fileNameCleaner = regexp.MustCompile(`[^\p{L}\p{N}._-]+`) func EnsureTempRoot() (string, error) { root := filepath.Join(os.TempDir(), "backupx") diff --git a/server/internal/backup/temp_files_test.go b/server/internal/backup/temp_files_test.go new file mode 100644 index 0000000..ddeea9f --- /dev/null +++ b/server/internal/backup/temp_files_test.go @@ -0,0 +1,62 @@ +package backup + +import ( + "os" + "path/filepath" + "strings" + "testing" + "time" +) + +func TestArtifactNamesPreserveUnicodeAndRejectUnsafeCharacters(t *testing.T) { + startedAt := time.Date(2026, 10, 6, 1, 2, 3, 0, time.UTC) + cases := []struct { + name string + legacy string + shared string + }{ + {"中文备份", "中文备份", "中文备份"}, + {" MySQL 数据库 ", "mysql_数据库", "mysql-数据库"}, + {"../目录\\备份:\x00?*", "目录备份", "目录-备份"}, + {"... /\\:*?", "", ""}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := sanitizeFileName(tc.name); got != tc.legacy { + t.Fatalf("sanitizeFileName(%q) = %q, want %q", tc.name, got, tc.legacy) + } + if got := sanitizeTaskName(tc.name); got != tc.shared { + t.Fatalf("sanitizeTaskName(%q) = %q, want %q", tc.name, got, tc.shared) + } + prefix := tc.shared + if prefix == "" { + prefix = "backup" + } + if got := BuildArtifactName(tc.name, startedAt, "sql"); got != prefix+"_20261006_010203.sql" { + t.Fatalf("unexpected artifact name: %q", got) + } + tempDir, artifactPath, err := createTempArtifact(t.TempDir(), tc.name, "tar") + if err != nil { + t.Fatal(err) + } + prefix = tc.legacy + if prefix == "" { + prefix = "backup" + } + if filepath.Dir(artifactPath) != tempDir || !strings.HasPrefix(filepath.Base(artifactPath), prefix+"_") { + t.Fatalf("unsafe or incorrect artifact path: %q", artifactPath) + } + }) + } + t.Setenv("TMPDIR", t.TempDir()) + taskDir, err := CreateTaskTempDir("中文备份", startedAt) + if err != nil { + t.Fatal(err) + } + if filepath.Base(taskDir) != "中文备份_20261006_010203" { + t.Fatalf("unexpected task directory: %q", taskDir) + } + if _, err := os.Stat(taskDir); err != nil { + t.Fatal(err) + } +} diff --git a/server/internal/installscript/renderer.go b/server/internal/installscript/renderer.go index d61ee9b..3ca855a 100644 --- a/server/internal/installscript/renderer.go +++ b/server/internal/installscript/renderer.go @@ -39,7 +39,7 @@ type Context struct { func DownloadBaseFor(src string) string { switch src { case model.InstallSourceGhproxy: - return "https://ghproxy.com/https://github.com/Awuqing/BackupX/releases/download" + return "https://ghfast.top/https://github.com/Awuqing/BackupX/releases/download" default: return "https://github.com/Awuqing/BackupX/releases/download" } diff --git a/server/internal/installscript/renderer_test.go b/server/internal/installscript/renderer_test.go index 209448f..aaab4fb 100644 --- a/server/internal/installscript/renderer_test.go +++ b/server/internal/installscript/renderer_test.go @@ -332,7 +332,7 @@ func TestRenderScriptRejectsBadVersion(t *testing.T) { func TestDownloadBaseMapping(t *testing.T) { cases := map[string]string{ model.InstallSourceGitHub: "https://github.com/Awuqing/BackupX/releases/download", - model.InstallSourceGhproxy: "https://ghproxy.com/https://github.com/Awuqing/BackupX/releases/download", + model.InstallSourceGhproxy: "https://ghfast.top/https://github.com/Awuqing/BackupX/releases/download", } for src, want := range cases { got := DownloadBaseFor(src) diff --git a/web/package-lock.json b/web/package-lock.json index c6e15cb..5156e00 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.0", "dependencies": { "@arco-design/web-react": "^2.66.16", - "axios": "^1.19.0", + "axios": "^1.20.0", "echarts": "^6.1.0", "echarts-for-react": "^3.0.6", "i18next": "^25.8.14", @@ -2261,9 +2261,9 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.19.0.tgz", - "integrity": "sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==", + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz", + "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==", "license": "MIT", "dependencies": { "follow-redirects": "^1.16.0", diff --git a/web/package.json b/web/package.json index 0f4f983..625baec 100644 --- a/web/package.json +++ b/web/package.json @@ -18,7 +18,7 @@ }, "dependencies": { "@arco-design/web-react": "^2.66.16", - "axios": "^1.19.0", + "axios": "^1.20.0", "echarts": "^6.1.0", "echarts-for-react": "^3.0.6", "i18next": "^25.8.14", diff --git a/web/src/pages/nodes/wizard/Step2DeployOptions.tsx b/web/src/pages/nodes/wizard/Step2DeployOptions.tsx index 5e0fdbf..3b74b70 100644 --- a/web/src/pages/nodes/wizard/Step2DeployOptions.tsx +++ b/web/src/pages/nodes/wizard/Step2DeployOptions.tsx @@ -107,7 +107,7 @@ export function Step2DeployOptions({ masterVersion, value, onChange }: Props) {