Рыба проекта. Минимальная функциональность
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/config"
|
||||
"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/crypto/composite"
|
||||
"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/crypto/keymanager"
|
||||
"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/crypto/mlkem768"
|
||||
"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/crypto/x25519"
|
||||
pgdumpadapter "git.tswf.io/infra/go-synapse-backupper/pkg/adapters/pgdump"
|
||||
"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/pipeline"
|
||||
"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/retention"
|
||||
"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/storage/local"
|
||||
"git.tswf.io/infra/go-synapse-backupper/pkg/domain"
|
||||
"git.tswf.io/infra/go-synapse-backupper/pkg/domain/crypto"
|
||||
"git.tswf.io/infra/go-synapse-backupper/pkg/domain/pgdump"
|
||||
)
|
||||
|
||||
var (
|
||||
newKeyManager = keymanager.NewKeyManager
|
||||
newLocalSink = local.NewLocalSink
|
||||
newRunner = defaultNewRunner
|
||||
outputWriter io.Writer = os.Stderr
|
||||
)
|
||||
|
||||
type pipelineRunner interface {
|
||||
Run(
|
||||
ctx context.Context,
|
||||
pgDumpOpts pgdump.Options,
|
||||
recipients []crypto.RecipientPub,
|
||||
sink domain.Sink,
|
||||
rand io.Reader,
|
||||
) error
|
||||
}
|
||||
|
||||
func defaultNewRunner(options ...pipeline.Option) pipelineRunner {
|
||||
return pipeline.NewRunner(options...)
|
||||
}
|
||||
|
||||
func init() {
|
||||
config.RegisterFlags(backupCmd)
|
||||
}
|
||||
|
||||
var backupCmd = &cobra.Command{
|
||||
Use: "backup",
|
||||
Short: "Run a one-off backup",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
startTime := time.Now().UTC()
|
||||
runID, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate run-id: %w", err)
|
||||
}
|
||||
|
||||
logger := slog.New(slog.NewTextHandler(outputWriter, nil))
|
||||
logger.Info(
|
||||
"backup started",
|
||||
slog.String("run_id", runID.String()),
|
||||
slog.Time("start_time", startTime),
|
||||
)
|
||||
|
||||
cfg, err := config.Load(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load config: %w", err)
|
||||
}
|
||||
|
||||
keyManager := newKeyManager(crypto.NewRegistry())
|
||||
pqPub, err := keyManager.LoadPub(cfg.PQPublicKeyPath, cfg.PQScheme)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load PQ public key: %w", err)
|
||||
}
|
||||
|
||||
classicalPub, err := keyManager.LoadPub(cfg.ClassicalPublicKeyPath, cfg.ClassicalScheme)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load classical public key: %w", err)
|
||||
}
|
||||
|
||||
recipients := []crypto.RecipientPub{pqPub, classicalPub}
|
||||
|
||||
pgDumpOpts := pgdump.Options{
|
||||
Host: cfg.PG.Host,
|
||||
Port: cfg.PG.Port,
|
||||
Database: cfg.PG.Database,
|
||||
User: cfg.PG.User,
|
||||
Password: cfg.PG.Password,
|
||||
Key: fmt.Sprintf("synapse-%s.dump.pqenc", startTime.Format("20060102-150405")),
|
||||
ExcludeTables: cfg.PG.ExcludeTables,
|
||||
}
|
||||
|
||||
finalPath := filepath.Join(cfg.Backup.Dir, pgDumpOpts.Key)
|
||||
logger.Info(
|
||||
"backup destination",
|
||||
slog.String("backup_dir", cfg.Backup.Dir),
|
||||
slog.String("output_path", finalPath),
|
||||
)
|
||||
|
||||
sink := newLocalSink(cfg.Backup.Dir)
|
||||
|
||||
registry := crypto.NewRegistry()
|
||||
_ = registry.Register(0x0006, func() crypto.KEM { return mlkem768.New() })
|
||||
_ = registry.Register(0x0007, func() crypto.KEM { return x25519.New() })
|
||||
runner := newRunner(
|
||||
pipeline.WithDumper(pgdumpadapter.New()),
|
||||
pipeline.WithEncryptor(composite.NewEncryptor(registry)),
|
||||
)
|
||||
|
||||
err = runner.Run(ctx, pgDumpOpts, recipients, sink, rand.Reader)
|
||||
endTime := time.Now().UTC()
|
||||
|
||||
var byteCount int64
|
||||
if info, statErr := os.Stat(finalPath); statErr == nil {
|
||||
byteCount = info.Size()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.Error(
|
||||
"backup failed",
|
||||
slog.String("run_id", runID.String()),
|
||||
slog.String("output_path", finalPath),
|
||||
slog.Time("start_time", startTime),
|
||||
slog.Time("end_time", endTime),
|
||||
slog.Int64("byte_count", byteCount),
|
||||
slog.String("error", err.Error()),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Info(
|
||||
"backup completed",
|
||||
slog.String("run_id", runID.String()),
|
||||
slog.String("output_path", finalPath),
|
||||
slog.Time("start_time", startTime),
|
||||
slog.Time("end_time", endTime),
|
||||
slog.Int64("byte_count", byteCount),
|
||||
)
|
||||
|
||||
if _, pruneErr := retention.PruneByAge(
|
||||
ctx,
|
||||
cfg.Backup.Dir,
|
||||
cfg.Backup.RetentionDays,
|
||||
time.Now(),
|
||||
); pruneErr != nil {
|
||||
logger.Error(
|
||||
"retention pruning failed",
|
||||
slog.String("error", pruneErr.Error()),
|
||||
)
|
||||
return pruneErr
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user