Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@

<table>
<tr>
<td width="50%"><img src="screenshots/dashboard.png" alt="Dashboard"></td>
<td width="50%"><img src="screenshots/backup-tasks.png" alt="Backup Tasks"></td>
<td width="50%"><img src="screenshots/dashboard.png" alt="BackupX dashboard with 30-day backup trends, storage distribution, and task health"><br><sub><strong>Dashboard</strong> — 30-day success and failure trends, storage distribution, task volume, and cluster health.</sub></td>
<td width="50%"><img src="screenshots/backup-tasks.png" alt="BackupX task list with schedules, targets, retention, tags, RPO, and verification status"><br><sub><strong>Backup tasks</strong> — schedules, multi-target policies, retention, tags, RPO goals, and recurring verification.</sub></td>
</tr>
<tr>
<td><img src="screenshots/storage-targets.png" alt="Storage Targets"></td>
<td><img src="screenshots/backup-records.png" alt="Backup Records"></td>
<td><img src="screenshots/storage-targets.png" alt="BackupX storage targets with connection health, capacity usage, and redundancy roles"><br><sub><strong>Storage targets</strong> — connection health, live capacity, favourites, and redundancy roles at a glance.</sub></td>
<td><img src="screenshots/backup-records.png" alt="BackupX backup history with success and failure states, checksums, destinations, and retention locks"><br><sub><strong>Backup records</strong> — success and failure states, checksums, destinations, execution logs, and retention locks.</sub></td>
</tr>
</table>

Expand Down
8 changes: 4 additions & 4 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@

<table>
<tr>
<td width="50%"><img src="screenshots/dashboard.png" alt="仪表盘"></td>
<td width="50%"><img src="screenshots/backup-tasks.png" alt="备份任务"></td>
<td width="50%"><img src="screenshots/dashboard.png" alt="BackupX 仪表盘,展示 30 天备份趋势、存储分布和任务健康度"><br><sub><strong>仪表盘</strong> — 30 天成功/失败趋势、存储分布、任务规模与集群健康度。</sub></td>
<td width="50%"><img src="screenshots/backup-tasks.png" alt="BackupX 备份任务列表,展示调度、存储目标、保留策略、标签、RPO 和验证状态"><br><sub><strong>备份任务</strong> — 调度、双目标策略、保留规则、标签、RPO 与定时验证。</sub></td>
</tr>
<tr>
<td><img src="screenshots/storage-targets.png" alt="存储目标"></td>
<td><img src="screenshots/backup-records.png" alt="备份记录"></td>
<td><img src="screenshots/storage-targets.png" alt="BackupX 存储目标,展示连接状态、容量使用和冗余角色"><br><sub><strong>存储目标</strong> — 连接状态、实时容量、收藏及冗余角色一目了然。</sub></td>
<td><img src="screenshots/backup-records.png" alt="BackupX 备份历史,展示成功失败状态、校验和、存储位置及保留锁"><br><sub><strong>备份记录</strong> — 成功/失败状态、校验和、落盘位置、执行日志与保留锁定。</sub></td>
</tr>
</table>

