Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed13194de1 |
@@ -28,12 +28,12 @@
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<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>
|
||||
<td width="50%"><img src="screenshots/dashboard.png" alt="Dashboard"></td>
|
||||
<td width="50%"><img src="screenshots/backup-tasks.png" alt="Backup Tasks"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
<td><img src="screenshots/storage-targets.png" alt="Storage Targets"></td>
|
||||
<td><img src="screenshots/backup-records.png" alt="Backup Records"></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -28,12 +28,12 @@
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<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>
|
||||
<td width="50%"><img src="screenshots/dashboard.png" alt="仪表盘"></td>
|
||||
<td width="50%"><img src="screenshots/backup-tasks.png" alt="备份任务"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
<td><img src="screenshots/storage-targets.png" alt="存储目标"></td>
|
||||
<td><img src="screenshots/backup-records.png" alt="备份记录"></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -47,18 +47,18 @@
|
||||
"showcase.tab.tasks": {"message": "备份任务"},
|
||||
"showcase.tab.storage": {"message": "存储目标"},
|
||||
"showcase.tab.nodes": {"message": "多节点"},
|
||||
"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.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.cta": {"message": "开始阅读文档"},
|
||||
|
||||
"community.tag": {"message": "社区"},
|
||||
|
||||
@@ -25,14 +25,11 @@ 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 30-day backup trends, storage distribution, task volume and cluster health',
|
||||
}),
|
||||
title: <Translate id="showcase.dashboard.title">Operations at a glance</Translate>,
|
||||
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>,
|
||||
description: (
|
||||
<Translate id="showcase.dashboard.desc">
|
||||
Track 30-day success and failure trends, storage distribution, task counts, data volume and recent runs from one live operations view.
|
||||
Backup success rates, storage usage, recent runs and upcoming schedules — all on one page with live data.
|
||||
</Translate>
|
||||
),
|
||||
},
|
||||
@@ -41,14 +38,11 @@ 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 task list showing schedules, storage targets, retention, tags, RPO goals and verification status',
|
||||
}),
|
||||
title: <Translate id="showcase.tasks.title">Policies you can scan</Translate>,
|
||||
imageAlt: translate({id: 'showcase.tasks.alt', message: 'BackupX backup task management screen'}),
|
||||
title: <Translate id="showcase.tasks.title">Visual task editor</Translate>,
|
||||
description: (
|
||||
<Translate id="showcase.tasks.desc">
|
||||
Review schedules, multi-target policies, retention, tags, RPO goals and verification status together, then act on any task in one click.
|
||||
Files, MySQL, PostgreSQL, SQLite and SAP HANA with a three-step wizard. Cron editor, multi-target dispatch, retention, compression and encryption — point and click.
|
||||
</Translate>
|
||||
),
|
||||
},
|
||||
@@ -57,14 +51,11 @@ 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 targets showing connection health, live capacity, favourites and redundancy roles',
|
||||
}),
|
||||
title: <Translate id="showcase.storage.title">Every target, one view</Translate>,
|
||||
imageAlt: translate({id: 'showcase.storage.alt', message: 'BackupX storage target management screen'}),
|
||||
title: <Translate id="showcase.storage.title">70+ backends, one flow</Translate>,
|
||||
description: (
|
||||
<Translate id="showcase.storage.desc">
|
||||
Compare connection health, live capacity, favourites and redundancy roles across local disks and 70+ remote backends.
|
||||
Alibaba OSS, Tencent COS, S3, Google Drive, WebDAV — plus every rclone backend behind a uniform form. Test connection, favourite, and view live usage.
|
||||
</Translate>
|
||||
),
|
||||
},
|
||||
@@ -73,14 +64,11 @@ 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 node list showing health, Agent versions, queue depth, labels and heartbeat times',
|
||||
}),
|
||||
title: <Translate id="showcase.nodes.title">Cluster health in one view</Translate>,
|
||||
imageAlt: translate({id: 'showcase.nodes.alt', message: 'BackupX remote node management screen'}),
|
||||
title: <Translate id="showcase.nodes.title">Master-Agent in minutes</Translate>,
|
||||
description: (
|
||||
<Translate id="showcase.nodes.desc">
|
||||
Monitor health, Agent versions, queue depth, labels and heartbeat time across the local Master and every remote node.
|
||||
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.
|
||||
</Translate>
|
||||
),
|
||||
},
|
||||
|
||||
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 96 KiB |
@@ -12,7 +12,6 @@ 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
|
||||
@@ -196,6 +195,7 @@ 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
|
||||
|
||||
@@ -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("StorageTarget").Preload("Task.StorageTarget").Order("started_at desc")
|
||||
query := r.db.WithContext(ctx).Model(&model.BackupRecord{}).Omit("Manifest").Preload("Task").Preload("Task.StorageTarget").Order("started_at desc")
|
||||
if options.TaskID != nil {
|
||||
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) {
|
||||
var item model.BackupRecord
|
||||
if err := r.db.WithContext(ctx).Preload("Task").Preload("StorageTarget").Preload("Task.StorageTarget").First(&item, id).Error; err != nil {
|
||||
if err := r.db.WithContext(ctx).Preload("Task").Preload("Task.StorageTarget").First(&item, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -115,9 +115,7 @@ func (r *GormBackupRecordRepository) Create(ctx context.Context, item *model.Bac
|
||||
}
|
||||
|
||||
func (r *GormBackupRecordRepository) Update(ctx context.Context, item *model.BackupRecord) error {
|
||||
// Task 与 StorageTarget 是查询时预加载的只读关联。更新记录字段时忽略它们,
|
||||
// 避免已加载的旧关联把刚修改的外键(例如首个成功上传目标)覆盖回去。
|
||||
return r.db.WithContext(ctx).Omit("Task", "StorageTarget").Save(item).Error
|
||||
return r.db.WithContext(ctx).Save(item).Error
|
||||
}
|
||||
|
||||
func (r *GormBackupRecordRepository) Delete(ctx context.Context, id uint) error {
|
||||
@@ -129,7 +127,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("StorageTarget").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("Task.StorageTarget").Order("started_at desc").Limit(limit).Find(&items).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
|
||||
@@ -65,9 +65,6 @@ 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)
|
||||
@@ -75,9 +72,6 @@ func TestBackupRecordRepositoryQueries(t *testing.T) {
|
||||
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)
|
||||
@@ -85,24 +79,6 @@ func TestBackupRecordRepositoryQueries(t *testing.T) {
|
||||
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)
|
||||
|
||||
@@ -1026,8 +1026,7 @@ 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 {
|
||||
@@ -1035,10 +1034,8 @@ func (s *BackupExecutionService) executeTask(ctx context.Context, task *model.Ba
|
||||
} else {
|
||||
logger.Warnf("备份清单序列化失败(不影响本次备份,但将禁用后续差异):%v", encErr)
|
||||
}
|
||||
} else if spec.Differential {
|
||||
backupKind = model.BackupKindDifferential
|
||||
} else {
|
||||
backupKind = model.BackupKindFull
|
||||
backupKind = model.BackupKindDifferential
|
||||
}
|
||||
finalPath := result.ArtifactPath
|
||||
if strings.EqualFold(task.Compression, "gzip") && !strings.HasSuffix(strings.ToLower(finalPath), ".gz") {
|
||||
|
||||
@@ -108,11 +108,6 @@ 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)
|
||||
@@ -161,34 +156,6 @@ 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()
|
||||
|
||||
@@ -8,11 +8,10 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"backupx/server/internal/config"
|
||||
|
||||
"github.com/shirou/gopsutil/v4/disk"
|
||||
)
|
||||
|
||||
type SystemInfo struct {
|
||||
@@ -125,10 +124,11 @@ func (s *SystemService) GetInfo(_ context.Context) *SystemInfo {
|
||||
if dir == "" {
|
||||
dir = "."
|
||||
}
|
||||
if stat, err := disk.Usage(dir); err == nil {
|
||||
info.DiskTotal = int64(stat.Total)
|
||||
info.DiskFree = int64(stat.Free)
|
||||
info.DiskUsed = int64(stat.Used)
|
||||
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
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||