Files
1Panel/agent/app/service/backup_compose.go
2026-07-15 18:06:19 +08:00

708 lines
23 KiB
Go

package service
import (
"context"
"encoding/json"
"fmt"
"io/fs"
"os"
"path"
"path/filepath"
"sort"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/common"
"github.com/1Panel-dev/1Panel/agent/utils/compose"
dockerUtils "github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/1Panel-dev/1Panel/agent/utils/files"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client"
)
type composeBackupFile struct {
OriginalPath string `json:"originalPath"`
FileName string `json:"fileName"`
RelativePath string `json:"relativePath,omitempty"`
BackupPath string `json:"backupPath"`
}
type composeBackupMeta struct {
ComposeName string `json:"composeName"`
ComposePath string `json:"composePath"`
CreatedAt string `json:"createdAt"`
Files []composeBackupFile `json:"files"`
Containers []string `json:"containers"`
}
type composeBackupContext struct {
req dto.CommonBackup
composeName string
composePath string
composeFiles []string
composeDir string
fileOp files.FileOp
dockerClient *client.Client
stopped bool
backupDir string
fileName string
filePath string
tmpDir string
meta composeBackupMeta
}
type composeRecoverContext struct {
req dto.CommonRecover
fileOp files.FileOp
tmpDir string
meta composeBackupMeta
composeName string
targetDir string
composePath string
enqueued bool
}
func (u *BackupService) ComposeBackup(req dto.CommonBackup) error {
timeNow := time.Now().Format(constant.DateTimeSlimLayout) + common.RandStrAndNum(5)
fileName := req.FileName
if fileName == "" {
fileName = fmt.Sprintf("%s_%s.tar.gz", req.Name, timeNow)
}
if !strings.HasSuffix(fileName, ".tar.gz") {
fileName += ".tar.gz"
}
itemDir := fmt.Sprintf("compose/%s", req.Name)
backupDir := path.Join(global.Dir.LocalBackupDir, itemDir)
record := &model.BackupRecord{
Type: req.Type,
Name: req.Name,
SourceAccountIDs: "1",
DownloadAccountID: 1,
FileDir: itemDir,
FileName: fileName,
TaskID: req.TaskID,
Status: constant.StatusWaiting,
Description: req.Description,
}
if err := backupRepo.CreateRecord(record); err != nil {
global.LOG.Errorf("save compose backup record failed, err: %v", err)
return err
}
if err := handleComposeBackup(req, nil, record.ID, backupDir, fileName); err != nil {
markBackupFailed(record.ID, err)
return err
}
return nil
}
func (u *BackupService) ComposeRecover(req dto.CommonRecover) error {
return handleComposeRecover(req, nil)
}
func handleComposeBackup(req dto.CommonBackup, parentTask *task.Task, recordID uint, backupDir, fileName string) error {
composeCtx, err := newComposeBackupContext(req, backupDir, fileName)
if err != nil {
return err
}
containerNames, err := loadComposeContainerNames(composeCtx)
if err != nil {
composeCtx.close()
return err
}
backupTask := parentTask
if backupTask == nil {
backupTask, err = task.NewTaskWithOps(composeCtx.composeName, task.TaskBackup, task.TaskScopeBackup, req.TaskID, 1)
if err != nil {
return err
}
}
if req.StopBefore {
backupTask.AddSubTaskWithOps(i18n.GetMsgByKey("ComposeBackupStop"), func(t *task.Task) error {
return stepStopComposeForBackup(composeCtx)
}, func(t *task.Task) {
_ = stepStartComposeAfterBackup(composeCtx)
}, 3, time.Hour)
}
backupTask.AddSubTaskWithOps(i18n.GetMsgByKey("ComposeBackupPrepare"), func(t *task.Task) error { return stepPrepareComposeBackup(composeCtx) }, nil, 3, time.Hour)
backupTask.AddSubTaskWithOps(i18n.GetMsgByKey("ComposeBackupFiles"), func(t *task.Task) error { return stepBackupComposeFiles(composeCtx) }, nil, 3, time.Hour)
backupTask.AddSubTaskWithOps(i18n.GetMsgByKey("ComposeBackupContainers"), func(t *task.Task) error { return nil }, nil, 3, time.Hour)
for _, containerName := range containerNames {
backupFileName := fmt.Sprintf("%s.tar.gz", sanitizeComposeFileName(containerName))
backupFile := path.Join(composeCtx.tmpDir, "containers", backupFileName)
if err := handleContainerBackup(containerName, backupTask, 0, path.Dir(backupFile), path.Base(backupFile), "", "", false); err != nil {
return err
}
composeCtx.meta.Containers = append(composeCtx.meta.Containers, path.Join("containers", backupFileName))
}
backupTask.AddSubTaskWithOps(i18n.GetMsgByKey("ComposeBackupMeta"), func(t *task.Task) error { return stepWriteComposeBackupMeta(composeCtx) }, nil, 3, time.Hour)
backupTask.AddSubTaskWithOps(task.GetTaskName(composeCtx.composeName, task.TaskBackup, task.TaskScopeBackup), func(t *task.Task) error {
return stepPackComposeBackup(composeCtx)
}, nil, 3, time.Hour)
if req.StopBefore {
backupTask.AddSubTaskWithOps(i18n.GetMsgByKey("ComposeBackupStart"), func(t *task.Task) error {
return stepStartComposeAfterBackup(composeCtx)
}, nil, 3, time.Hour)
}
backupTask.AddSubTaskWithOps(i18n.GetMsgByKey("ComposeBackupCleanup"), func(t *task.Task) error {
composeCtx.close()
return nil
}, nil, 0, time.Hour)
if parentTask != nil {
return nil
}
go func() {
defer composeCtx.close()
if err := backupTask.Execute(); err != nil {
markBackupFailed(recordID, err)
return
}
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
}()
return nil
}
func loadComposeContainerNames(composeCtx *composeBackupContext) ([]string, error) {
options := container.ListOptions{All: true}
options.Filters = filters.NewArgs(filters.Arg("label", composeProjectLabel+"="+composeCtx.composeName))
containers, err := composeCtx.dockerClient.ContainerList(context.Background(), options)
if err != nil {
return nil, err
}
names := make([]string, 0, len(containers))
for _, item := range containers {
if len(item.Names) == 0 {
continue
}
names = append(names, strings.TrimPrefix(item.Names[0], "/"))
}
sort.Strings(names)
return names, nil
}
func handleComposeRecover(req dto.CommonRecover, parentTask *task.Task) error {
var recoverCtx *composeRecoverContext
recoverTask := parentTask
var err error
if recoverTask == nil {
if isImportRecover(req) {
taskName := i18n.GetMsgByKey("TaskImport") + i18n.GetMsgByKey("Compose")
recoverTask, err = task.NewTask(taskName, task.TaskImport, task.TaskScopeBackup, req.TaskID, 1)
if err != nil {
return err
}
} else {
taskName := req.Name
if taskName == "" {
taskName = "compose"
}
recoverTask, err = task.NewTaskWithOps(taskName, task.TaskRecover, task.TaskScopeBackup, req.TaskID, 1)
if err != nil {
return err
}
}
}
timeout := loadRecoverTimeout(req.Timeout)
recoverTask.AddSubTaskWithOps(i18n.GetMsgByKey("ComposeRecoverPrepare"), func(t *task.Task) error {
ctx, err := newComposeRecoverContext(req)
if err != nil {
return err
}
recoverCtx = ctx
if err := stepPrepareComposeRecover(recoverCtx); err != nil {
recoverCtx.close()
recoverCtx = nil
return err
}
return nil
}, func(t *task.Task) {
if recoverCtx != nil {
recoverCtx.close()
recoverCtx = nil
}
}, 3, timeout)
recoverTask.AddSubTaskWithOps(i18n.GetMsgByKey("ComposeRecoverExtract"), func(t *task.Task) error { return stepExtractComposeRecover(recoverCtx) }, nil, 3, timeout)
recoverTask.AddSubTaskWithOps(i18n.GetMsgByKey("ComposeRecoverMeta"), func(t *task.Task) error {
if err := stepLoadComposeRecoverMeta(recoverCtx); err != nil {
return err
}
t.Log(i18n.GetMsgWithMap("ComposeRecoverMetaLogName", map[string]interface{}{
"name": recoverCtx.composeName,
}))
t.Log(i18n.GetMsgWithMap("ComposeRecoverMetaLogPath", map[string]interface{}{
"backupPath": recoverCtx.meta.ComposePath,
"targetDir": recoverCtx.targetDir,
}))
t.Log(i18n.GetMsgWithMap("ComposeRecoverMetaLogCount", map[string]interface{}{
"files": len(recoverCtx.meta.Files),
"containers": len(recoverCtx.meta.Containers),
}))
return nil
}, nil, 3, timeout)
recoverTask.AddSubTaskWithOps(i18n.GetMsgByKey("ComposeRecoverFiles"), func(t *task.Task) error { return stepRestoreComposeFiles(recoverCtx) }, nil, 3, timeout)
recoverTask.AddSubTaskWithOps(i18n.GetMsgByKey("ComposeRecoverContainers"), func(t *task.Task) error {
if recoverCtx.enqueued {
return nil
}
containerItems := make([]string, 0, len(recoverCtx.meta.Containers))
for _, item := range recoverCtx.meta.Containers {
backupItem := item
filePath, err := safeJoinWithinBase(recoverCtx.tmpDir, backupItem)
if err != nil {
return fmt.Errorf("invalid container backup path %q, err: %v", backupItem, err)
}
if !recoverCtx.fileOp.Stat(filePath) {
return fmt.Errorf("container backup file not found: %s", backupItem)
}
containerItems = append(containerItems, backupItem)
}
for _, backupItem := range containerItems {
filePath, err := safeJoinWithinBase(recoverCtx.tmpDir, backupItem)
if err != nil {
return fmt.Errorf("invalid container backup path %q, err: %v", backupItem, err)
}
containerLabel := strings.TrimSuffix(path.Base(backupItem), ".tar.gz")
containerReq := recoverCtx.req
containerReq.Type = "container"
containerReq.Name = containerLabel
containerReq.DetailName = ""
containerReq.File = filePath
containerReq.Secret = ""
if err := handleContainerRecover(containerReq, recoverTask); err != nil {
return err
}
}
recoverTask.AddSubTaskWithOps(i18n.GetMsgByKey("ComposeRecoverRecord"), func(t *task.Task) error {
return stepSaveComposeRecord(recoverCtx)
}, nil, 3, timeout)
recoverTask.AddSubTaskWithOps(i18n.GetMsgByKey("ComposeRecoverCleanup"), func(t *task.Task) error {
if recoverCtx != nil {
recoverCtx.close()
recoverCtx = nil
}
return nil
}, nil, 0, timeout)
recoverCtx.enqueued = true
return nil
}, nil, 3, timeout)
if parentTask != nil {
return nil
}
go func() {
_ = recoverTask.Execute()
}()
return nil
}
func newComposeBackupContext(req dto.CommonBackup, backupDir, fileName string) (*composeBackupContext, error) {
if req.Name == "" {
return nil, fmt.Errorf("compose name is required")
}
dockerClient, err := dockerUtils.NewDockerClient()
if err != nil {
return nil, err
}
composePath, composeFiles, err := loadComposePathAndFiles(req.Name, dockerClient)
if err != nil {
_ = dockerClient.Close()
return nil, err
}
filePath := path.Join(backupDir, fileName)
tmpDir := path.Join(path.Dir(filePath), strings.TrimSuffix(path.Base(filePath), ".tar.gz"))
ctx := &composeBackupContext{
req: req,
composeName: req.Name,
composePath: composePath,
composeFiles: composeFiles,
composeDir: path.Dir(composeFiles[0]),
fileOp: files.NewFileOp(),
dockerClient: dockerClient,
backupDir: backupDir,
fileName: fileName,
filePath: filePath,
tmpDir: tmpDir,
meta: composeBackupMeta{
ComposeName: req.Name,
ComposePath: composePath,
CreatedAt: time.Now().Format(constant.DateTimeLayout),
Files: make([]composeBackupFile, 0),
Containers: make([]string, 0),
},
}
return ctx, nil
}
func loadComposePathAndFiles(composeName string, dockerClient *client.Client) (string, []string, error) {
composeRecord, _ := composeRepo.GetRecord(repo.WithByName(composeName))
if composeRecord.ID == 0 {
composeRecord, _ = composeRepo.GetRecord(repo.WithByName(strings.ToLower(composeName)))
}
composePath := composeRecord.Path
if composePath == "" {
options := container.ListOptions{All: true}
options.Filters = filters.NewArgs(filters.Arg("label", composeProjectLabel))
list, err := dockerClient.ContainerList(context.Background(), options)
if err != nil {
return "", nil, err
}
if len(list) == 0 {
return "", nil, fmt.Errorf("compose %s not found", composeName)
}
var targetContainer *container.Summary
for i := range list {
if strings.EqualFold(list[i].Labels[composeProjectLabel], composeName) {
targetContainer = &list[i]
break
}
}
if targetContainer == nil {
return "", nil, fmt.Errorf("compose %s not found", composeName)
}
config := targetContainer.Labels[composeConfigLabel]
workdir := targetContainer.Labels[composeWorkdirLabel]
if len(config) != 0 && len(workdir) != 0 && strings.Contains(config, workdir) {
composePath = config
} else {
composePath = workdir
}
}
composeFiles := normalizeComposeFiles(composePath)
if len(composeFiles) == 0 {
return "", nil, fmt.Errorf("compose file not found for %s", composeName)
}
return composePath, composeFiles, nil
}
func normalizeComposeFiles(composePath string) []string {
items := strings.Split(composePath, ",")
result := make([]string, 0)
seen := make(map[string]struct{})
for _, item := range items {
item = strings.TrimSpace(item)
if item == "" {
continue
}
stat, err := os.Stat(item)
if err == nil && stat.IsDir() {
item = path.Join(item, "docker-compose.yml")
}
if _, err := os.Stat(item); err != nil {
continue
}
if _, ok := seen[item]; ok {
continue
}
seen[item] = struct{}{}
result = append(result, item)
}
sort.Strings(result)
return result
}
func (c *composeBackupContext) close() {
if c.dockerClient != nil {
_ = c.dockerClient.Close()
c.dockerClient = nil
}
if c.tmpDir != "" {
_ = os.RemoveAll(c.tmpDir)
c.tmpDir = ""
}
}
func stepPrepareComposeBackup(composeCtx *composeBackupContext) error {
if err := os.MkdirAll(composeCtx.backupDir, os.ModePerm); err != nil {
return fmt.Errorf("mkdir %s failed, err: %v", composeCtx.backupDir, err)
}
_ = os.RemoveAll(composeCtx.tmpDir)
if err := os.MkdirAll(path.Join(composeCtx.tmpDir, "compose_files"), os.ModePerm); err != nil {
return err
}
if err := os.MkdirAll(path.Join(composeCtx.tmpDir, "containers"), os.ModePerm); err != nil {
return err
}
return nil
}
func stepStopComposeForBackup(composeCtx *composeBackupContext) error {
if composeCtx.stopped {
return nil
}
options := container.ListOptions{All: false}
options.Filters = filters.NewArgs(filters.Arg("label", composeProjectLabel+"="+composeCtx.composeName))
runningList, err := composeCtx.dockerClient.ContainerList(context.Background(), options)
if err != nil {
return err
}
if len(runningList) == 0 {
return nil
}
if stdout, err := compose.Operate(composeCtx.composePath, "stop"); err != nil {
return fmt.Errorf("docker-compose stop failed, std: %s, err: %v", stdout, err)
}
composeCtx.stopped = true
return nil
}
func stepStartComposeAfterBackup(composeCtx *composeBackupContext) error {
if !composeCtx.stopped {
return nil
}
if stdout, err := compose.Up(composeCtx.composePath); err != nil {
return fmt.Errorf("docker-compose up failed, std: %s, err: %v", stdout, err)
}
composeCtx.stopped = false
return nil
}
func stepBackupComposeFiles(composeCtx *composeBackupContext) error {
for i, filePath := range composeCtx.composeFiles {
backupName := fmt.Sprintf("%02d_%s", i, path.Base(filePath))
backupPath := path.Join(composeCtx.tmpDir, "compose_files", backupName)
content, err := os.ReadFile(filePath)
if err != nil {
return err
}
if err := composeCtx.fileOp.SaveFile(backupPath, string(content), fs.ModePerm); err != nil {
return err
}
relativePath := path.Base(filePath)
if composeCtx.composeDir != "" {
rel, relErr := filepath.Rel(composeCtx.composeDir, filePath)
if relErr == nil {
rel = filepath.ToSlash(rel)
if rel != "" && rel != "." && !strings.HasPrefix(rel, "../") {
relativePath = rel
}
}
}
composeCtx.meta.Files = append(composeCtx.meta.Files, composeBackupFile{
OriginalPath: filePath,
FileName: path.Base(filePath),
RelativePath: relativePath,
BackupPath: path.Join("compose_files", backupName),
})
}
if len(composeCtx.composeFiles) != 0 {
envPath := path.Join(path.Dir(composeCtx.composeFiles[0]), ".env")
if composeCtx.fileOp.Stat(envPath) {
envContent, err := os.ReadFile(envPath)
if err != nil {
return err
}
if err := composeCtx.fileOp.SaveFile(path.Join(composeCtx.tmpDir, "compose_files", ".env"), string(envContent), fs.ModePerm); err != nil {
return err
}
}
}
return nil
}
func stepWriteComposeBackupMeta(composeCtx *composeBackupContext) error {
metaBytes, err := json.MarshalIndent(composeCtx.meta, "", " ")
if err != nil {
return err
}
return composeCtx.fileOp.SaveFile(path.Join(composeCtx.tmpDir, "compose_meta.json"), string(metaBytes), fs.ModePerm)
}
func stepPackComposeBackup(composeCtx *composeBackupContext) error {
return composeCtx.fileOp.TarGzCompressPro(true, composeCtx.tmpDir, composeCtx.filePath, composeCtx.req.Secret, "")
}
func newComposeRecoverContext(req dto.CommonRecover) (*composeRecoverContext, error) {
tmpDir := path.Join(path.Dir(req.File), strings.TrimSuffix(path.Base(req.File), ".tar.gz"))
ctx := &composeRecoverContext{
req: req,
fileOp: files.NewFileOp(),
tmpDir: tmpDir,
meta: composeBackupMeta{
Files: make([]composeBackupFile, 0),
Containers: make([]string, 0),
},
}
return ctx, nil
}
func (c *composeRecoverContext) close() {
if c.tmpDir != "" {
_ = os.RemoveAll(c.tmpDir)
}
}
func stepPrepareComposeRecover(recoverCtx *composeRecoverContext) error {
if !recoverCtx.fileOp.Stat(recoverCtx.req.File) {
return buserr.WithName("ErrFileNotFound", recoverCtx.req.File)
}
_ = os.RemoveAll(recoverCtx.tmpDir)
return nil
}
func stepExtractComposeRecover(recoverCtx *composeRecoverContext) error {
return recoverCtx.fileOp.TarGzExtractPro(recoverCtx.req.File, path.Dir(recoverCtx.req.File), recoverCtx.req.Secret)
}
func stepLoadComposeRecoverMeta(recoverCtx *composeRecoverContext) error {
metaPath := path.Join(recoverCtx.tmpDir, "compose_meta.json")
if !recoverCtx.fileOp.Stat(metaPath) {
return fmt.Errorf("compose_meta.json not found in backup file")
}
metaBytes, err := os.ReadFile(metaPath)
if err != nil {
return err
}
if err := json.Unmarshal(metaBytes, &recoverCtx.meta); err != nil {
return fmt.Errorf("unmarshal compose_meta.json failed, err: %v", err)
}
recoverCtx.composeName = strings.TrimSpace(recoverCtx.req.Name)
if recoverCtx.composeName == "" {
recoverCtx.composeName = strings.TrimSpace(recoverCtx.meta.ComposeName)
}
if recoverCtx.composeName == "" {
return fmt.Errorf("compose name not found in recover request or backup file")
}
recoverCtx.targetDir = resolveComposeRecoverTargetDir(recoverCtx.meta, recoverCtx.composeName)
return nil
}
func resolveComposeRecoverTargetDir(meta composeBackupMeta, composeName string) string {
composePath := strings.TrimSpace(meta.ComposePath)
if composePath != "" {
items := strings.Split(composePath, ",")
for _, item := range items {
p := strings.TrimSpace(item)
if p == "" {
continue
}
ext := strings.ToLower(path.Ext(p))
if ext == ".yml" || ext == ".yaml" {
return path.Dir(p)
}
return p
}
}
return path.Join(global.Dir.DataDir, "docker/compose", composeName)
}
func safeJoinWithinBase(baseDir, name string) (string, error) {
base := filepath.Clean(baseDir)
candidate := strings.TrimSpace(name)
candidate = strings.ReplaceAll(candidate, "\\", "/")
candidate = filepath.Clean(filepath.FromSlash(candidate))
if candidate == "" || candidate == "." {
return "", fmt.Errorf("invalid path: empty")
}
if filepath.IsAbs(candidate) {
return "", fmt.Errorf("invalid path %q: absolute path is not allowed", name)
}
if candidate == ".." || strings.HasPrefix(candidate, ".."+string(filepath.Separator)) {
return "", fmt.Errorf("invalid path %q: path escapes base directory", name)
}
resolved := filepath.Clean(filepath.Join(base, candidate))
rel, err := filepath.Rel(base, resolved)
if err != nil {
return "", fmt.Errorf("resolve path %q failed, err: %v", name, err)
}
if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return "", fmt.Errorf("invalid path %q: path escapes base directory", name)
}
return resolved, nil
}
func stepRestoreComposeFiles(recoverCtx *composeRecoverContext) error {
if recoverCtx.targetDir != "" {
_ = os.RemoveAll(recoverCtx.targetDir)
}
if err := os.MkdirAll(recoverCtx.targetDir, os.ModePerm); err != nil {
return err
}
restored := make([]string, 0, len(recoverCtx.meta.Files))
for _, item := range recoverCtx.meta.Files {
backupPath, err := safeJoinWithinBase(recoverCtx.tmpDir, item.BackupPath)
if err != nil {
return fmt.Errorf("invalid compose backup path %q, err: %v", item.BackupPath, err)
}
if !recoverCtx.fileOp.Stat(backupPath) {
continue
}
targetName := item.FileName
if item.RelativePath != "" {
targetName = item.RelativePath
}
if targetName == "" {
targetName = path.Base(item.OriginalPath)
}
if targetName == "" {
targetName = "docker-compose.yml"
}
targetPath, err := safeJoinWithinBase(recoverCtx.targetDir, targetName)
if err != nil {
return fmt.Errorf("invalid compose target path %q, err: %v", targetName, err)
}
if err := os.MkdirAll(path.Dir(targetPath), os.ModePerm); err != nil {
return err
}
content, err := os.ReadFile(backupPath)
if err != nil {
return err
}
if err := recoverCtx.fileOp.SaveFile(targetPath, string(content), fs.ModePerm); err != nil {
return err
}
restored = append(restored, targetPath)
}
envPath := path.Join(recoverCtx.tmpDir, "compose_files", ".env")
if recoverCtx.fileOp.Stat(envPath) {
envContent, err := os.ReadFile(envPath)
if err != nil {
return err
}
if err := recoverCtx.fileOp.SaveFile(path.Join(recoverCtx.targetDir, ".env"), string(envContent), fs.ModePerm); err != nil {
return err
}
}
if len(restored) == 0 {
defaultPath := path.Join(recoverCtx.targetDir, "docker-compose.yml")
if !recoverCtx.fileOp.Stat(defaultPath) {
return fmt.Errorf("compose file not found in backup data")
}
restored = append(restored, defaultPath)
}
sort.Strings(restored)
recoverCtx.composePath = strings.Join(restored, ",")
return nil
}
func stepSaveComposeRecord(recoverCtx *composeRecoverContext) error {
if recoverCtx.composePath == "" {
recoverCtx.composePath = path.Join(recoverCtx.targetDir, "docker-compose.yml")
}
recordName := strings.ToLower(recoverCtx.composeName)
record, _ := composeRepo.GetRecord(repo.WithByName(recordName))
if record.ID == 0 {
return composeRepo.CreateRecord(&model.Compose{Name: recordName, Path: recoverCtx.composePath})
}
return composeRepo.UpdateRecord(recordName, map[string]interface{}{"path": recoverCtx.composePath})
}
func sanitizeComposeFileName(in string) string {
name := strings.TrimSpace(in)
name = strings.ReplaceAll(name, "/", "_")
name = strings.ReplaceAll(name, ":", "_")
if name == "" {
return "container"
}
return name
}