Expand Down
24 changes: 12 additions & 12 deletions docs-site/i18n/zh-CN/code.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,18 @@
"showcase.tab.tasks": {"message": "备份任务"},
"showcase.tab.storage": {"message": "存储目标"},
"showcase.tab.nodes": {"message": "多节点"},
"showcase.dashboard.alt": {"message": "显示备份健康和存储使用情况的 BackupX 仪表盘"},
"showcase.dashboard.title": {"message": "一眼掌握全局"},
"showcase.dashboard.desc": {"message": "备份成功率、存储使用量、最近执行记录和即将触发的计划集中显示在一个实时页面。"},
"showcase.tasks.alt": {"message": "BackupX 备份任务管理界面"},
"showcase.tasks.title": {"message": "可视化任务编辑器"},
"showcase.tasks.desc": {"message": "通过三步向导配置文件、MySQL、PostgreSQL、SQLite 和 SAP HANA,并绑定调度、多目标、保留、压缩和加密。"},
"showcase.storage.alt": {"message": "BackupX 存储目标管理界面"},
"showcase.storage.title": {"message": "多种后端,统一流程"},
"showcase.storage.desc": {"message": "以一致表单管理阿里云 OSS、腾讯云 COS、S3、Google Drive、WebDAV 及 rclone 后端,并测试连接和查看容量。"},
"showcase.nodes.alt": {"message": "BackupX 远程节点管理界面"},
"showcase.nodes.title": {"message": "快速搭建 Master-Agent"},
"showcase.nodes.desc": {"message": "创建节点、复制令牌并启动远程 Agent。任务在节点本地执行并直接上传存储,无需反向网络连通。"},
"showcase.dashboard.alt": {"message": "BackupX 仪表盘,展示 30 天备份趋势、存储分布、任务规模和集群健康度"},
"showcase.dashboard.title": {"message": "一眼掌握运行态"},
"showcase.dashboard.desc": {"message": "在一个实时运维视图中掌握 30 天成功/失败趋势、存储分布、任务数量、数据量和最近执行记录。"},
"showcase.tasks.alt": {"message": "BackupX 任务列表,展示调度、存储目标、保留策略、标签、RPO 目标和验证状态"},
"showcase.tasks.title": {"message": "策略状态清晰可扫"},
"showcase.tasks.desc": {"message": "集中查看调度、多目标策略、保留规则、标签、RPO 目标和验证状态,并可一键操作任意任务。"},
"showcase.storage.alt": {"message": "BackupX 存储目标,展示连接状态、实时容量、收藏和冗余角色"},
"showcase.storage.title": {"message": "每个目标,统一视图"},
"showcase.storage.desc": {"message": "统一比较本地磁盘与 70+ 远程后端的连接状态、实时容量、收藏及冗余角色。"},
"showcase.nodes.alt": {"message": "BackupX 节点列表,展示健康度、Agent 版本、队列深度、标签和心跳时间"},
"showcase.nodes.title": {"message": "集群健康一屏掌握"},
"showcase.nodes.desc": {"message": "集中监控本地 Master 与每个远程节点的健康度、Agent 版本、队列深度、标签和心跳时间。"},
"showcase.cta": {"message": "开始阅读文档"},

