Files
BackupX/server/internal/service/storage_target_service.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

587 lines
23 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"context"
"fmt"
"net/http"
"strings"
"time"
"backupx/server/internal/apperror"
"backupx/server/internal/model"
"backupx/server/internal/repository"
"backupx/server/internal/security"
"backupx/server/internal/storage"
"backupx/server/internal/storage/codec"
"golang.org/x/oauth2"
googleoauth "golang.org/x/oauth2/google"
goauth2api "google.golang.org/api/oauth2/v2"
"google.golang.org/api/option"
)
type StorageTargetUpsertInput struct {
Name string `json:"name" binding:"required,min=1,max=128"`
Type string `json:"type" binding:"required,oneof=local_disk google_drive s3 webdav"`
Description string `json:"description" binding:"max=255"`
Enabled bool `json:"enabled"`
Config map[string]any `json:"config" binding:"required"`
}
type StorageTargetTestInput struct {
TargetID *uint `json:"targetId"`
Payload StorageTargetUpsertInput `json:"payload"`
}
type GoogleDriveAuthStartInput struct {
TargetID *uint `json:"targetId"`
Name string `json:"name" binding:"required,min=1,max=128"`
Description string `json:"description" binding:"max=255"`
Enabled bool `json:"enabled"`
ClientID string `json:"clientId" binding:"required"`
ClientSecret string `json:"clientSecret" binding:"required"`
FolderID string `json:"folderId"`
}
type GoogleDriveAuthCompleteInput struct {
State string `json:"state" binding:"required"`
Code string `json:"code" binding:"required"`
}
type StorageTargetSummary struct {
ID uint `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Description string `json:"description"`
Enabled bool `json:"enabled"`
Starred bool `json:"starred"`
ConfigVersion int `json:"configVersion"`
LastTestedAt *time.Time `json:"lastTestedAt"`
LastTestStatus string `json:"lastTestStatus"`
LastTestMessage string `json:"lastTestMessage"`
UpdatedAt time.Time `json:"updatedAt"`
}
type StorageTargetDetail struct {
StorageTargetSummary
Config map[string]any `json:"config"`
MaskedFields []string `json:"maskedFields,omitempty"`
}
type GoogleDriveAuthStartResult struct {
AuthorizationURL string `json:"authorizationUrl"`
State string `json:"state"`
ExpiresAt time.Time `json:"expiresAt"`
}
type googleDriveOAuthDraft struct {
TargetID *uint `json:"targetId"`
Name string `json:"name"`
Description string `json:"description"`
Enabled bool `json:"enabled"`
ClientID string `json:"clientId"`
ClientSecret string `json:"clientSecret"`
FolderID string `json:"folderId"`
RedirectURI string `json:"redirectUri"`
}
type StorageTargetService struct {
targets repository.StorageTargetRepository
oauthSessions repository.OAuthSessionRepository
backupTasks repository.BackupTaskRepository
records repository.BackupRecordRepository
registry *storage.Registry
cipher *codec.ConfigCipher
}
func NewStorageTargetService(
targets repository.StorageTargetRepository,
oauthSessions repository.OAuthSessionRepository,
registry *storage.Registry,
cipher *codec.ConfigCipher,
) *StorageTargetService {
return &StorageTargetService{targets: targets, oauthSessions: oauthSessions, registry: registry, cipher: cipher}
}
func (s *StorageTargetService) SetBackupTaskRepository(tasks repository.BackupTaskRepository) {
s.backupTasks = tasks
}
func (s *StorageTargetService) SetBackupRecordRepository(records repository.BackupRecordRepository) {
s.records = records
}
func (s *StorageTargetService) List(ctx context.Context) ([]StorageTargetSummary, error) {
items, err := s.targets.List(ctx)
if err != nil {
return nil, apperror.Internal("STORAGE_TARGET_LIST_FAILED", "无法获取存储目标列表", err)
}
result := make([]StorageTargetSummary, 0, len(items))
for _, item := range items {
result = append(result, toStorageTargetSummary(&item))
}
return result, nil
}
func (s *StorageTargetService) Get(ctx context.Context, id uint) (*StorageTargetDetail, error) {
item, err := s.targets.FindByID(ctx, id)
if err != nil {
return nil, apperror.Internal("STORAGE_TARGET_GET_FAILED", "无法获取存储目标详情", err)
}
if item == nil {
return nil, apperror.New(http.StatusNotFound, "STORAGE_TARGET_NOT_FOUND", "存储目标不存在", fmt.Errorf("storage target %d not found", id))
}
configMap, err := s.decryptTargetConfig(item)
if err != nil {
return nil, apperror.Internal("STORAGE_TARGET_DECRYPT_FAILED", "无法解密存储目标配置", err)
}
sensitiveFields := s.registry.SensitiveFields(storage.ParseProviderType(item.Type))
return &StorageTargetDetail{StorageTargetSummary: toStorageTargetSummary(item), Config: codec.MaskConfig(configMap, sensitiveFields), MaskedFields: sensitiveFields}, nil
}
func (s *StorageTargetService) Create(ctx context.Context, input StorageTargetUpsertInput) (*StorageTargetDetail, error) {
if err := s.validateType(input.Type); err != nil {
return nil, err
}
existing, err := s.targets.FindByName(ctx, strings.TrimSpace(input.Name))
if err != nil {
return nil, apperror.Internal("STORAGE_TARGET_LOOKUP_FAILED", "无法检查存储目标名称", err)
}
if existing != nil {
return nil, apperror.Conflict("STORAGE_TARGET_NAME_EXISTS", "存储目标名称已存在", nil)
}
item, err := s.buildStorageTarget(ctx, nil, input)
if err != nil {
return nil, err
}
if err := s.targets.Create(ctx, item); err != nil {
return nil, apperror.Internal("STORAGE_TARGET_CREATE_FAILED", "无法创建存储目标", err)
}
return s.Get(ctx, item.ID)
}
func (s *StorageTargetService) Update(ctx context.Context, id uint, input StorageTargetUpsertInput) (*StorageTargetDetail, error) {
if err := s.validateType(input.Type); err != nil {
return nil, err
}
existing, err := s.targets.FindByID(ctx, id)
if err != nil {
return nil, apperror.Internal("STORAGE_TARGET_GET_FAILED", "无法获取存储目标详情", err)
}
if existing == nil {
return nil, apperror.New(http.StatusNotFound, "STORAGE_TARGET_NOT_FOUND", "存储目标不存在", fmt.Errorf("storage target %d not found", id))
}
if sameName, err := s.targets.FindByName(ctx, strings.TrimSpace(input.Name)); err != nil {
return nil, apperror.Internal("STORAGE_TARGET_LOOKUP_FAILED", "无法检查存储目标名称", err)
} else if sameName != nil && sameName.ID != existing.ID {
return nil, apperror.Conflict("STORAGE_TARGET_NAME_EXISTS", "存储目标名称已存在", nil)
}
item, err := s.buildStorageTarget(ctx, existing, input)
if err != nil {
return nil, err
}
item.ID = existing.ID
item.CreatedAt = existing.CreatedAt
if err := s.targets.Update(ctx, item); err != nil {
return nil, apperror.Internal("STORAGE_TARGET_UPDATE_FAILED", "无法更新存储目标", err)
}
return s.Get(ctx, item.ID)
}
func (s *StorageTargetService) Delete(ctx context.Context, id uint) error {
existing, err := s.targets.FindByID(ctx, id)
if err != nil {
return apperror.Internal("STORAGE_TARGET_GET_FAILED", "无法获取存储目标详情", err)
}
if existing == nil {
return apperror.New(http.StatusNotFound, "STORAGE_TARGET_NOT_FOUND", "存储目标不存在", fmt.Errorf("storage target %d not found", id))
}
if s.backupTasks != nil {
count, countErr := s.backupTasks.CountByStorageTargetID(ctx, id)
if countErr != nil {
return apperror.Internal("STORAGE_TARGET_REF_CHECK_FAILED", "无法检查存储目标引用关系", countErr)
}
if count > 0 {
return apperror.Conflict("STORAGE_TARGET_IN_USE", "当前存储目标已被备份任务引用,无法删除", nil)
}
}
if err := s.targets.Delete(ctx, id); err != nil {
return apperror.Internal("STORAGE_TARGET_DELETE_FAILED", "无法删除存储目标", err)
}
return nil
}
func (s *StorageTargetService) ToggleStar(ctx context.Context, id uint) (*StorageTargetSummary, error) {
item, err := s.targets.FindByID(ctx, id)
if err != nil {
return nil, apperror.Internal("STORAGE_TARGET_GET_FAILED", "无法获取存储目标详情", err)
}
if item == nil {
return nil, apperror.New(http.StatusNotFound, "STORAGE_TARGET_NOT_FOUND", "存储目标不存在", fmt.Errorf("storage target %d not found", id))
}
item.Starred = !item.Starred
if err := s.targets.Update(ctx, item); err != nil {
return nil, apperror.Internal("STORAGE_TARGET_UPDATE_FAILED", "无法更新存储目标收藏状态", err)
}
summary := toStorageTargetSummary(item)
return &summary, nil
}
func (s *StorageTargetService) TestConnection(ctx context.Context, input StorageTargetTestInput) error {
item, err := s.buildStorageTargetForTest(ctx, input)
if err != nil {
return err
}
configMap, err := s.decryptTargetConfig(item)
if err != nil {
return apperror.Internal("STORAGE_TARGET_DECRYPT_FAILED", "无法解密存储目标配置", err)
}
provider, err := s.registry.Create(ctx, storage.ParseProviderType(item.Type), configMap)
if err != nil {
return apperror.BadRequest("STORAGE_TARGET_INVALID_CONFIG", sanitizeMessage(err.Error()), err)
}
testErr := provider.TestConnection(ctx)
now := time.Now().UTC()
item.LastTestedAt = &now
if testErr != nil {
item.LastTestStatus = "failed"
item.LastTestMessage = sanitizeMessage(testErr.Error())
} else {
item.LastTestStatus = "success"
item.LastTestMessage = "连接成功"
}
if item.ID != 0 {
_ = s.targets.Update(ctx, item)
}
if testErr != nil {
return apperror.BadRequest("STORAGE_TARGET_TEST_FAILED", sanitizeMessage(testErr.Error()), testErr)
}
return nil
}
func (s *StorageTargetService) StartGoogleDriveOAuth(ctx context.Context, input GoogleDriveAuthStartInput, origin string) (*GoogleDriveAuthStartResult, error) {
origin = normalizeOrigin(origin)
if origin == "" {
return nil, apperror.BadRequest("STORAGE_GOOGLE_OAUTH_ORIGIN_REQUIRED", "无法确定 Google Drive 回调地址", nil)
}
draft, err := s.buildGoogleDriveDraft(ctx, input, origin)
if err != nil {
return nil, err
}
payload, err := s.cipher.EncryptJSON(draft)
if err != nil {
return nil, apperror.Internal("STORAGE_GOOGLE_OAUTH_ENCRYPT_FAILED", "无法创建授权会话", err)
}
state, err := security.GenerateSecret(24)
if err != nil {
return nil, apperror.Internal("STORAGE_GOOGLE_OAUTH_STATE_FAILED", "无法生成授权状态", err)
}
expiresAt := time.Now().UTC().Add(10 * time.Minute)
session := &model.OAuthSession{ProviderType: storage.TypeGoogleDrive, State: state, PayloadCiphertext: payload, TargetID: input.TargetID, ExpiresAt: expiresAt}
if err := s.oauthSessions.Create(ctx, session); err != nil {
return nil, apperror.Internal("STORAGE_GOOGLE_OAUTH_SESSION_FAILED", "无法创建授权会话", err)
}
oauthCfg := &oauth2.Config{ClientID: draft.ClientID, ClientSecret: draft.ClientSecret, RedirectURL: draft.RedirectURI, Endpoint: googleoauth.Endpoint, Scopes: []string{"https://www.googleapis.com/auth/drive"}}
url := oauthCfg.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.SetAuthURLParam("prompt", "consent"))
return &GoogleDriveAuthStartResult{AuthorizationURL: url, State: state, ExpiresAt: expiresAt}, nil
}
func (s *StorageTargetService) CompleteGoogleDriveOAuth(ctx context.Context, input GoogleDriveAuthCompleteInput) (*StorageTargetDetail, error) {
session, err := s.oauthSessions.FindByState(ctx, strings.TrimSpace(input.State))
if err != nil {
return nil, apperror.Internal("STORAGE_GOOGLE_OAUTH_SESSION_FAILED", "无法读取授权会话", err)
}
if session == nil || session.UsedAt != nil || time.Now().UTC().After(session.ExpiresAt) {
return nil, apperror.BadRequest("STORAGE_GOOGLE_OAUTH_STATE_INVALID", "Google Drive 授权状态无效或已过期", nil)
}
// Mark used immediately to prevent duplicate requests (e.g. React StrictMode double invocation)
now := time.Now().UTC()
session.UsedAt = &now
_ = s.oauthSessions.Update(ctx, session)
var draft googleDriveOAuthDraft
if err := s.cipher.DecryptJSON(session.PayloadCiphertext, &draft); err != nil {
return nil, apperror.Internal("STORAGE_GOOGLE_OAUTH_DECRYPT_FAILED", "无法读取授权会话内容", err)
}
oauthCfg := &oauth2.Config{ClientID: draft.ClientID, ClientSecret: draft.ClientSecret, RedirectURL: draft.RedirectURI, Endpoint: googleoauth.Endpoint, Scopes: []string{"https://www.googleapis.com/auth/drive"}}
token, err := oauthCfg.Exchange(ctx, strings.TrimSpace(input.Code))
if err != nil {
return nil, apperror.BadRequest("STORAGE_GOOGLE_OAUTH_EXCHANGE_FAILED", "Google Drive 授权码换取失败", err)
}
if strings.TrimSpace(token.RefreshToken) == "" {
return nil, apperror.BadRequest("STORAGE_GOOGLE_OAUTH_REFRESH_TOKEN_MISSING", "未获取到 Google Drive refresh token请重新授权", nil)
}
configMap := map[string]any{
"clientId": draft.ClientID,
"clientSecret": draft.ClientSecret,
"refreshToken": token.RefreshToken,
"folderId": draft.FolderID,
"redirectUri": draft.RedirectURI,
}
payload := StorageTargetUpsertInput{Name: draft.Name, Type: storage.TypeGoogleDrive, Description: draft.Description, Enabled: draft.Enabled, Config: configMap}
var detail *StorageTargetDetail
if session.TargetID != nil {
detail, err = s.Update(ctx, *session.TargetID, payload)
} else {
detail, err = s.Create(ctx, payload)
}
if err != nil {
return nil, err
}
return detail, nil
}
func (s *StorageTargetService) GoogleDriveProfile(ctx context.Context, id uint) (map[string]any, error) {
detail, err := s.Get(ctx, id)
if err != nil {
return nil, err
}
if detail.Type != storage.TypeGoogleDrive {
return nil, apperror.BadRequest("STORAGE_GOOGLE_DRIVE_TYPE_MISMATCH", "目标不是 Google Drive 存储类型", nil)
}
stored, err := s.targets.FindByID(ctx, id)
if err != nil || stored == nil {
return nil, apperror.New(http.StatusNotFound, "STORAGE_TARGET_NOT_FOUND", "存储目标不存在", err)
}
var cfg storage.GoogleDriveConfig
if err := s.cipher.DecryptJSON(stored.ConfigCiphertext, &cfg); err != nil {
return nil, apperror.Internal("STORAGE_TARGET_DECRYPT_FAILED", "无法解密存储目标配置", err)
}
cfg = cfg.Normalize()
oauthCfg := &oauth2.Config{ClientID: cfg.ClientID, ClientSecret: cfg.ClientSecret, Endpoint: googleoauth.Endpoint, RedirectURL: cfg.RedirectURL, Scopes: []string{"https://www.googleapis.com/auth/drive"}}
tokenSource := oauthCfg.TokenSource(ctx, &oauth2.Token{RefreshToken: cfg.RefreshToken, Expiry: time.Now().Add(-time.Hour)})
client, err := goauth2api.NewService(ctx, option.WithTokenSource(tokenSource))
if err != nil {
return nil, apperror.BadRequest("STORAGE_GOOGLE_PROFILE_FAILED", "无法获取 Google Drive 用户信息", err)
}
userInfo, err := client.Userinfo.Get().Do()
if err != nil {
return nil, apperror.BadRequest("STORAGE_GOOGLE_PROFILE_FAILED", "无法获取 Google Drive 用户信息", err)
}
return map[string]any{"email": userInfo.Email, "name": userInfo.Name, "picture": userInfo.Picture}, nil
}
func (s *StorageTargetService) buildStorageTargetForTest(ctx context.Context, input StorageTargetTestInput) (*model.StorageTarget, error) {
if input.TargetID == nil {
return s.buildStorageTarget(ctx, nil, input.Payload)
}
existing, err := s.targets.FindByID(ctx, *input.TargetID)
if err != nil {
return nil, apperror.Internal("STORAGE_TARGET_GET_FAILED", "无法获取存储目标详情", err)
}
if existing == nil {
return nil, apperror.New(http.StatusNotFound, "STORAGE_TARGET_NOT_FOUND", "存储目标不存在", fmt.Errorf("storage target %d not found", *input.TargetID))
}
if strings.TrimSpace(input.Payload.Type) == "" && strings.TrimSpace(input.Payload.Name) == "" && len(input.Payload.Config) == 0 {
return existing, nil
}
item, err := s.buildStorageTarget(ctx, existing, input.Payload)
if err != nil {
return nil, err
}
item.ID = existing.ID
item.LastTestedAt = existing.LastTestedAt
item.LastTestStatus = existing.LastTestStatus
item.LastTestMessage = existing.LastTestMessage
return item, nil
}
func (s *StorageTargetService) buildStorageTarget(ctx context.Context, existing *model.StorageTarget, input StorageTargetUpsertInput) (*model.StorageTarget, error) {
configMap, err := s.prepareConfig(ctx, existing, input)
if err != nil {
return nil, err
}
ciphertext, err := s.cipher.EncryptJSON(configMap)
if err != nil {
return nil, apperror.Internal("STORAGE_TARGET_ENCRYPT_FAILED", "无法保存存储目标配置", err)
}
item := &model.StorageTarget{
Name: strings.TrimSpace(input.Name),
Type: input.Type,
Description: strings.TrimSpace(input.Description),
Enabled: input.Enabled,
ConfigCiphertext: ciphertext,
ConfigVersion: 1,
LastTestStatus: "unknown",
}
if existing != nil {
item.LastTestedAt = existing.LastTestedAt
item.LastTestStatus = existing.LastTestStatus
item.LastTestMessage = existing.LastTestMessage
if existing.Type == input.Type {
item.ConfigVersion = existing.ConfigVersion
}
}
return item, nil
}
func (s *StorageTargetService) prepareConfig(ctx context.Context, existing *model.StorageTarget, input StorageTargetUpsertInput) (map[string]any, error) {
if err := s.validateType(input.Type); err != nil {
return nil, err
}
configMap := cloneMap(input.Config)
if existing != nil {
if existing.Type != input.Type {
return nil, apperror.BadRequest("STORAGE_TARGET_TYPE_IMMUTABLE", "不支持直接修改存储目标类型", nil)
}
existingMap, err := s.decryptTargetConfig(existing)
if err != nil {
return nil, apperror.Internal("STORAGE_TARGET_DECRYPT_FAILED", "无法读取现有存储目标配置", err)
}
configMap = codec.MergeMaskedConfig(configMap, existingMap, s.registry.SensitiveFields(storage.ParseProviderType(input.Type)))
}
if _, err := s.registry.Create(ctx, storage.ParseProviderType(input.Type), configMap); err != nil {
return nil, apperror.BadRequest("STORAGE_TARGET_INVALID_CONFIG", sanitizeMessage(err.Error()), err)
}
return configMap, nil
}
func (s *StorageTargetService) decryptTargetConfig(item *model.StorageTarget) (map[string]any, error) {
var configMap map[string]any
if err := s.cipher.DecryptJSON(item.ConfigCiphertext, &configMap); err != nil {
return nil, err
}
return configMap, nil
}
func (s *StorageTargetService) buildGoogleDriveDraft(ctx context.Context, input GoogleDriveAuthStartInput, origin string) (*googleDriveOAuthDraft, error) {
draft := &googleDriveOAuthDraft{
TargetID: input.TargetID,
Name: strings.TrimSpace(input.Name),
Description: strings.TrimSpace(input.Description),
Enabled: input.Enabled,
ClientID: strings.TrimSpace(input.ClientID),
ClientSecret: strings.TrimSpace(input.ClientSecret),
FolderID: strings.TrimSpace(input.FolderID),
RedirectURI: strings.TrimRight(origin, "/") + "/storage-targets/google-drive/callback",
}
if input.TargetID == nil {
if draft.Name == "" || draft.ClientID == "" || draft.ClientSecret == "" {
return nil, apperror.BadRequest("STORAGE_GOOGLE_OAUTH_INVALID", "Google Drive 授权参数不完整", nil)
}
return draft, nil
}
existing, err := s.targets.FindByID(ctx, *input.TargetID)
if err != nil {
return nil, apperror.Internal("STORAGE_TARGET_GET_FAILED", "无法获取存储目标详情", err)
}
if existing == nil {
return nil, apperror.New(http.StatusNotFound, "STORAGE_TARGET_NOT_FOUND", "存储目标不存在", fmt.Errorf("storage target %d not found", *input.TargetID))
}
if existing.Type != storage.TypeGoogleDrive {
return nil, apperror.BadRequest("STORAGE_GOOGLE_DRIVE_TYPE_MISMATCH", "目标不是 Google Drive 存储类型", nil)
}
var cfg storage.GoogleDriveConfig
if err := s.cipher.DecryptJSON(existing.ConfigCiphertext, &cfg); err != nil {
return nil, apperror.Internal("STORAGE_TARGET_DECRYPT_FAILED", "无法解密存储目标配置", err)
}
cfg = cfg.Normalize()
if draft.Name == "" {
draft.Name = existing.Name
}
if draft.Description == "" {
draft.Description = existing.Description
}
if draft.ClientID == "" || codec.IsMaskedString(draft.ClientID) {
draft.ClientID = cfg.ClientID
}
if draft.ClientSecret == "" || codec.IsMaskedString(draft.ClientSecret) {
draft.ClientSecret = cfg.ClientSecret
}
if draft.FolderID == "" {
draft.FolderID = cfg.FolderID
}
if draft.Name == "" || draft.ClientID == "" || draft.ClientSecret == "" {
return nil, apperror.BadRequest("STORAGE_GOOGLE_OAUTH_INVALID", "Google Drive 授权参数不完整", nil)
}
return draft, nil
}
func (s *StorageTargetService) validateType(providerType string) error {
if _, ok := s.registry.Factory(storage.ParseProviderType(providerType)); !ok {
return apperror.BadRequest("STORAGE_PROVIDER_UNSUPPORTED", "不支持的存储类型", fmt.Errorf("provider %s not found", providerType))
}
return nil
}
func toStorageTargetSummary(item *model.StorageTarget) StorageTargetSummary {
return StorageTargetSummary{
ID: item.ID,
Name: item.Name,
Type: item.Type,
Description: item.Description,
Enabled: item.Enabled,
Starred: item.Starred,
ConfigVersion: item.ConfigVersion,
LastTestedAt: item.LastTestedAt,
LastTestStatus: item.LastTestStatus,
LastTestMessage: item.LastTestMessage,
UpdatedAt: item.UpdatedAt,
}
}
func sanitizeMessage(message string) string {
message = strings.TrimSpace(message)
if message == "" {
return "操作失败"
}
if len(message) > 255 {
return message[:255]
}
return message
}
func normalizeOrigin(origin string) string {
origin = strings.TrimSpace(origin)
return strings.TrimRight(origin, "/")
}
func cloneMap(source map[string]any) map[string]any {
result := make(map[string]any, len(source))
for key, value := range source {
result[key] = value
}
return result
}
type StorageTargetUsage struct {
TargetID uint `json:"targetId"`
TargetName string `json:"targetName"`
RecordCount int64 `json:"recordCount"`
TotalSize int64 `json:"totalSize"`
DiskUsage *storage.StorageUsageInfo `json:"diskUsage,omitempty"`
}
func (s *StorageTargetService) GetUsage(ctx context.Context, id uint) (*StorageTargetUsage, error) {
target, err := s.targets.FindByID(ctx, id)
if err != nil {
return nil, apperror.Internal("STORAGE_TARGET_GET_FAILED", "无法获取存储目标详情", err)
}
if target == nil {
return nil, apperror.New(http.StatusNotFound, "STORAGE_TARGET_NOT_FOUND", "存储目标不存在", fmt.Errorf("storage target %d not found", id))
}
result := &StorageTargetUsage{TargetID: id, TargetName: target.Name}
if s.records != nil {
usageItems, usageErr := s.records.StorageUsage(ctx)
if usageErr == nil {
for _, item := range usageItems {
if item.StorageTargetID == id {
result.TotalSize = item.TotalSize
break
}
}
}
}
// 尝试查询远端真实存储空间(部分后端如 local/Google Drive/WebDAV 支持)
configMap := map[string]any{}
if decryptErr := s.cipher.DecryptJSON(target.ConfigCiphertext, &configMap); decryptErr == nil {
if provider, createErr := s.registry.Create(ctx, target.Type, configMap); createErr == nil {
if abouter, ok := provider.(storage.StorageAbout); ok {
if diskUsage, aboutErr := abouter.About(ctx); aboutErr == nil {
result.DiskUsage = diskUsage
}
}
}
}
return result, nil
}