Files
BackupX/server/internal/storage/rclone/provider_test.go
Awuqing 1003302bdd 功能: 集成 rclone 高级传输特性 + 全 70+ 后端支持
1. 失败自动重试:rclone Pacer 指数退避,默认 10 次底层 HTTP 重试
2. 带宽限制:配置 bandwidth_limit + Settings 运行时可调
3. 上传实时进度:progressReader + LogHub SSE 推送字节级进度/速率
4. 存储空间查询:StorageAbout 可选接口,GetUsage 返回远端真实空间
5. 全 rclone 后端:backend/all 引入 70+ 后端,新增 rclone 存储类型,
   API 驱动的可搜索后端选择器 + 动态配置表单
2026-03-31 23:37:59 +08:00

203 lines
5.8 KiB
Go

package rclone
import (
"context"
"io"
"strings"
"testing"
)
func TestProviderLocalDiskCRUD(t *testing.T) {
factory := NewLocalDiskFactory()
provider, err := factory.New(context.Background(), map[string]any{"basePath": t.TempDir()})
if err != nil {
t.Fatalf("Factory.New returned error: %v", err)
}
if err := provider.TestConnection(context.Background()); err != nil {
t.Fatalf("TestConnection returned error: %v", err)
}
// Upload
if err := provider.Upload(context.Background(), "daily/backup.txt", strings.NewReader("hello"), 5, nil); err != nil {
t.Fatalf("Upload returned error: %v", err)
}
// Download
reader, err := provider.Download(context.Background(), "daily/backup.txt")
if err != nil {
t.Fatalf("Download returned error: %v", err)
}
defer reader.Close()
content, _ := io.ReadAll(reader)
if string(content) != "hello" {
t.Fatalf("expected 'hello', got %q", string(content))
}
// List with prefix
items, err := provider.List(context.Background(), "daily")
if err != nil {
t.Fatalf("List returned error: %v", err)
}
if len(items) != 1 || items[0].Key != "daily/backup.txt" {
t.Fatalf("unexpected list result: %#v", items)
}
// Delete
if err := provider.Delete(context.Background(), "daily/backup.txt"); err != nil {
t.Fatalf("Delete returned error: %v", err)
}
// List after delete should be empty
items, err = provider.List(context.Background(), "daily")
if err != nil {
t.Fatalf("List after delete returned error: %v", err)
}
if len(items) != 0 {
t.Fatalf("expected empty list after delete, got %d items", len(items))
}
}
func TestProviderLocalDiskRequiresBasePath(t *testing.T) {
_, err := NewLocalDiskFactory().New(context.Background(), map[string]any{"basePath": ""})
if err == nil {
t.Fatal("expected error for empty basePath")
}
}
func TestProviderS3RequiresBucketAndCredentials(t *testing.T) {
factory := NewS3Factory()
_, err := factory.New(context.Background(), map[string]any{"bucket": "", "accessKeyId": "a", "secretAccessKey": "b"})
if err == nil || !strings.Contains(err.Error(), "bucket") {
t.Fatalf("expected bucket required error, got %v", err)
}
_, err = factory.New(context.Background(), map[string]any{"bucket": "demo", "accessKeyId": "", "secretAccessKey": "b"})
if err == nil || !strings.Contains(err.Error(), "credentials") {
t.Fatalf("expected credentials required error, got %v", err)
}
}
func TestQuoteParam(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"simple", "simple"},
{"", ""},
{"has,comma", "'has,comma'"},
{"has:colon", "'has:colon'"},
{"has=equals", "'has=equals'"},
{"has'quote", "'has''quote'"},
{"a,b:c=d'e", "'a,b:c=d''e'"},
}
for _, tt := range tests {
got := quoteParam(tt.input)
if got != tt.expected {
t.Errorf("quoteParam(%q) = %q, want %q", tt.input, got, tt.expected)
}
}
}
func TestBuildS3Remote(t *testing.T) {
remote := buildS3Remote("Alibaba", "keyID", "secret", "https://oss-cn-hangzhou.aliyuncs.com", "cn-hangzhou", "my-bucket", false)
if !strings.Contains(remote, "provider=Alibaba") {
t.Fatalf("expected provider=Alibaba in remote: %s", remote)
}
if !strings.Contains(remote, ":my-bucket") {
t.Fatalf("expected :my-bucket suffix in remote: %s", remote)
}
if !strings.HasPrefix(remote, ":s3,") {
t.Fatalf("expected :s3, prefix in remote: %s", remote)
}
}
func TestRcloneFactoryCRUD(t *testing.T) {
dir := t.TempDir()
factory := NewRcloneFactory()
// 使用 rclone 的 local 后端
provider, err := factory.New(context.Background(), map[string]any{
"backend": "local",
"root": dir,
})
if err != nil {
t.Fatalf("RcloneFactory.New returned error: %v", err)
}
if err := provider.Upload(context.Background(), "test.txt", strings.NewReader("rclone"), 6, nil); err != nil {
t.Fatalf("Upload via rclone factory returned error: %v", err)
}
reader, err := provider.Download(context.Background(), "test.txt")
if err != nil {
t.Fatalf("Download returned error: %v", err)
}
defer reader.Close()
content, _ := io.ReadAll(reader)
if string(content) != "rclone" {
t.Fatalf("expected 'rclone', got %q", string(content))
}
if err := provider.Delete(context.Background(), "test.txt"); err != nil {
t.Fatalf("Delete returned error: %v", err)
}
}
func TestRcloneFactoryRequiresBackend(t *testing.T) {
_, err := NewRcloneFactory().New(context.Background(), map[string]any{"root": "/tmp"})
if err == nil || !strings.Contains(err.Error(), "backend") {
t.Fatalf("expected backend required error, got %v", err)
}
}
func TestListBackends(t *testing.T) {
backends := ListBackends()
if len(backends) < 30 {
t.Fatalf("expected at least 30 backends, got %d", len(backends))
}
// 确认 sftp 在列表中
found := false
for _, b := range backends {
if b.Name == "sftp" {
found = true
if len(b.Options) == 0 {
t.Fatal("sftp backend should have options")
}
break
}
}
if !found {
t.Fatal("sftp backend not found in ListBackends()")
}
}
func TestProviderAbout(t *testing.T) {
factory := NewLocalDiskFactory()
provider, err := factory.New(context.Background(), map[string]any{"basePath": t.TempDir()})
if err != nil {
t.Fatalf("Factory.New returned error: %v", err)
}
// local 后端支持 About
rcloneProvider := provider.(*Provider)
usage, err := rcloneProvider.About(context.Background())
if err != nil {
t.Fatalf("About returned error: %v", err)
}
if usage.Total == nil || *usage.Total <= 0 {
t.Fatalf("expected non-zero total disk space, got %v", usage.Total)
}
}
func TestPathDir(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"BackupX/file/260308/backup.tar.gz", "BackupX/file/260308"},
{"backup.tar.gz", ""},
{"a/b", "a"},
{"", ""},
}
for _, tt := range tests {
got := pathDir(tt.input)
if got != tt.expected {
t.Errorf("pathDir(%q) = %q, want %q", tt.input, got, tt.expected)
}
}
}