mirror of
https://github.com/tnb-labs/panel.git
synced 2026-10-28 03:27:53 +08:00
上一轮打通 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>
139 lines
4.4 KiB
Go
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)
|
|
}
|