cluster

Infrastructure files for Nordgedanken and Midnightthoughts.
git clone git://archive.git.mtrnord.blog/MTRNord/cluster.git
Log | Files | Refs | README

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 }