A tool for backing up ATProto related data to S3
at main 167 lines 4.1 kB view raw
1package main 2 3import ( 4 "archive/tar" 5 "compress/gzip" 6 "context" 7 "fmt" 8 "io" 9 "log/slog" 10 "os" 11 "path" 12 "path/filepath" 13 "time" 14 15 "github.com/bugsnag/bugsnag-go/v2" 16 "github.com/minio/minio-go/v7" 17) 18 19func (s *service) backupTangledKnot(ctx context.Context) { 20 err := s.backupKnotDB(ctx) 21 if err != nil { 22 bugsnag.Notify(fmt.Errorf("backup knot db: %w", err)) 23 slog.Error("failed to backup knot db", "error", err) 24 } 25 err = s.backupKnotRepos(ctx) 26 if err != nil { 27 bugsnag.Notify(fmt.Errorf("backup knot db: %w", err)) 28 slog.Error("failed to backup knot db", "error", err) 29 } 30 31 slog.Info("finished tangled knot backup") 32} 33 34func (s *service) backupKnotDB(ctx context.Context) error { 35 dir := os.Getenv("TANGLED_KNOT_DATABASE_DIRECTORY") 36 if dir == "" { 37 slog.Info("TANGLED_KNOT_DATABASE_DIRECTORY env not set - skipping knot DB backup") 38 } 39 40 filename := path.Join(s.blobDir, fmt.Sprintf("%d-knot.zip", time.Now().UnixMilli())) 41 f, err := os.Create(filename) 42 if err != nil { 43 return fmt.Errorf("creating temp file: %w", err) 44 } 45 defer func() { 46 f.Close() 47 48 err = os.Remove(filename) 49 if err != nil { 50 slog.Error("failed to delete knot db zip file after uploading", "error", err, "filename", f.Name()) 51 metadata := bugsnag.MetaData{ 52 "file": { 53 "filename": f.Name(), 54 }, 55 } 56 bugsnag.Notify(fmt.Errorf("delete knot db zip file after uploading: %w", err), metadata) 57 } 58 }() 59 60 compress(dir, f) 61 62 // reset the reader back to the start so that the minio upload can read the data that's been written. 63 _, err = f.Seek(0, 0) 64 if err != nil { 65 return fmt.Errorf("setting seek on written file: %w", err) 66 } 67 68 fi, err := f.Stat() 69 if err != nil { 70 return fmt.Errorf("stat written file: %w", err) 71 } 72 73 _, err = s.minioClient.PutObject(ctx, s.bucketName, "knot-db.zip", f, fi.Size(), minio.PutObjectOptions{}) 74 if err != nil { 75 return fmt.Errorf("put knot db zip file to bucket: %w", err) 76 } 77 return nil 78} 79 80func (s *service) backupKnotRepos(ctx context.Context) error { 81 dir := os.Getenv("TANGLED_KNOT_REPOSITORY_DIRECTORY") 82 if dir == "" { 83 slog.Info("TANGLED_KNOT_REPOSITORY_DIRECTORY env not set - skipping knot repo backup") 84 } 85 86 filename := path.Join(s.blobDir, fmt.Sprintf("%d-knot-repos.zip", time.Now().UnixMilli())) 87 f, err := os.Create(filename) 88 if err != nil { 89 return fmt.Errorf("creating temp file: %w", err) 90 } 91 defer func() { 92 f.Close() 93 94 err = os.Remove(filename) 95 if err != nil { 96 slog.Error("failed to delete knot repos zip file after uploading", "error", err, "filename", f.Name()) 97 metadata := bugsnag.MetaData{ 98 "file": { 99 "filename": f.Name(), 100 }, 101 } 102 bugsnag.Notify(fmt.Errorf("delete knot repos zip file after uploading: %w", err), metadata) 103 } 104 }() 105 106 compress(dir, f) 107 108 // reset the reader back to the start so that the minio upload can read the data that's been written. 109 _, err = f.Seek(0, 0) 110 if err != nil { 111 return fmt.Errorf("setting seek on written file: %w", err) 112 } 113 114 fi, err := f.Stat() 115 if err != nil { 116 return fmt.Errorf("stat written file: %w", err) 117 } 118 119 _, err = s.minioClient.PutObject(ctx, s.bucketName, "knot-repos.zip", f, fi.Size(), minio.PutObjectOptions{}) 120 if err != nil { 121 return fmt.Errorf("put knot repo file to bucket: %w", err) 122 } 123 return nil 124} 125 126func compress(src string, writer io.Writer) error { 127 zipWriter := gzip.NewWriter(writer) 128 tarWriter := tar.NewWriter(zipWriter) 129 130 filepath.Walk(src, func(file string, fi os.FileInfo, err error) error { 131 header, err := tar.FileInfoHeader(fi, file) 132 if err != nil { 133 return err 134 } 135 136 // must provide real name 137 // (see https://golang.org/src/archive/tar/common.go?#L626) 138 header.Name = filepath.ToSlash(file) 139 140 if err := tarWriter.WriteHeader(header); err != nil { 141 return err 142 } 143 // if not a dir, write file content 144 if !fi.IsDir() { 145 data, err := os.Open(file) 146 if err != nil { 147 return err 148 } 149 if _, err := io.Copy(tarWriter, data); err != nil { 150 return err 151 } 152 } 153 154 return nil 155 }) 156 157 // produce tar 158 if err := tarWriter.Close(); err != nil { 159 return err 160 } 161 // produce gzip 162 if err := zipWriter.Close(); err != nil { 163 return err 164 } 165 166 return nil 167}