@@ -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+
1620var 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