store.go (2997B)
1 // store.go — crypto-store tarball backup and restore (persists Megolm sessions 2 // and sync tokens across CronJob runs via S3). 3 package main 4 5 import ( 6 "archive/tar" 7 "bytes" 8 "compress/gzip" 9 "context" 10 "errors" 11 "fmt" 12 "io" 13 "log/slog" 14 "os" 15 ) 16 17 // downloadStore restores a previously uploaded crypto store tarball from S3. 18 // A missing store (first run) is not an error — the caller starts fresh. 19 func downloadStore(ctx context.Context, storeDir, s3Key string) error { 20 data, err := s3Get(ctx, s3Key) 21 if err != nil { 22 return err 23 } 24 if data == nil { 25 slog.Info("No existing store in S3, starting fresh", "key", s3Key) 26 return nil 27 } 28 if err := os.MkdirAll(storeDir, 0700); err != nil { 29 return err 30 } 31 gr, err := gzip.NewReader(bytes.NewReader(data)) 32 if err != nil { 33 return err 34 } 35 defer gr.Close() 36 tr := tar.NewReader(gr) 37 for { 38 hdr, err := tr.Next() 39 if errors.Is(err, io.EOF) { 40 break 41 } 42 if err != nil { 43 return err 44 } 45 target := storeDir + "/" + hdr.Name 46 switch hdr.Typeflag { 47 case tar.TypeDir: 48 os.MkdirAll(target, 0700) //nolint:errcheck 49 case tar.TypeReg: 50 f, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600) 51 if err != nil { 52 return err 53 } 54 if _, err := io.Copy(f, tr); err != nil { 55 f.Close() 56 return err 57 } 58 f.Close() 59 } 60 } 61 slog.Info("Store restored from S3", "key", s3Key, "bytes", len(data)) 62 return nil 63 } 64 65 // uploadStore tarballs the crypto store directory and uploads it to S3 for 66 // the next run to restore. 67 func uploadStore(ctx context.Context, storeDir, s3Key string) error { 68 var buf bytes.Buffer 69 gw := gzip.NewWriter(&buf) 70 tw := tar.NewWriter(gw) 71 if err := addDirToTar(tw, storeDir, "."); err != nil { 72 return err 73 } 74 if err := tw.Close(); err != nil { 75 return fmt.Errorf("finalize tar: %w", err) 76 } 77 if err := gw.Close(); err != nil { 78 return fmt.Errorf("finalize gzip: %w", err) 79 } 80 data := buf.Bytes() 81 if err := s3Put(ctx, s3Key, data, "application/gzip"); err != nil { 82 return err 83 } 84 slog.Info("Store saved to S3", "key", s3Key, "bytes", len(data)) 85 return nil 86 } 87 88 func addDirToTar(tw *tar.Writer, baseDir, arcBase string) error { 89 entries, err := os.ReadDir(baseDir) 90 if err != nil { 91 if os.IsNotExist(err) { 92 return nil 93 } 94 return err 95 } 96 for _, e := range entries { 97 srcPath := baseDir + "/" + e.Name() 98 arcPath := arcBase + "/" + e.Name() 99 if e.IsDir() { 100 _ = tw.WriteHeader(&tar.Header{ 101 Typeflag: tar.TypeDir, 102 Name: arcPath + "/", 103 Mode: 0700, 104 }) 105 if err := addDirToTar(tw, srcPath, arcPath); err != nil { 106 return err 107 } 108 continue 109 } 110 info, err := e.Info() 111 if err != nil { 112 continue 113 } 114 f, err := os.Open(srcPath) 115 if err != nil { 116 continue 117 } 118 if err := tw.WriteHeader(&tar.Header{ 119 Typeflag: tar.TypeReg, 120 Name: arcPath, 121 Size: info.Size(), 122 Mode: 0600, 123 }); err != nil { 124 f.Close() 125 return err 126 } 127 _, copyErr := io.Copy(tw, f) 128 f.Close() 129 if copyErr != nil { 130 return copyErr 131 } 132 } 133 return nil 134 }