Files
panel/internal/biz/backup.go
T
耗子andClaude Opus 5 e0588a1468 refactor: 把 context 打通到数据库、对象存储与容器 API
上一轮打通 shell 路径时留下三处接口本身不带 ctx,导致底下的
context.Background() 无法消除。这三处 contextcheck 都不报(接口方法没有
ctx 形参,静态分析认为合理),属于 lint 发现不了的死角:

- pkg/db.Operator 的 17 个方法加 ctx,MySQL/Postgres/ClickHouse/SQLite/
  MongoDB/Elasticsearch 实现同步改。其中 Elasticsearch 的 resty 请求原先
  根本没挂 ctx,不只是传了 Background
- pkg/storage.Storage 的 6 个方法加 ctx,local/s3/sftp/webdav 四个实现与
  s3sdk 子包同步改;sftp 连接改用 net.Dialer.DialContext,连接阶段可取消
- internal/data/container*.go 的 moby client 调用,Container/Image/Network/
  Volume 四个 Repo 共 26 个方法加 ctx

非测试代码的 context.Background() 从 71 处降到 14 处,剩下的是应用根 ctx、
有意的独立超时,以及几个瞬时本机操作的叶子函数。双平台 lint 保持 0 issues。

新增 23 处 context.WithoutCancel,其中两处的理由值得记:

备份上传断开取消链,是因为 ClearStorageExpired 按 mtime 保留最新 N 份,
上传被取消留下的截断文件会被当成最新的有效备份,把真正完好的旧备份挤出
保留窗口——静默的数据丢失。而 Put 失败路径没有任何远端清理逻辑,本地侧
反而有(任务队列独享 TMPDIR + defer RemoveAll),两边不对称。

Postgres.UserDrop 是 REASSIGN OWNED → DROP OWNED → DROP USER 三步,中途
取消会留下丢了所有权却没被删掉的用户;MySQL 权限变更后的 FLUSH 被跳过则
新授权根本不生效。

ImagePull 经核实不需要断开:moby 的 LayerDownloadManager 把未完成的层写在
临时文件里并在取消时删除,层只有完整校验后才注册进 layer store,取消不会
留下悬空层。

顺带删掉 8 处 //nolint:contextcheck,它们当初就是因为 Operator 接口没有
ctx 才挂上的。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 01:17:02 +08:00

139 lines
4.4 KiB
Go

package biz
import (
"context"
"log/slog"
"time"
"github.com/leonelquinteros/gotext"
"github.com/acepanel/panel/v3/pkg/types"
)
type BackupType string
const (
BackupTypePath BackupType = "path"
BackupTypeWebsite BackupType = "website"
BackupTypeMySQL BackupType = "mysql"
BackupTypePostgres BackupType = "postgresql"
BackupTypeClickHouse BackupType = "clickhouse"
BackupTypeRedis BackupType = "redis"
BackupTypeValkey BackupType = "valkey"
BackupTypePanel BackupType = "panel"
)
type BackupRepo interface {
List(typ BackupType) ([]*types.BackupFile, error)
GetStorage(id uint) (*BackupStorage, error)
Create(ctx context.Context, typ BackupType, target string, account uint) error
CreatePanel(ctx context.Context) error
Delete(ctx context.Context, typ BackupType, name string) error
Restore(ctx context.Context, typ BackupType, backup, target string) error
ClearExpired(path, prefix string, save uint) error
ClearStorageExpired(ctx context.Context, account uint, dir, prefix string, save uint) error
CutoffLog(ctx context.Context, path, target string) (string, error)
CutoffUpload(ctx context.Context, account uint, typ BackupType, name string, files []string) error
GetDefaultPath(typ BackupType) string
FixPanel(ctx context.Context) error
UpdatePanel(ctx context.Context, version, url, checksum string, progress func(string)) error
}
type BackupUsecase struct {
repo BackupRepo
log *slog.Logger
notify *NotifyUsecase
t *gotext.Locale
}
func NewBackupUsecase(notifyUsecase *NotifyUsecase, t *gotext.Locale, log *slog.Logger, backupRepo BackupRepo) *BackupUsecase {
return &BackupUsecase{
repo: backupRepo,
log: log,
notify: notifyUsecase,
t: t,
}
}
func (uc *BackupUsecase) List(typ BackupType) ([]*types.BackupFile, error) {
return uc.repo.List(typ)
}
func (uc *BackupUsecase) Create(ctx context.Context, typ BackupType, target string, account uint) error {
err := uc.repo.Create(ctx, typ, target, account)
if err == nil {
return nil
}
// 定时备份由 CLI 执行,命令返回即退出,异步通知来不及发出,必须同步发送
if sendErr := uc.notify.SendEventSync(ctx, NotifyEventBackup, uc.t.Get("[AcePanel] Backup Failed"), NotifyBody(uc.t.Get("backup task failed"), [][2]string{
{uc.t.Get("Type"), string(typ)},
{uc.t.Get("Target"), target},
{uc.t.Get("Error"), err.Error()},
{uc.t.Get("Time"), time.Now().Format(time.DateTime)},
})); sendErr != nil {
uc.log.Warn("failed to send backup failure notification", slog.Any("err", sendErr))
}
return err
}
func (uc *BackupUsecase) CreatePanel(ctx context.Context) error {
return uc.repo.CreatePanel(ctx)
}
func (uc *BackupUsecase) Delete(ctx context.Context, typ BackupType, name string) error {
if err := uc.repo.Delete(ctx, typ, name); err != nil {
return err
}
// 记录日志
uc.log.Info("backup deleted", slog.String("type", OperationTypeBackup), slog.Uint64("operator_id", operatorID(ctx)), slog.String("backup_type", string(typ)), slog.String("name", name))
return nil
}
func (uc *BackupUsecase) Restore(ctx context.Context, typ BackupType, backup, target string) error {
if err := uc.repo.Restore(ctx, typ, backup, target); err != nil {
return err
}
// 记录日志
uc.log.Info("backup restored",
slog.String("type", OperationTypeBackup),
slog.Uint64("operator_id", operatorID(ctx)),
slog.String("backup_type", string(typ)),
slog.String("target", target),
)
return nil
}
func (uc *BackupUsecase) ClearExpired(path, prefix string, save uint) error {
return uc.repo.ClearExpired(path, prefix, save)
}
func (uc *BackupUsecase) ClearStorageExpired(ctx context.Context, account uint, dir, prefix string, save uint) error {
return uc.repo.ClearStorageExpired(ctx, account, dir, prefix, save)
}
func (uc *BackupUsecase) CutoffLog(ctx context.Context, path, target string) (string, error) {
return uc.repo.CutoffLog(ctx, path, target)
}
func (uc *BackupUsecase) CutoffUpload(ctx context.Context, account uint, typ BackupType, name string, files []string) error {
return uc.repo.CutoffUpload(ctx, account, typ, name, files)
}
func (uc *BackupUsecase) GetDefaultPath(typ BackupType) string {
return uc.repo.GetDefaultPath(typ)
}
func (uc *BackupUsecase) FixPanel(ctx context.Context) error {
return uc.repo.FixPanel(ctx)
}
func (uc *BackupUsecase) UpdatePanel(ctx context.Context, version, url, checksum string, progress func(string)) error {
return uc.repo.UpdatePanel(ctx, version, url, checksum, progress)
}