fix: 修复备份工具的多处问题
- go.mod: 将非法版本号 go 1.26.4 改为 go 1.26 - engine: 压缩失败时删除残留的半截 tar.gz 文件 - config: 支持仅按天数保留(count 和 days 均未设时才用默认 count=7) - engine: docker compose stop 失败时也尝试重启,避免容器停摆 - list: 仅显示真正的备份归档文件(新增导出 IsBackupArchive) - systemd: 移除 PrivateTmp,暂存目录改到安装目录内,避免私有 /tmp 被大目录撑爆
This commit is contained in:
@@ -54,13 +54,13 @@ sudo mkdir -p /opt/docker-compose-backup
|
|||||||
sudo cp docker-compose-backup-linux-arm64 /opt/docker-compose-backup/docker-compose-backup
|
sudo cp docker-compose-backup-linux-arm64 /opt/docker-compose-backup/docker-compose-backup
|
||||||
sudo chmod +x /opt/docker-compose-backup/docker-compose-backup
|
sudo chmod +x /opt/docker-compose-backup/docker-compose-backup
|
||||||
sudo cp config.yaml /opt/docker-compose-backup/config.yaml
|
sudo cp config.yaml /opt/docker-compose-backup/config.yaml
|
||||||
sudo mkdir -p /opt/docker-compose-backup/backups
|
sudo mkdir -p /opt/docker-compose-backup/backups /opt/docker-compose-backup/tmp
|
||||||
sudo cp docker-compose-backup.service /etc/systemd/system/
|
sudo cp docker-compose-backup.service /etc/systemd/system/
|
||||||
sudo systemctl daemon-reload
|
sudo systemctl daemon-reload
|
||||||
sudo systemctl enable --now docker-compose-backup
|
sudo systemctl enable --now docker-compose-backup
|
||||||
```
|
```
|
||||||
|
|
||||||
The example config uses `/opt/docker-compose-backup/backups`, which is allowed by the service sandbox.
|
The example config uses `/opt/docker-compose-backup/backups`, which is allowed by the service sandbox. The unit's `ReadWritePaths` covers the whole install dir, so set `global.temp_dir` to a subdirectory there (e.g. `/opt/docker-compose-backup/tmp`). The unit intentionally does **not** enable `PrivateTmp`, since a private `/tmp` is often small and would be exhausted when copying large project directories.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ See [config.example.yaml](config.example.yaml) for a full annotated example.
|
|||||||
```yaml
|
```yaml
|
||||||
global:
|
global:
|
||||||
backup_dir: /opt/docker-compose-backup/backups
|
backup_dir: /opt/docker-compose-backup/backups
|
||||||
temp_dir: /tmp/docker-backup # optional, defaults to OS temp
|
temp_dir: /opt/docker-compose-backup/tmp # optional, defaults to OS temp
|
||||||
|
|
||||||
projects:
|
projects:
|
||||||
- name: myapp
|
- name: myapp
|
||||||
|
|||||||
+1
-1
@@ -56,7 +56,7 @@ func runList(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
var backups []string
|
var backups []string
|
||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
if !e.IsDir() {
|
if !e.IsDir() && backup.IsBackupArchive(proj.Name, e.Name()) {
|
||||||
backups = append(backups, e.Name())
|
backups = append(backups, e.Name())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -1,7 +1,10 @@
|
|||||||
# docker-compose-backup example configuration
|
# docker-compose-backup example configuration
|
||||||
global:
|
global:
|
||||||
backup_dir: /opt/docker-compose-backup/backups
|
backup_dir: /opt/docker-compose-backup/backups
|
||||||
# temp_dir: /tmp/docker-compose-backup # defaults to OS temp dir
|
# Staging area for the pre-compression copy. Defaults to the OS temp dir.
|
||||||
|
# Under the bundled systemd unit, set this inside the install dir so it stays
|
||||||
|
# within the sandbox's writable path (the unit no longer uses PrivateTmp).
|
||||||
|
temp_dir: /opt/docker-compose-backup/tmp
|
||||||
|
|
||||||
projects:
|
projects:
|
||||||
- name: myapp
|
- name: myapp
|
||||||
|
|||||||
@@ -19,8 +19,11 @@ User=root
|
|||||||
NoNewPrivileges=yes
|
NoNewPrivileges=yes
|
||||||
ProtectSystem=strict
|
ProtectSystem=strict
|
||||||
ProtectHome=yes
|
ProtectHome=yes
|
||||||
ReadWritePaths=/opt/docker-compose-backup /tmp
|
# The backup copies whole project directories into a staging area before
|
||||||
PrivateTmp=yes
|
# compressing. Keep staging inside the writable install dir (see temp_dir in
|
||||||
|
# config.yaml) rather than /tmp, and do NOT use PrivateTmp — a private /tmp is
|
||||||
|
# often small/tmpfs-backed and can be exhausted by large project copies.
|
||||||
|
ReadWritePaths=/opt/docker-compose-backup
|
||||||
ProtectKernelTunables=yes
|
ProtectKernelTunables=yes
|
||||||
ProtectKernelModules=yes
|
ProtectKernelModules=yes
|
||||||
ProtectControlGroups=yes
|
ProtectControlGroups=yes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
module git.misaka.ren/M1saka/docker_backup
|
module git.misaka.ren/M1saka/docker_backup
|
||||||
|
|
||||||
go 1.26.4
|
go 1.26
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/aws/aws-sdk-go-v2 v1.42.1
|
github.com/aws/aws-sdk-go-v2 v1.42.1
|
||||||
|
|||||||
@@ -83,9 +83,12 @@ func (e *Engine) backupOne(ctx context.Context, proj config.ProjectConfig) Resul
|
|||||||
|
|
||||||
executor := NewComposeExecutor(proj.Path, proj.ComposeFile)
|
executor := NewComposeExecutor(proj.Path, proj.ComposeFile)
|
||||||
|
|
||||||
// 1. Stop the compose project.
|
// 1. Stop the compose project. If the stop is interrupted midway
|
||||||
|
// (e.g. the context is cancelled), containers may be left stopped, so
|
||||||
|
// always attempt a restart before returning.
|
||||||
stopStart := time.Now()
|
stopStart := time.Now()
|
||||||
if err := executor.Stop(ctx); err != nil {
|
if err := executor.Stop(ctx); err != nil {
|
||||||
|
_ = restartCompose(executor)
|
||||||
result.Error = fmt.Errorf("stop: %w", err)
|
result.Error = fmt.Errorf("stop: %w", err)
|
||||||
result.Duration = time.Since(start)
|
result.Duration = time.Since(start)
|
||||||
return result
|
return result
|
||||||
@@ -226,9 +229,14 @@ func createTarGz(ctx context.Context, srcDir, dstPath string) error {
|
|||||||
|
|
||||||
closeErr := closeTarGz(tw, gw, f)
|
closeErr := closeTarGz(tw, gw, f)
|
||||||
if walkErr != nil {
|
if walkErr != nil {
|
||||||
|
_ = os.Remove(dstPath)
|
||||||
return walkErr
|
return walkErr
|
||||||
}
|
}
|
||||||
return closeErr
|
if closeErr != nil {
|
||||||
|
_ = os.Remove(dstPath)
|
||||||
|
return closeErr
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func closeTarGz(tw *tar.Writer, gw *gzip.Writer, f *os.File) error {
|
func closeTarGz(tw *tar.Writer, gw *gzip.Writer, f *os.File) error {
|
||||||
|
|||||||
@@ -87,5 +87,10 @@ func ApplyRetention(backupDir, projectName string, policy RetentionPolicy) (int,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func isBackupArchive(projectName, name string) bool {
|
func isBackupArchive(projectName, name string) bool {
|
||||||
|
return IsBackupArchive(projectName, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsBackupArchive reports whether name is a backup archive for the project.
|
||||||
|
func IsBackupArchive(projectName, name string) bool {
|
||||||
return strings.HasPrefix(name, projectName+"-") && strings.HasSuffix(name, ".tar.gz")
|
return strings.HasPrefix(name, projectName+"-") && strings.HasSuffix(name, ".tar.gz")
|
||||||
}
|
}
|
||||||
@@ -85,7 +85,9 @@ func (c *Config) Validate() error {
|
|||||||
if p.ComposeFile == "" {
|
if p.ComposeFile == "" {
|
||||||
p.ComposeFile = "docker-compose.yml"
|
p.ComposeFile = "docker-compose.yml"
|
||||||
}
|
}
|
||||||
if p.Retention.Count <= 0 {
|
// Apply the default count only when no retention policy is set at all.
|
||||||
|
// This lets a user configure days-only retention (count omitted).
|
||||||
|
if p.Retention.Count <= 0 && p.Retention.Days <= 0 {
|
||||||
p.Retention.Count = DefaultRetentionCount
|
p.Retention.Count = DefaultRetentionCount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,34 @@ projects:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoadDaysOnlyRetention(t *testing.T) {
|
||||||
|
yaml := `
|
||||||
|
global:
|
||||||
|
backup_dir: /tmp/backups
|
||||||
|
projects:
|
||||||
|
- name: testapp
|
||||||
|
path: /opt/testapp
|
||||||
|
retention:
|
||||||
|
days: 30
|
||||||
|
`
|
||||||
|
dir := t.TempDir()
|
||||||
|
path := filepath.Join(dir, "config.yaml")
|
||||||
|
if err := os.WriteFile(path, []byte(yaml), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
cfg, err := Load(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load: %v", err)
|
||||||
|
}
|
||||||
|
p := cfg.Projects[0]
|
||||||
|
if p.Retention.Count != 0 {
|
||||||
|
t.Errorf("count should stay 0 for days-only retention, got %d", p.Retention.Count)
|
||||||
|
}
|
||||||
|
if p.Retention.Days != 30 {
|
||||||
|
t.Errorf("days = %d", p.Retention.Days)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestLoadMissingBackupDir(t *testing.T) {
|
func TestLoadMissingBackupDir(t *testing.T) {
|
||||||
yaml := `
|
yaml := `
|
||||||
projects:
|
projects:
|
||||||
|
|||||||
Reference in New Issue
Block a user