feat: improve local demo and documentation
@@ -26,12 +26,12 @@
|
|||||||
|
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<td width="50%"><img src="screenshots/dashboard.png" alt="Dashboard"></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="Backup Tasks"></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>
|
||||||
<tr>
|
<tr>
|
||||||
<td><img src="screenshots/storage-targets.png" alt="Storage Targets"></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="Backup Records"></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>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|||||||
@@ -26,12 +26,12 @@
|
|||||||
|
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<td width="50%"><img src="screenshots/dashboard.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="备份任务"></td>
|
<td width="50%"><img src="screenshots/backup-tasks.png" alt="BackupX 备份任务列表,展示调度、存储目标、保留策略、标签、RPO 和验证状态"><br><sub><strong>备份任务</strong> — 调度、双目标策略、保留规则、标签、RPO 与定时验证。</sub></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><img src="screenshots/storage-targets.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="备份记录"></td>
|
<td><img src="screenshots/backup-records.png" alt="BackupX 备份历史,展示成功失败状态、校验和、存储位置及保留锁"><br><sub><strong>备份记录</strong> — 成功/失败状态、校验和、落盘位置、执行日志与保留锁定。</sub></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|||||||
@@ -83,14 +83,18 @@
|
|||||||
"showcase.tab.tasks": {"message": "备份任务"},
|
"showcase.tab.tasks": {"message": "备份任务"},
|
||||||
"showcase.tab.storage": {"message": "存储目标"},
|
"showcase.tab.storage": {"message": "存储目标"},
|
||||||
"showcase.tab.nodes": {"message": "多节点"},
|
"showcase.tab.nodes": {"message": "多节点"},
|
||||||
"showcase.dashboard.title": {"message": "一眼掌握全局"},
|
"showcase.dashboard.alt": {"message": "BackupX 仪表盘,展示 30 天备份趋势、存储分布、任务规模和集群健康度"},
|
||||||
"showcase.dashboard.desc": {"message": "备份成功率、存储使用量、最近执行记录、即将触发的计划 — 一页实时数据。"},
|
"showcase.dashboard.title": {"message": "一眼掌握运行态"},
|
||||||
"showcase.tasks.title": {"message": "可视化任务编辑器"},
|
"showcase.dashboard.desc": {"message": "在一个实时运维视图中掌握 30 天成功/失败趋势、存储分布、任务数量、数据量和最近执行记录。"},
|
||||||
"showcase.tasks.desc": {"message": "文件、MySQL、PostgreSQL、SQLite、SAP HANA — 三步完成。Cron 编辑器、多目标分发、保留策略、压缩、加密 — 点击即用。"},
|
"showcase.tasks.alt": {"message": "BackupX 任务列表,展示调度、存储目标、保留策略、标签、RPO 目标和验证状态"},
|
||||||
"showcase.storage.title": {"message": "70+ 后端,统一体验"},
|
"showcase.tasks.title": {"message": "策略状态清晰可扫"},
|
||||||
"showcase.storage.desc": {"message": "阿里云 OSS、腾讯云 COS、S3、Google Drive、WebDAV — 加上每一种 rclone 后端。测试连接、收藏、查看实时容量。"},
|
"showcase.tasks.desc": {"message": "集中查看调度、多目标策略、保留规则、标签、RPO 目标和验证状态,并可一键操作任意任务。"},
|
||||||
"showcase.nodes.title": {"message": "几分钟搭起 Master-Agent"},
|
"showcase.storage.alt": {"message": "BackupX 存储目标,展示连接状态、实时容量、收藏和冗余角色"},
|
||||||
"showcase.nodes.desc": {"message": "创建节点、复制令牌、在任意远程主机启动 Agent。路由到节点的任务在本地执行并直接上传到存储 — 无需反向连通性。"},
|
"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": "开始阅读文档"},
|
"showcase.cta": {"message": "开始阅读文档"},
|
||||||
|
|
||||||
"community.tag": {"message": "社区"},
|
"community.tag": {"message": "社区"},
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type {ReactNode} from 'react';
|
|||||||
import {useState} from 'react';
|
import {useState} from 'react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import Heading from '@theme/Heading';
|
import Heading from '@theme/Heading';
|
||||||
import Translate from '@docusaurus/Translate';
|
import Translate, {translate} from '@docusaurus/Translate';
|
||||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||||
import Link from '@docusaurus/Link';
|
import Link from '@docusaurus/Link';
|
||||||
import styles from './styles.module.css';
|
import styles from './styles.module.css';
|
||||||
@@ -11,6 +11,7 @@ type Tab = {
|
|||||||
id: string;
|
id: string;
|
||||||
label: ReactNode;
|
label: ReactNode;
|
||||||
image: string;
|
image: string;
|
||||||
|
imageAlt: string;
|
||||||
title: ReactNode;
|
title: ReactNode;
|
||||||
description: ReactNode;
|
description: ReactNode;
|
||||||
};
|
};
|
||||||
@@ -21,10 +22,14 @@ function useTabs(): Tab[] {
|
|||||||
id: 'dashboard',
|
id: 'dashboard',
|
||||||
label: <Translate id="showcase.tab.dashboard">Dashboard</Translate>,
|
label: <Translate id="showcase.tab.dashboard">Dashboard</Translate>,
|
||||||
image: useBaseUrl('/img/screenshots/dashboard.png'),
|
image: useBaseUrl('/img/screenshots/dashboard.png'),
|
||||||
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: (
|
description: (
|
||||||
<Translate id="showcase.dashboard.desc">
|
<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>
|
</Translate>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -32,10 +37,14 @@ function useTabs(): Tab[] {
|
|||||||
id: 'tasks',
|
id: 'tasks',
|
||||||
label: <Translate id="showcase.tab.tasks">Backup Tasks</Translate>,
|
label: <Translate id="showcase.tab.tasks">Backup Tasks</Translate>,
|
||||||
image: useBaseUrl('/img/screenshots/backup-tasks.png'),
|
image: useBaseUrl('/img/screenshots/backup-tasks.png'),
|
||||||
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: (
|
description: (
|
||||||
<Translate id="showcase.tasks.desc">
|
<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>
|
</Translate>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -43,10 +52,14 @@ function useTabs(): Tab[] {
|
|||||||
id: 'storage',
|
id: 'storage',
|
||||||
label: <Translate id="showcase.tab.storage">Storage Targets</Translate>,
|
label: <Translate id="showcase.tab.storage">Storage Targets</Translate>,
|
||||||
image: useBaseUrl('/img/screenshots/storage-targets.png'),
|
image: useBaseUrl('/img/screenshots/storage-targets.png'),
|
||||||
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: (
|
description: (
|
||||||
<Translate id="showcase.storage.desc">
|
<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>
|
</Translate>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -54,10 +67,14 @@ function useTabs(): Tab[] {
|
|||||||
id: 'nodes',
|
id: 'nodes',
|
||||||
label: <Translate id="showcase.tab.nodes">Multi-Node</Translate>,
|
label: <Translate id="showcase.tab.nodes">Multi-Node</Translate>,
|
||||||
image: useBaseUrl('/img/screenshots/nodes.png'),
|
image: useBaseUrl('/img/screenshots/nodes.png'),
|
||||||
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: (
|
description: (
|
||||||
<Translate id="showcase.nodes.desc">
|
<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>
|
</Translate>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -103,7 +120,7 @@ export default function HomepageShowcase(): ReactNode {
|
|||||||
<span className={clsx(styles.browserDot, styles.browserDotGreen)} />
|
<span className={clsx(styles.browserDot, styles.browserDotGreen)} />
|
||||||
<div className={styles.browserUrl}>backupx.local</div>
|
<div className={styles.browserUrl}>backupx.local</div>
|
||||||
</div>
|
</div>
|
||||||
<img src={current.image} alt="" className={styles.screenshot} />
|
<img src={current.image} alt={current.imageAlt} className={styles.screenshot} />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.caption}>
|
<div className={styles.caption}>
|
||||||
<Heading as="h3" className={styles.captionTitle}>{current.title}</Heading>
|
<Heading as="h3" className={styles.captionTitle}>{current.title}</Heading>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 88 KiB |
@@ -12,6 +12,7 @@ require (
|
|||||||
github.com/prometheus/client_golang v1.23.2
|
github.com/prometheus/client_golang v1.23.2
|
||||||
github.com/rclone/rclone v1.73.5
|
github.com/rclone/rclone v1.73.5
|
||||||
github.com/robfig/cron/v3 v3.0.1
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
|
github.com/shirou/gopsutil/v4 v4.25.10
|
||||||
github.com/spf13/viper v1.20.0
|
github.com/spf13/viper v1.20.0
|
||||||
go.uber.org/zap v1.27.0
|
go.uber.org/zap v1.27.0
|
||||||
golang.org/x/crypto v0.50.0
|
golang.org/x/crypto v0.50.0
|
||||||
@@ -195,7 +196,6 @@ require (
|
|||||||
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 // indirect
|
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 // indirect
|
||||||
github.com/sagikazarmark/locafero v0.7.0 // indirect
|
github.com/sagikazarmark/locafero v0.7.0 // indirect
|
||||||
github.com/samber/lo v1.52.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/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
|
||||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect
|
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect
|
||||||
github.com/sony/gobreaker v1.0.0 // indirect
|
github.com/sony/gobreaker v1.0.0 // indirect
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ func NewBackupRecordRepository(db *gorm.DB) *GormBackupRecordRepository {
|
|||||||
|
|
||||||
func (r *GormBackupRecordRepository) List(ctx context.Context, options BackupRecordListOptions) ([]model.BackupRecord, error) {
|
func (r *GormBackupRecordRepository) List(ctx context.Context, options BackupRecordListOptions) ([]model.BackupRecord, error) {
|
||||||
// Omit("Manifest"):列表不需要可能很大的清单 JSON,避免每行拖出该 TEXT 列。
|
// 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 {
|
if options.TaskID != nil {
|
||||||
query = query.Where("task_id = ?", *options.TaskID)
|
query = query.Where("task_id = ?", *options.TaskID)
|
||||||
}
|
}
|
||||||
@@ -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) {
|
func (r *GormBackupRecordRepository) FindByID(ctx context.Context, id uint) (*model.BackupRecord, error) {
|
||||||
var item model.BackupRecord
|
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) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
@@ -127,7 +127,7 @@ func (r *GormBackupRecordRepository) ListRecent(ctx context.Context, limit int)
|
|||||||
limit = 10
|
limit = 10
|
||||||
}
|
}
|
||||||
var items []model.BackupRecord
|
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 nil, err
|
||||||
}
|
}
|
||||||
return items, nil
|
return items, nil
|
||||||
|
|||||||
@@ -22,6 +22,11 @@ func newBackupRecordTestRepository(t *testing.T) *GormBackupRecordRepository {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("database.Open returned error: %v", err)
|
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() })
|
||||||
storageTarget := &model.StorageTarget{Name: "local", Type: "local_disk", Enabled: true, ConfigCiphertext: "{}", ConfigVersion: 1, LastTestStatus: "unknown"}
|
storageTarget := &model.StorageTarget{Name: "local", Type: "local_disk", Enabled: true, ConfigCiphertext: "{}", ConfigVersion: 1, LastTestStatus: "unknown"}
|
||||||
if err := db.Create(storageTarget).Error; err != nil {
|
if err := db.Create(storageTarget).Error; err != nil {
|
||||||
t.Fatalf("seed storage target error: %v", err)
|
t.Fatalf("seed storage target error: %v", err)
|
||||||
@@ -60,6 +65,9 @@ func TestBackupRecordRepositoryQueries(t *testing.T) {
|
|||||||
if stored == nil || stored.FileName != "website.tar.gz" {
|
if stored == nil || stored.FileName != "website.tar.gz" {
|
||||||
t.Fatalf("unexpected stored record: %#v", stored)
|
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"})
|
listed, err := repo.List(ctx, BackupRecordListOptions{TaskID: &record.TaskID, Status: "success"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("List returned error: %v", err)
|
t.Fatalf("List returned error: %v", err)
|
||||||
@@ -67,6 +75,9 @@ func TestBackupRecordRepositoryQueries(t *testing.T) {
|
|||||||
if len(listed) != 1 {
|
if len(listed) != 1 {
|
||||||
t.Fatalf("expected one listed record, got %d", len(listed))
|
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)
|
recent, err := repo.ListRecent(ctx, 5)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ListRecent returned error: %v", err)
|
t.Fatalf("ListRecent returned error: %v", err)
|
||||||
@@ -74,6 +85,9 @@ func TestBackupRecordRepositoryQueries(t *testing.T) {
|
|||||||
if len(recent) != 1 {
|
if len(recent) != 1 {
|
||||||
t.Fatalf("expected one recent record, got %d", len(recent))
|
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)
|
||||||
|
}
|
||||||
total, err := repo.Count(ctx)
|
total, err := repo.Count(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Count returned error: %v", err)
|
t.Fatalf("Count returned error: %v", err)
|
||||||
|
|||||||
@@ -721,7 +721,8 @@ func (s *BackupExecutionService) executeTask(ctx context.Context, task *model.Ba
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(result.TempDir)
|
defer os.RemoveAll(result.TempDir)
|
||||||
// 依据运行器产出判定实际类型:产出清单 → 全量(记录清单供后续差异比对);否则为差异。
|
// 依据运行器产出判定实际类型:文件全量会产出清单;只有明确启用差异模式且
|
||||||
|
// 未产出清单时才是差异备份。数据库运行器本身不产出文件清单,但仍属于全量。
|
||||||
if result.Manifest != nil {
|
if result.Manifest != nil {
|
||||||
backupKind = model.BackupKindFull
|
backupKind = model.BackupKindFull
|
||||||
if data, encErr := backup.EncodeManifest(*result.Manifest); encErr == nil {
|
if data, encErr := backup.EncodeManifest(*result.Manifest); encErr == nil {
|
||||||
@@ -729,8 +730,10 @@ func (s *BackupExecutionService) executeTask(ctx context.Context, task *model.Ba
|
|||||||
} else {
|
} else {
|
||||||
logger.Warnf("备份清单序列化失败(不影响本次备份,但将禁用后续差异):%v", encErr)
|
logger.Warnf("备份清单序列化失败(不影响本次备份,但将禁用后续差异):%v", encErr)
|
||||||
}
|
}
|
||||||
} else {
|
} else if spec.Differential {
|
||||||
backupKind = model.BackupKindDifferential
|
backupKind = model.BackupKindDifferential
|
||||||
|
} else {
|
||||||
|
backupKind = model.BackupKindFull
|
||||||
}
|
}
|
||||||
finalPath := result.ArtifactPath
|
finalPath := result.ArtifactPath
|
||||||
if strings.EqualFold(task.Compression, "gzip") && !strings.HasSuffix(strings.ToLower(finalPath), ".gz") {
|
if strings.EqualFold(task.Compression, "gzip") && !strings.HasSuffix(strings.ToLower(finalPath), ".gz") {
|
||||||
|
|||||||
@@ -107,6 +107,11 @@ func newExecutionTestServices(t *testing.T) (*BackupExecutionService, *BackupRec
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("database.Open returned error: %v", err)
|
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")
|
cipher := codec.NewConfigCipher("execution-secret")
|
||||||
tasks := repository.NewBackupTaskRepository(db)
|
tasks := repository.NewBackupTaskRepository(db)
|
||||||
targets := repository.NewStorageTargetRepository(db)
|
targets := repository.NewStorageTargetRepository(db)
|
||||||
@@ -155,6 +160,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 TestBackupExecutionServiceNodePoolSelectionDoesNotPersistTaskNodeID(t *testing.T) {
|
func TestBackupExecutionServiceNodePoolSelectionDoesNotPersistTaskNodeID(t *testing.T) {
|
||||||
executionService, _, tasks, _, records, _, _ := newExecutionTestServices(t)
|
executionService, _, tasks, _, records, _, _ := newExecutionTestServices(t)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"backupx/server/internal/config"
|
"backupx/server/internal/config"
|
||||||
|
|
||||||
|
"github.com/shirou/gopsutil/v4/disk"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SystemInfo struct {
|
type SystemInfo struct {
|
||||||
@@ -124,12 +125,10 @@ func (s *SystemService) GetInfo(_ context.Context) *SystemInfo {
|
|||||||
if dir == "" {
|
if dir == "" {
|
||||||
dir = "."
|
dir = "."
|
||||||
}
|
}
|
||||||
var stat syscall.Statfs_t
|
if stat, err := disk.Usage(dir); err == nil {
|
||||||
if err := syscall.Statfs(dir, &stat); err == nil {
|
info.DiskTotal = int64(stat.Total)
|
||||||
info.DiskTotal = int64(stat.Blocks) * int64(stat.Bsize)
|
info.DiskFree = int64(stat.Free)
|
||||||
info.DiskFree = int64(stat.Bavail) * int64(stat.Bsize)
|
info.DiskUsed = int64(stat.Used)
|
||||||
info.DiskUsed = info.DiskTotal - info.DiskFree
|
|
||||||
}
|
}
|
||||||
return info
|
return info
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||