Files
BackupX/server/internal/storage/rclone/provider.go
Wu Qing 970eb154e1 优化: 多模块功能修复与体验改进 (#34)
1. 保留策略清理后自动删除空文件夹(新增 StorageDirCleaner 接口)
2. 备份任务删除时清理远端文件但保留备份记录
3. 节点管理修复:本机 IP/版本检测、Heartbeat OS/Arch 修正、新增编辑功能
4. 审计日志规范化:统一格式、丰富详情、节点操作增加审计记录
5. 系统设置移除一键更新操作,仅保留版本检查
6. Rclone 配置项分层展示(必填 + 高级可选折叠)
7. DirectoryPicker 目录选择器样式优化
2026-04-05 11:23:46 +08:00

166 lines
4.7 KiB
Go

package rclone
import (
"context"
"fmt"
"io"
"sort"
"strings"
"time"
"backupx/server/internal/storage"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/object"
"github.com/rclone/rclone/fs/walk"
)
// Provider 包装 rclone fs.Fs,实现 storage.StorageProvider 接口。
type Provider struct {
providerType storage.ProviderType
rfs fs.Fs
}
func newProvider(providerType storage.ProviderType, rfs fs.Fs) *Provider {
return &Provider{providerType: providerType, rfs: rfs}
}
func (p *Provider) Type() storage.ProviderType { return p.providerType }
// TestConnection 验证连通性。对本地磁盘会先确保目录存在。
func (p *Provider) TestConnection(ctx context.Context) error {
// 确保根目录存在(本地磁盘等后端需要预创建)
if err := p.rfs.Mkdir(ctx, ""); err != nil {
return fmt.Errorf("rclone test connection (mkdir): %w", err)
}
_, err := p.rfs.List(ctx, "")
if err != nil {
return fmt.Errorf("rclone test connection: %w", err)
}
return nil
}
// Upload 通过 rclone fs.Fs.Put 上传文件。
func (p *Provider) Upload(ctx context.Context, objectKey string, reader io.Reader, size int64, _ map[string]string) error {
dir := pathDir(objectKey)
if dir != "" && dir != "." {
if err := p.rfs.Mkdir(ctx, dir); err != nil {
return fmt.Errorf("rclone mkdir %s: %w", dir, err)
}
}
info := object.NewStaticObjectInfo(objectKey, time.Now(), size, true, nil, nil)
if _, err := p.rfs.Put(ctx, reader, info); err != nil {
return fmt.Errorf("rclone upload %s: %w", objectKey, err)
}
return nil
}
// Download 通过 rclone 获取对象并返回 io.ReadCloser。
func (p *Provider) Download(ctx context.Context, objectKey string) (io.ReadCloser, error) {
obj, err := p.rfs.NewObject(ctx, objectKey)
if err != nil {
return nil, fmt.Errorf("rclone find object %s: %w", objectKey, err)
}
reader, err := obj.Open(ctx)
if err != nil {
return nil, fmt.Errorf("rclone download %s: %w", objectKey, err)
}
return reader, nil
}
// Delete 通过 rclone 删除远端对象。
func (p *Provider) Delete(ctx context.Context, objectKey string) error {
obj, err := p.rfs.NewObject(ctx, objectKey)
if err != nil {
return fmt.Errorf("rclone find object %s: %w", objectKey, err)
}
if err := obj.Remove(ctx); err != nil {
return fmt.Errorf("rclone delete %s: %w", objectKey, err)
}
return nil
}
// List 递归列出指定前缀下的所有对象。
func (p *Provider) List(ctx context.Context, prefix string) ([]storage.ObjectInfo, error) {
var items []storage.ObjectInfo
err := walk.ListR(ctx, p.rfs, prefix, true, -1, walk.ListObjects, func(entries fs.DirEntries) error {
for _, entry := range entries {
obj, ok := entry.(fs.Object)
if !ok {
continue
}
key := obj.Remote()
if prefix != "" && !strings.HasPrefix(key, prefix) {
continue
}
items = append(items, storage.ObjectInfo{
Key: key,
Size: obj.Size(),
UpdatedAt: obj.ModTime(ctx),
})
}
return nil
})
if err != nil {
return nil, fmt.Errorf("rclone list %s: %w", prefix, err)
}
return items, nil
}
// About 查询远端存储空间。并非所有 rclone 后端都支持。
func (p *Provider) About(ctx context.Context) (*storage.StorageUsageInfo, error) {
about := p.rfs.Features().About
if about == nil {
return nil, fmt.Errorf("rclone about: backend %s does not support About", p.providerType)
}
usage, err := about(ctx)
if err != nil {
return nil, fmt.Errorf("rclone about: %w", err)
}
return &storage.StorageUsageInfo{
Total: usage.Total,
Used: usage.Used,
Free: usage.Free,
Objects: usage.Objects,
}, nil
}
// RemoveEmptyDirs 递归删除 prefix 下的空目录,从最深层开始。
// 非空目录删除会失败(安全忽略),仅清理真正的空目录。
func (p *Provider) RemoveEmptyDirs(ctx context.Context, prefix string) error {
var dirs []string
err := walk.ListR(ctx, p.rfs, prefix, true, -1, walk.ListDirs, func(entries fs.DirEntries) error {
for _, entry := range entries {
if _, ok := entry.(fs.Directory); ok {
dirs = append(dirs, entry.Remote())
}
}
return nil
})
if err != nil {
// 列目录失败(比如目录不存在)静默返回
return nil
}
// 按路径长度倒序(深目录优先删除),同长度保持稳定顺序
sort.SliceStable(dirs, func(i, j int) bool {
return len(dirs[i]) > len(dirs[j])
})
for _, dir := range dirs {
_ = p.rfs.Rmdir(ctx, dir)
}
// 尝试清理 prefix 本身
if prefix != "" {
_ = p.rfs.Rmdir(ctx, prefix)
}
return nil
}
// pathDir 返回 objectKey 的目录部分(正斜杠分隔)。
func pathDir(objectKey string) string {
idx := strings.LastIndex(objectKey, "/")
if idx < 0 {
return ""
}
return objectKey[:idx]
}