Skip to content

Commit 39e7970

Browse files
committed
feat: add backup purging
1 parent 31c05c7 commit 39e7970

2 files changed

Lines changed: 94 additions & 1 deletion

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ Optional args:
4545
- `--delete`: delete the `backup` folder after upload. This requires `rw` permission on the volume.
4646
- `--replace`: always save into the same prefix, replacing the previous uploaded backup.
4747
- `--compress`: use zstd to compress files before uploading.
48+
- `--purge [days]`: purge older backups after defined days.
4849

4950
## Restore a backup
5051

schedule.go

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,18 @@ import (
55
"fmt"
66
"log"
77
"path"
8+
"strings"
89
"time"
910

1011
"github.com/aws/aws-sdk-go-v2/service/s3"
12+
s3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
1113
"github.com/redis/go-redis/v9"
1214
"github.com/robfig/cron/v3"
1315
"github.com/urfave/cli/v3"
1416
)
1517

18+
const BACKUP_FORMAT = "2006-01-02_15-04-05"
19+
1620
var ScheduleCmd = &cli.Command{
1721
Name: "schedule",
1822
Usage: "Execute backup on a schedule",
@@ -35,6 +39,10 @@ var ScheduleCmd = &cli.Command{
3539
Name: "compress",
3640
Usage: "Compress files using Zstd",
3741
},
42+
&cli.IntFlag{
43+
Name: "purge",
44+
Usage: "Purge backup older than defined number of days",
45+
},
3846
},
3947
Action: func(ctx context.Context, c *cli.Command) error {
4048
s3client := ctx.Value("s3client").(*s3.Client)
@@ -44,7 +52,7 @@ var ScheduleCmd = &cli.Command{
4452
_, err := scheduler.AddFunc(c.StringArg("cron"), func() {
4553
prefix := c.String("prefix")
4654
if !c.Bool("replace") {
47-
date := time.Now().Format("2006-01-02_15-04-05")
55+
date := time.Now().Format(BACKUP_FORMAT)
4856
prefix = path.Join(prefix, date)
4957
}
5058

@@ -65,9 +73,93 @@ var ScheduleCmd = &cli.Command{
6573
return fmt.Errorf("cannot register scheduler: %s", err)
6674
}
6775

76+
if c.IsSet("purge") {
77+
scheduler.AddFunc("0 * * * *", func() {
78+
purgeBackups(s3client, c.String("bucket"), c.String("prefix"), c.Int("purge"))
79+
})
80+
}
81+
6882
log.Println("Start scheduler")
6983
scheduler.Run()
7084

7185
return nil
7286
},
7387
}
88+
89+
func purgeBackups(s3client *s3.Client, bucket, prefix string, day int) error {
90+
prefixWithSuffix := prefix
91+
if !strings.HasSuffix(prefixWithSuffix, "/") {
92+
prefixWithSuffix += "/"
93+
}
94+
95+
paginator := s3.NewListObjectsV2Paginator(s3client, &s3.ListObjectsV2Input{
96+
Bucket: new(bucket),
97+
Prefix: new(prefixWithSuffix),
98+
Delimiter: new("/"),
99+
})
100+
ctx := context.Background()
101+
limit := time.Now().Add(-time.Duration(day) * time.Hour * 24)
102+
103+
for paginator.HasMorePages() {
104+
page, err := paginator.NextPage(ctx)
105+
if err != nil {
106+
return fmt.Errorf("cannot paginate s3: %w", err)
107+
}
108+
109+
for _, obj := range page.CommonPrefixes {
110+
pf := path.Base(*obj.Prefix)
111+
ts, err := time.Parse(BACKUP_FORMAT, pf)
112+
if err != nil {
113+
return fmt.Errorf("cannot parse time: %w", err)
114+
}
115+
116+
if ts.Before(limit) {
117+
if err := deletePrefix(ctx, s3client, bucket, prefixWithSuffix+pf); err != nil {
118+
return fmt.Errorf("cannot delete backup: %w", err)
119+
}
120+
}
121+
}
122+
}
123+
124+
return nil
125+
}
126+
127+
func deletePrefix(ctx context.Context, client *s3.Client, bucket, prefix string) error {
128+
paginator := s3.NewListObjectsV2Paginator(client, &s3.ListObjectsV2Input{
129+
Bucket: new(bucket),
130+
Prefix: new(prefix),
131+
})
132+
133+
for paginator.HasMorePages() {
134+
page, err := paginator.NextPage(ctx)
135+
if err != nil {
136+
return err
137+
}
138+
139+
if len(page.Contents) == 0 {
140+
continue
141+
}
142+
143+
objects := make([]s3types.ObjectIdentifier, 0, len(page.Contents))
144+
145+
for _, obj := range page.Contents {
146+
objects = append(objects, s3types.ObjectIdentifier{
147+
Key: obj.Key,
148+
})
149+
}
150+
151+
_, err = client.DeleteObjects(ctx, &s3.DeleteObjectsInput{
152+
Bucket: new(bucket),
153+
Delete: &s3types.Delete{
154+
Objects: objects,
155+
Quiet: new(true),
156+
},
157+
})
158+
if err != nil {
159+
return err
160+
}
161+
}
162+
163+
log.Println("Purged old backup:", prefix)
164+
return nil
165+
}

0 commit comments

Comments
 (0)