项目标题与描述
Backrest是一个基于Restic构建的现代化Web可访问备份解决方案。它提供了一个直观的Web界面,封装了restic CLI的功能,使用户能够轻松创建存储库、浏览快照和恢复文件。Backrest可以在后台运行,并采用智能化的方法来调度快照执行和协调存储库健康操作。
该项目使用Go语言构建,作为一个独立的轻量级二进制文件分发,唯一依赖是restic。它可以安全地创建新存储库或管理现有存储库,一旦配置了存储,WebUI可以处理大多数操作,同时在需要时仍然允许直接访问强大的restic CLI进行高级操作。
功能特性
- Web界面:支持本地或远程访问(非常适合NAS部署)
- 多平台支持:
- Linux
- macOS
- Windows
- FreeBSD
- Docker
- 备份管理:
- 导入现有的restic存储库
- 基于Cron的备份和维护计划(如清理、检查、遗忘等)
- 浏览和从快照恢复文件
- 可配置的备份策略和保留策略
- 自动化操作:
- 智能调度系统
- 自动健康检查和维护
- 钩子系统支持自定义操作
- 安全特性:
- 基于JWT的身份验证
- 加密通信支持
- 安全的密钥管理
安装指南
系统要求
- 支持的操作系统:Linux、macOS、Windows、FreeBSD
- 需要安装restic 0.18.0或更高版本
- 至少512MB内存
- 足够的磁盘空间用于备份数据
安装步骤
- 下载最新版本:
# 从GitHub Releases页面下载对应平台的二进制文件
wget https://github.com/garethgeorge/backrest/releases/latest/download/backrest-linux-amd64
- 运行安装脚本:
# 本地访问模式(默认)
./install.sh# 启用远程访问
./install.sh --allow-remote-access
- 验证安装:
# 检查服务状态
systemctl status backrest # Linux
launchctl list | grep backrest # macOS
- 访问Web界面:
本地访问:http://localhost:9898
远程访问:http://0.0.0.0:9898
Docker安装
docker pull garethgeorge/backrest
docker run -p 9898:9898 -v /path/to/config:/config garethgeorge/backrest
使用说明
基本使用示例
- 初始化备份存储库:
# 通过WebUI创建新存储库或导入现有存储库
# 支持本地路径、SFTP、AWS S3、Backblaze B2等多种后端
- 创建备份计划:
# 示例备份计划配置
plan:id: "daily-backup"paths: ["/home/user/documents", "/home/user/photos"]schedule:cron: "0 2 * * *" # 每天凌晨2点执行retention:keep-daily: 7keep-weekly: 4keep-monthly: 12
- 执行即时备份:
# 通过WebUI触发即时备份操作
# 或使用API端点
curl -X POST http://localhost:9898/api/v1/backup \-H "Content-Type: application/json" \-d '{"repoId": "my-repo", "planId": "daily-backup"}'
API概览
Backrest提供完整的gRPC和RESTful API接口,支持以下操作:
- 配置管理(获取/设置配置)
- 存储库操作(添加/删除/检查存储库)
- 备份管理(启动/停止/监控备份)
- 快照操作(列表/恢复/删除快照)
- 系统状态监控
核心代码
主程序入口
package mainimport ("context""flag""os""os/signal""syscall"v1 "github.com/garethgeorge/backrest/gen/go/v1""github.com/garethgeorge/backrest/internal/api""github.com/garethgeorge/backrest/internal/config""github.com/garethgeorge/backrest/internal/orchestrator""go.uber.org/zap"
)var InstallDepsOnly = flag.Bool("install-deps-only", false, "安装依赖并退出")func main() {flag.Parse()// 初始化日志系统installLoggers()// 查找或安装restic二进制文件resticPath, err := resticinstaller.FindOrInstallResticBinary()if err != nil {zap.S().Fatalf("查找或安装restic时出错: %v", err)}if *InstallDepsOnly {zap.S().Info("依赖已安装,正在退出")return}// 创建上下文和取消函数ctx, cancel := context.WithCancel(context.Background())defer cancel()// 加载配置configStore := createConfigProvider()cfg, err := configStore.Get()if err != nil {zap.S().Fatalf("加载配置时出错: %v", err)}// 创建编排器实例orc := orchestrator.NewOrchestrator(configStore, oplog, logStore)// 启动HTTP服务器startHTTPServer(cfg, orc, oplog)// 等待终止信号waitForShutdown(cancel)
}func waitForShutdown(cancel context.CancelFunc) {sigCh := make(chan os.Signal, 1)signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)<-sigChcancel()
}
备份任务实现
package tasksimport ("context""fmt""time"v1 "github.com/garethgeorge/backrest/gen/go/v1"
)// BackupTask 表示一个计划的备份操作
type BackupTask struct {BaseTaskforce booldidRun bool
}// NewScheduledBackupTask 创建新的计划备份任务
func NewScheduledBackupTask(repo *v1.Repo, plan *v1.Plan) *BackupTask {return &BackupTask{BaseTask: BaseTask{TaskType: "backup",TaskName: fmt.Sprintf("plan %q backup", plan.Id),TaskRepo: repo,TaskPlanID: plan.Id,},}
}// Next 计算下一次运行时间
func (t *BackupTask) Next(now time.Time, runner TaskRunner) (ScheduledTask, error) {if t.force {if t.didRun {return NeverScheduledTask, nil}t.didRun = truereturn ScheduledTask{Task: t,RunAt: now,Op: &v1.Operation{Op: &v1.Operation_OperationBackup{},},}, nil}plan, err := runner.GetPlan(t.PlanID())if err != nil {return NeverScheduledTask, err}if plan.Schedule == nil {return NeverScheduledTask, nil}// 计算基于计划的下一次运行时间// ... 计划调度逻辑return nextScheduledTime, nil
}// Run 执行备份任务
func (t *BackupTask) Run(ctx context.Context, st ScheduledTask, runner TaskRunner) error {op := st.Oprepo, err := runner.GetRepoOrchestrator(t.RepoID())if err != nil {return fmt.Errorf("get repo %q: %w", t.RepoID(), err)}// 执行备份前钩子if err := runner.ExecuteHooks(ctx, []v1.Hook_Condition{v1.Hook_CONDITION_SNAPSHOT_START,}, HookVars{}); err != nil {return fmt.Errorf("snapshot start hook: %w", err)}// 执行实际备份操作// ... 备份执行逻辑// 执行备份后钩子if err := runner.ExecuteHooks(ctx, []v1.Hook_Condition{v1.Hook_CONDITION_SNAPSHOT_END,}, HookVars{}); err != nil {return fmt.Errorf("snapshot end hook: %w", err)}return nil
}
Web API处理程序
package apiimport ("context""connectrpc.com/connect"v1 "github.com/garethgeorge/backrest/gen/go/v1""github.com/garethgeorge/backrest/internal/orchestrator"
)// BackrestHandler 处理Backrest API请求
type BackrestHandler struct {config config.ConfigStoreorchestrator *orchestrator.Orchestrator
}// NewBackrestHandler 创建新的API处理程序
func NewBackrestHandler(config config.ConfigStore, orchestrator *orchestrator.Orchestrator) *BackrestHandler {return &BackrestHandler{config: config,orchestrator: orchestrator,}
}// GetConfig 获取当前配置
func (s *BackrestHandler) GetConfig(ctx context.Context, req *connect.Request[emptypb.Empty]) (*connect.Response[v1.Config], error) {config, err := s.config.Get()if err != nil {return nil, connect.NewError(connect.CodeInternal, err)}return connect.NewResponse(config), nil
}// Backup 执行备份操作
func (s *BackrestHandler) Backup(ctx context.Context, req *connect.Request[v1.BackupRequest]) (*connect.Response[v1.Operation], error) {repo, err := s.orchestrator.GetRepo(req.Msg.RepoId)if err != nil {return nil, connect.NewError(connect.CodeNotFound, err)}// 创建并调度备份任务task := tasks.NewOneoffBackupTask(repo, req.Msg.PlanId, time.Now())if err := s.orchestrator.ScheduleTask(task, tasks.TaskPriorityInteractive); err != nil {return nil, connect.NewError(connect.CodeInternal, err)}return connect.NewResponse(&v1.Operation{Status: v1.OperationStatus_STATUS_PENDING,}), nil
}
配置管理
package configimport ("encoding/json""os""sync"v1 "github.com/garethgeorge/backrest/gen/go/v1"
)// ConfigStore 管理应用程序配置
type ConfigStore interface {Get() (*v1.Config, error)Set(*v1.Config) error
}// FileConfigStore 基于文件的配置存储
type FileConfigStore struct {path stringmu sync.RWMutex
}// NewFileConfigStore 创建新的文件配置存储
func NewFileConfigStore(path string) *FileConfigStore {return &FileConfigStore{path: path}
}// Get 从文件加载配置
func (s *FileConfigStore) Get() (*v1.Config, error) {s.mu.RLock()defer s.mu.RUnlock()data, err := os.ReadFile(s.path)if err != nil {if os.IsNotExist(err) {return &v1.Config{}, nil}return nil, err}var config v1.Configif err := json.Unmarshal(data, &config); err != nil {return nil, err}return &config, nil
}// Set 保存配置到文件
func (s *FileConfigStore) Set(config *v1.Config) error {s.mu.Lock()defer s.mu.Unlock()data, err := json.MarshalIndent(config, "", " ")if err != nil {return err}if err := os.MkdirAll(s.path, 0700); err != nil {return err}return os.WriteFile(s.path, data, 0600)
}
更多精彩内容 请关注我的个人公众号 公众号(办公AI智能小助手)
公众号二维码