mirror of
https://github.com/Awuqing/BackupX.git
synced 2026-08-28 11:37:04 +08:00
feat(backup): 新增备份内容浏览 (#90)
查看每次备份捕获的文件清单(路径/大小/目录),核对完整性、排查遗漏。清单取自全量备份记录,无需下载解压;差异记录回退基线清单。只读端点 + 前端可筛选弹窗。
This commit is contained in:
@@ -52,6 +52,20 @@ func (h *BackupRecordHandler) Get(c *gin.Context) {
|
||||
response.Success(c, item)
|
||||
}
|
||||
|
||||
// Contents 返回备份记录的文件清单(内容浏览,只读)。
|
||||
func (h *BackupRecordHandler) Contents(c *gin.Context) {
|
||||
id, ok := parseUintParam(c, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
contents, err := h.service.ListContents(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
response.Error(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, contents)
|
||||
}
|
||||
|
||||
func (h *BackupRecordHandler) StreamLogs(c *gin.Context) {
|
||||
id, ok := parseUintParam(c, "id")
|
||||
if !ok {
|
||||
|
||||
@@ -167,6 +167,7 @@ func NewRouter(deps RouterDependencies) *gin.Engine {
|
||||
backupRecords.GET("/:id", backupRecordHandler.Get)
|
||||
backupRecords.GET("/:id/logs/stream", backupRecordHandler.StreamLogs)
|
||||
backupRecords.GET("/:id/download", backupRecordHandler.Download)
|
||||
backupRecords.GET("/:id/contents", backupRecordHandler.Contents)
|
||||
backupRecords.POST("/:id/restore", RequireNotViewer(), backupRecordHandler.Restore)
|
||||
backupRecords.POST("/batch-delete", RequireNotViewer(), backupRecordHandler.BatchDelete)
|
||||
backupRecords.DELETE("/:id", RequireNotViewer(), backupRecordHandler.Delete)
|
||||
|
||||
@@ -3,6 +3,7 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -80,6 +81,64 @@ func (s *BackupRecordService) Get(ctx context.Context, id uint) (*BackupRecordDe
|
||||
return toBackupRecordDetail(item, s.logHub), nil
|
||||
}
|
||||
|
||||
// BackupContentEntry 描述备份内单个条目(文件或目录),用于内容浏览。
|
||||
type BackupContentEntry struct {
|
||||
Path string `json:"path"`
|
||||
Size int64 `json:"size"`
|
||||
IsDir bool `json:"isDir"`
|
||||
}
|
||||
|
||||
// BackupRecordContents 是一次备份的内容清单视图。
|
||||
type BackupRecordContents struct {
|
||||
RecordID uint `json:"recordId"`
|
||||
Total int `json:"total"`
|
||||
Truncated bool `json:"truncated"`
|
||||
BasedOnFull uint `json:"basedOnFull,omitempty"` // 差异记录时,清单取自该基线全量
|
||||
Entries []BackupContentEntry `json:"entries"`
|
||||
}
|
||||
|
||||
const backupContentsMaxEntries = 10000
|
||||
|
||||
// ListContents 返回某备份记录的文件清单(仅文件类型的新全量备份会记录清单)。
|
||||
// 差异记录回退到其基线全量的清单,近似展示恢复后的目录结构。无清单时返回明确错误。
|
||||
func (s *BackupRecordService) ListContents(ctx context.Context, id uint) (*BackupRecordContents, error) {
|
||||
item, err := s.records.FindByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, apperror.Internal("BACKUP_RECORD_GET_FAILED", "无法获取备份记录", err)
|
||||
}
|
||||
if item == nil {
|
||||
return nil, apperror.New(404, "BACKUP_RECORD_NOT_FOUND", "备份记录不存在", nil)
|
||||
}
|
||||
manifestJSON := item.Manifest
|
||||
basedOnFull := uint(0)
|
||||
if strings.TrimSpace(manifestJSON) == "" && item.BaseRecordID != 0 {
|
||||
if base, baseErr := s.records.FindByID(ctx, item.BaseRecordID); baseErr == nil && base != nil {
|
||||
manifestJSON = base.Manifest
|
||||
basedOnFull = base.ID
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(manifestJSON) == "" {
|
||||
return nil, apperror.New(422, "BACKUP_CONTENTS_UNAVAILABLE", "该备份未记录文件清单(仅文件类型的新全量备份支持内容浏览),请重新执行一次全量备份后再试。", nil)
|
||||
}
|
||||
manifest, decErr := backup.DecodeManifest([]byte(manifestJSON))
|
||||
if decErr != nil {
|
||||
return nil, apperror.Internal("BACKUP_CONTENTS_DECODE_FAILED", "解析备份清单失败", decErr)
|
||||
}
|
||||
entries := manifest.Entries
|
||||
sort.Slice(entries, func(i, j int) bool { return entries[i].Path < entries[j].Path })
|
||||
total := len(entries)
|
||||
truncated := false
|
||||
if total > backupContentsMaxEntries {
|
||||
entries = entries[:backupContentsMaxEntries]
|
||||
truncated = true
|
||||
}
|
||||
result := &BackupRecordContents{RecordID: item.ID, Total: total, Truncated: truncated, BasedOnFull: basedOnFull, Entries: make([]BackupContentEntry, 0, len(entries))}
|
||||
for _, e := range entries {
|
||||
result.Entries = append(result.Entries, BackupContentEntry{Path: e.Path, Size: e.Size, IsDir: e.IsDir})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *BackupRecordService) SubscribeLogs(ctx context.Context, id uint, buffer int) (<-chan backup.LogEvent, func(), error) {
|
||||
item, err := s.records.FindByID(ctx, id)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user