mirror of
https://github.com/Awuqing/BackupX.git
synced 2026-08-13 00:14:00 +08:00
实现 CDC 内容寻址仓库、远程 Agent 中央中转备份与首次初始化体验,并补充安全校验、测试及双语文档。 Closes #94 Closes #101 Closes #104
213 lines
6.3 KiB
Go
213 lines
6.3 KiB
Go
package rclone
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"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
|
||
}
|
||
|
||
// DownloadRange reads one slice from an object. Most object-storage backends
|
||
// map this to a native HTTP Range request. Backends that reject ranged reads
|
||
// fall back to a full stream while preserving the same interface contract.
|
||
func (p *Provider) DownloadRange(ctx context.Context, objectKey string, offset, length int64) (io.ReadCloser, error) {
|
||
if offset < 0 || length <= 0 {
|
||
return nil, fmt.Errorf("rclone download range %s: invalid offset=%d length=%d", objectKey, offset, length)
|
||
}
|
||
obj, err := p.rfs.NewObject(ctx, objectKey)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("rclone find object %s: %w", objectKey, err)
|
||
}
|
||
reader, rangeErr := obj.Open(ctx, &fs.RangeOption{Start: offset, End: offset + length - 1})
|
||
if rangeErr == nil {
|
||
return reader, nil
|
||
}
|
||
reader, err = obj.Open(ctx)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("rclone download range %s (range: %v; fallback: %w)", objectKey, rangeErr, err)
|
||
}
|
||
if offset > 0 {
|
||
if _, err := io.CopyN(io.Discard, reader, offset); err != nil {
|
||
closeErr := reader.Close()
|
||
return nil, errors.Join(fmt.Errorf("rclone seek object %s: %w", objectKey, err), closeErr)
|
||
}
|
||
}
|
||
return &limitedReadCloser{Reader: io.LimitReader(reader, length), closer: reader}, nil
|
||
}
|
||
|
||
type limitedReadCloser struct {
|
||
io.Reader
|
||
closer io.Closer
|
||
}
|
||
|
||
func (r *limitedReadCloser) Close() error {
|
||
return r.closer.Close()
|
||
}
|
||
|
||
// Delete 通过 rclone 删除远端对象。
|
||
func (p *Provider) Delete(ctx context.Context, objectKey string) error {
|
||
obj, err := p.rfs.NewObject(ctx, objectKey)
|
||
if err != nil {
|
||
if errors.Is(err, fs.ErrorObjectNotFound) || errors.Is(err, fs.ErrorDirNotFound) {
|
||
return nil
|
||
}
|
||
return fmt.Errorf("rclone find object %s: %w", objectKey, err)
|
||
}
|
||
if err := obj.Remove(ctx); err != nil {
|
||
if errors.Is(err, fs.ErrorObjectNotFound) || errors.Is(err, fs.ErrorDirNotFound) {
|
||
return 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 {
|
||
if errors.Is(err, fs.ErrorDirNotFound) || errors.Is(err, fs.ErrorObjectNotFound) {
|
||
return []storage.ObjectInfo{}, 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]
|
||
}
|