"community.tag": {"message": "社区"},
Expand Down
36 changes: 24 additions & 12 deletions docs-site/src/components/HomepageShowcase/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,14 @@ function useTabs(): Tab[] {
label: <Translate id="showcase.tab.dashboard">Dashboard</Translate>,
icon: 'monitor',
image: useBaseUrl('/img/screenshots/dashboard.png'),
imageAlt: translate({id: 'showcase.dashboard.alt', message: 'BackupX dashboard showing backup health and storage usage'}),
title: <Translate id="showcase.dashboard.title">Know at a glance</Translate>,
imageAlt: translate({
id: 'showcase.dashboard.alt',
message: 'BackupX dashboard showing 30-day backup trends, storage distribution, task volume and cluster health',
}),
title: <Translate id="showcase.dashboard.title">Operations at a glance</Translate>,
description: (
<Translate id="showcase.dashboard.desc">
Backup success rates, storage usage, recent runs and upcoming schedules — all on one page with live data.
Track 30-day success and failure trends, storage distribution, task counts, data volume and recent runs from one live operations view.
</Translate>
),
},
Expand All @@ -38,11 +41,14 @@ function useTabs(): Tab[] {
label: <Translate id="showcase.tab.tasks">Backup Tasks</Translate>,
icon: 'database',
image: useBaseUrl('/img/screenshots/backup-tasks.png'),
imageAlt: translate({id: 'showcase.tasks.alt', message: 'BackupX backup task management screen'}),
title: <Translate id="showcase.tasks.title">Visual task editor</Translate>,
imageAlt: translate({
id: 'showcase.tasks.alt',
message: 'BackupX task list showing schedules, storage targets, retention, tags, RPO goals and verification status',
}),
title: <Translate id="showcase.tasks.title">Policies you can scan</Translate>,
description: (
<Translate id="showcase.tasks.desc">
Files, MySQL, PostgreSQL, SQLite and SAP HANA with a three-step wizard. Cron editor, multi-target dispatch, retention, compression and encryption — point and click.
Review schedules, multi-target policies, retention, tags, RPO goals and verification status together, then act on any task in one click.
</Translate>
),
},
Expand All @@ -51,11 +57,14 @@ function useTabs(): Tab[] {
label: <Translate id="showcase.tab.storage">Storage Targets</Translate>,
icon: 'storage',
image: useBaseUrl('/img/screenshots/storage-targets.png'),
imageAlt: translate({id: 'showcase.storage.alt', message: 'BackupX storage target management screen'}),
title: <Translate id="showcase.storage.title">70+ backends, one flow</Translate>,
imageAlt: translate({
id: 'showcase.storage.alt',
message: 'BackupX storage targets showing connection health, live capacity, favourites and redundancy roles',
}),
title: <Translate id="showcase.storage.title">Every target, one view</Translate>,
description: (
<Translate id="showcase.storage.desc">
Alibaba OSS, Tencent COS, S3, Google Drive, WebDAV — plus every rclone backend behind a uniform form. Test connection, favourite, and view live usage.
Compare connection health, live capacity, favourites and redundancy roles across local disks and 70+ remote backends.
</Translate>
),
},
Expand All @@ -64,11 +73,14 @@ function useTabs(): Tab[] {
label: <Translate id="showcase.tab.nodes">Multi-Node</Translate>,
icon: 'network',
image: useBaseUrl('/img/screenshots/nodes.png'),
imageAlt: translate({id: 'showcase.nodes.alt', message: 'BackupX remote node management screen'}),
title: <Translate id="showcase.nodes.title">Master-Agent in minutes</Translate>,
imageAlt: translate({
id: 'showcase.nodes.alt',
message: 'BackupX node list showing health, Agent versions, queue depth, labels and heartbeat times',
}),
title: <Translate id="showcase.nodes.title">Cluster health in one view</Translate>,
description: (
<Translate id="showcase.nodes.desc">
Create a node, copy the token, start the Agent on any remote host. Tasks routed to a node run locally there and upload directly to storage — no reverse connectivity required.
Monitor health, Agent versions, queue depth, labels and heartbeat time across the local Master and every remote node.
</Translate>
),
},
Expand Down
Binary file modified docs-site/static/img/screenshots/backup-tasks.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs-site/static/img/screenshots/dashboard.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs-site/static/img/screenshots/nodes.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs-site/static/img/screenshots/storage-targets.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/backup-records.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/backup-tasks.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/dashboard.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/nodes.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/storage-targets.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion server/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ require (
github.com/prometheus/client_golang v1.23.2
github.com/rclone/rclone v1.73.5
github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil/v4 v4.25.10
github.com/spf13/viper v1.20.0
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.50.0
Expand Down Expand Up @@ -195,7 +196,6 @@ require (
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 // indirect
github.com/sagikazarmark/locafero v0.7.0 // indirect
github.com/samber/lo v1.52.0 // indirect
github.com/shirou/gopsutil/v4 v4.25.10 // indirect
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect
github.com/sony/gobreaker v1.0.0 // indirect
Expand Down
10 changes: 6 additions & 4 deletions server/internal/repository/backup_record_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func NewBackupRecordRepository(db *gorm.DB) *GormBackupRecordRepository {

func (r *GormBackupRecordRepository) List(ctx context.Context, options BackupRecordListOptions) ([]model.BackupRecord, error) {
// Omit("Manifest"):列表不需要可能很大的清单 JSON,避免每行拖出该 TEXT 列。
query := r.db.WithContext(ctx).Model(&model.BackupRecord{}).Omit("Manifest").Preload("Task").Preload("Task.StorageTarget").Order("started_at desc")
query := r.db.WithContext(ctx).Model(&model.BackupRecord{}).Omit("Manifest").Preload("Task").Preload("StorageTarget").Preload("Task.StorageTarget").Order("started_at desc")
if options.TaskID != nil {
query = query.Where("task_id = ?", *options.TaskID)
}
Expand Down Expand Up @@ -87,7 +87,7 @@ func (r *GormBackupRecordRepository) List(ctx context.Context, options BackupRec

func (r *GormBackupRecordRepository) FindByID(ctx context.Context, id uint) (*model.BackupRecord, error) {
var item model.BackupRecord
if err := r.db.WithContext(ctx).Preload("Task").Preload("Task.StorageTarget").First(&item, id).Error; err != nil {
if err := r.db.WithContext(ctx).Preload("Task").Preload("StorageTarget").Preload("Task.StorageTarget").First(&item, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
Expand Down Expand Up @@ -115,7 +115,9 @@ func (r *GormBackupRecordRepository) Create(ctx context.Context, item *model.Bac
}

func (r *GormBackupRecordRepository) Update(ctx context.Context, item *model.BackupRecord) error {
return r.db.WithContext(ctx).Save(item).Error
// Task 与 StorageTarget 是查询时预加载的只读关联。更新记录字段时忽略它们,
// 避免已加载的旧关联把刚修改的外键(例如首个成功上传目标)覆盖回去。
return r.db.WithContext(ctx).Omit("Task", "StorageTarget").Save(item).Error
}

func (r *GormBackupRecordRepository) Delete(ctx context.Context, id uint) error {
Expand All @@ -127,7 +129,7 @@ func (r *GormBackupRecordRepository) ListRecent(ctx context.Context, limit int)
limit = 10
}
var items []model.BackupRecord
if err := r.db.WithContext(ctx).Omit("Manifest").Preload("Task").Preload("Task.StorageTarget").Order("started_at desc").Limit(limit).Find(&items).Error; err != nil {
if err := r.db.WithContext(ctx).Omit("Manifest").Preload("Task").Preload("StorageTarget").Preload("Task.StorageTarget").Order("started_at desc").Limit(limit).Find(&items).Error; err != nil {
return nil, err
}
return items, nil
Expand Down
24 changes: 24 additions & 0 deletions server/internal/repository/backup_record_repository_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,20 +65,44 @@ func TestBackupRecordRepositoryQueries(t *testing.T) {
if stored == nil || stored.FileName != "website.tar.gz" {
t.Fatalf("unexpected stored record: %#v", stored)
}
if stored.StorageTarget.Name != "local" {
t.Fatalf("expected record storage target to be preloaded, got %#v", stored.StorageTarget)
}
listed, err := repo.List(ctx, BackupRecordListOptions{TaskID: &record.TaskID, Status: "success"})
if err != nil {
t.Fatalf("List returned error: %v", err)
}
if len(listed) != 1 {
t.Fatalf("expected one listed record, got %d", len(listed))
}
if listed[0].StorageTarget.Name != "local" {
t.Fatalf("expected listed storage target to be preloaded, got %#v", listed[0].StorageTarget)
}
recent, err := repo.ListRecent(ctx, 5)
if err != nil {
t.Fatalf("ListRecent returned error: %v", err)
}
if len(recent) != 1 {
t.Fatalf("expected one recent record, got %d", len(recent))
}
if recent[0].StorageTarget.Name != "local" {
t.Fatalf("expected recent storage target to be preloaded, got %#v", recent[0].StorageTarget)
}
secondTarget := &model.StorageTarget{Name: "secondary", Type: "local_disk", Enabled: true, ConfigCiphertext: "{}", ConfigVersion: 1, LastTestStatus: "unknown"}
if err := repo.db.Create(secondTarget).Error; err != nil {
t.Fatalf("seed second storage target error: %v", err)
}
stored.StorageTargetID = secondTarget.ID
if err := repo.Update(ctx, stored); err != nil {
t.Fatalf("Update storage target ID returned error: %v", err)
}
updated, err := repo.FindByID(ctx, record.ID)
if err != nil {
t.Fatalf("FindByID after update returned error: %v", err)
}
if updated == nil || updated.StorageTargetID != secondTarget.ID || updated.StorageTarget.Name != "secondary" {
t.Fatalf("expected updated storage target to remain %d, got %#v", secondTarget.ID, updated)
}
total, err := repo.Count(ctx)
if err != nil {
t.Fatalf("Count returned error: %v", err)
Expand Down
7 changes: 5 additions & 2 deletions server/internal/service/backup_execution_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -1026,16 +1026,19 @@ func (s *BackupExecutionService) executeTask(ctx context.Context, task *model.Ba
return
}
defer os.RemoveAll(result.TempDir)
// 依据运行器产出判定实际类型:产出清单 → 全量(记录清单供后续差异比对);否则为差异。
// 依据运行器产出判定实际类型:文件全量会产出清单;只有明确启用差异模式且
// 未产出清单时才是差异备份。数据库运行器本身不产出文件清单,但仍属于全量。
if result.Manifest != nil {
backupKind = model.BackupKindFull
if data, encErr := backup.EncodeManifest(*result.Manifest); encErr == nil {
manifestJSON = string(data)
} else {
logger.Warnf("备份清单序列化失败(不影响本次备份,但将禁用后续差异):%v", encErr)
}
} else {
} else if spec.Differential {
backupKind = model.BackupKindDifferential
} else {
backupKind = model.BackupKindFull
}
finalPath := result.ArtifactPath
if strings.EqualFold(task.Compression, "gzip") && !strings.HasSuffix(strings.ToLower(finalPath), ".gz") {
Expand Down
33 changes: 33 additions & 0 deletions server/internal/service/backup_execution_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,11 @@ func newExecutionTestServices(t *testing.T) (*BackupExecutionService, *BackupRec
if err != nil {
t.Fatalf("database.Open returned error: %v", err)
}
sqlDB, err := db.DB()
if err != nil {
t.Fatalf("db.DB returned error: %v", err)
}
t.Cleanup(func() { _ = sqlDB.Close() })
cipher := codec.NewConfigCipher("execution-secret")
tasks := repository.NewBackupTaskRepository(db)
targets := repository.NewStorageTargetRepository(db)
Expand Down Expand Up @@ -156,6 +161,34 @@ func TestBackupExecutionServiceRunTaskByIDSync(t *testing.T) {
}
}

func TestBackupExecutionServiceSQLiteBackupRemainsFull(t *testing.T) {
executionService, _, tasks, _, records, sourceDir, _ := newExecutionTestServices(t)
dbPath := filepath.Join(sourceDir, "finance.db")
if err := os.WriteFile(dbPath, []byte("sqlite-demo"), 0o644); err != nil {
t.Fatalf("WriteFile sqlite fixture returned error: %v", err)
}
task := &model.BackupTask{
Name: "finance-db", Type: model.BackupTaskTypeSQLite, Enabled: true,
DBPath: dbPath, StorageTargetID: 1, RetentionDays: 30,
Compression: "none", MaxBackups: 10, LastStatus: "idle",
}
if err := tasks.Create(context.Background(), task); err != nil {
t.Fatalf("Create sqlite task returned error: %v", err)
}

detail, err := executionService.RunTaskByIDSync(context.Background(), task.ID)
if err != nil {
t.Fatalf("RunTaskByIDSync sqlite returned error: %v", err)
}
stored, err := records.FindByID(context.Background(), detail.ID)
if err != nil {
t.Fatalf("FindByID sqlite record returned error: %v", err)
}
if stored == nil || stored.BackupKind != model.BackupKindFull {
t.Fatalf("expected sqlite backup kind full, got %#v", stored)
}
}

func TestBackupExecutionServiceRepositoryModeRoundTrip(t *testing.T) {
executionService, recordService, tasks, _, records, sourceDir, storageDir := newExecutionTestServices(t)
ctx := context.Background()
Expand Down
12 changes: 6 additions & 6 deletions server/internal/service/system_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ import (
"path/filepath"
"runtime"
"strings"
"syscall"
"time"

"backupx/server/internal/config"

"github.com/shirou/gopsutil/v4/disk"
)

type SystemInfo struct {
Expand Down Expand Up @@ -124,11 +125,10 @@ func (s *SystemService) GetInfo(_ context.Context) *SystemInfo {
if dir == "" {
dir = "."
}
var stat syscall.Statfs_t
if err := syscall.Statfs(dir, &stat); err == nil {
info.DiskTotal = int64(stat.Blocks) * int64(stat.Bsize)
info.DiskFree = int64(stat.Bavail) * int64(stat.Bsize)
info.DiskUsed = info.DiskTotal - info.DiskFree
if stat, err := disk.Usage(dir); err == nil {
info.DiskTotal = int64(stat.Total)
info.DiskFree = int64(stat.Free)
info.DiskUsed = int64(stat.Used)
}
return info
}
Loading