mirror of
https://github.com/luxfi/mpc.git
synced 2026-09-24 00:26:24 +00:00
Canonical S3 SDK across the stack is the Hanzo S3 fork at github.com/hanzos3/go-sdk (package `s3`). Imports use an explicit `s3 "github.com/hanzos3/go-sdk"` alias to avoid the `go-sdk` last-path- segment mismatch with `package s3`. `pkg/backup/s3.go` swept: `minio.Client` → `s3.Client`, `minio.New` → `s3.New`, `minio.Options` → `s3.Options`, `minio.MakeBucketOptions` → `s3.MakeBucketOptions`, `minio.PutObjectOptions` → `s3.PutObjectOptions`. Field name `s3Client *s3.Client` is fine (no shadow with the package qualifier). Dropped stale `luxfi/zapdb v1.0.0` entries in go.work.sum that were blocking go mod tidy (their hashes no longer match the proxy after zapdb's own sweep). No behavioral changes.
212 lines
4.8 KiB
Go
212 lines
4.8 KiB
Go
package backup
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
s3 "github.com/hanzos3/go-sdk"
|
|
"github.com/hanzos3/go-sdk/pkg/credentials"
|
|
|
|
"github.com/luxfi/mpc/pkg/kvstore"
|
|
"github.com/luxfi/mpc/pkg/logger"
|
|
)
|
|
|
|
// S3Config configures S3-compatible backup storage
|
|
type S3Config struct {
|
|
Endpoint string // e.g., "s3.amazonaws.com" or "minio.example.com"
|
|
Bucket string // e.g., "lux-mpc-backups"
|
|
AccessKey string
|
|
SecretKey string
|
|
Region string // e.g., "us-east-1"
|
|
UseSSL bool
|
|
Prefix string // e.g., "mpc/node0/"
|
|
}
|
|
|
|
// S3ConfigFromEnv creates S3Config from environment variables
|
|
func S3ConfigFromEnv(nodeID string) *S3Config {
|
|
endpoint := os.Getenv("S3_ENDPOINT")
|
|
if endpoint == "" {
|
|
return nil // S3 not configured
|
|
}
|
|
|
|
bucket := os.Getenv("S3_BUCKET")
|
|
if bucket == "" {
|
|
bucket = "lux-mpc-backups"
|
|
}
|
|
|
|
prefix := os.Getenv("S3_PREFIX")
|
|
if prefix == "" {
|
|
prefix = fmt.Sprintf("mpc/%s/", nodeID)
|
|
}
|
|
if !strings.HasSuffix(prefix, "/") {
|
|
prefix += "/"
|
|
}
|
|
|
|
useSSL := os.Getenv("S3_USE_SSL") != "false"
|
|
|
|
return &S3Config{
|
|
Endpoint: endpoint,
|
|
Bucket: bucket,
|
|
AccessKey: os.Getenv("S3_ACCESS_KEY"),
|
|
SecretKey: os.Getenv("S3_SECRET_KEY"),
|
|
Region: os.Getenv("S3_REGION"),
|
|
UseSSL: useSSL,
|
|
Prefix: prefix,
|
|
}
|
|
}
|
|
|
|
// Manager handles periodic backups with optional S3 upload
|
|
type Manager struct {
|
|
executor *kvstore.Backup
|
|
backupDir string
|
|
s3Config *S3Config
|
|
s3Client *s3.Client
|
|
nodeID string
|
|
period time.Duration
|
|
done chan struct{}
|
|
}
|
|
|
|
// NewManager creates a backup manager
|
|
func NewManager(executor *kvstore.Backup, backupDir, nodeID string, period time.Duration, s3Cfg *S3Config) (*Manager, error) {
|
|
m := &Manager{
|
|
executor: executor,
|
|
backupDir: backupDir,
|
|
s3Config: s3Cfg,
|
|
nodeID: nodeID,
|
|
period: period,
|
|
done: make(chan struct{}),
|
|
}
|
|
|
|
if s3Cfg != nil && s3Cfg.Endpoint != "" {
|
|
client, err := s3.New(s3Cfg.Endpoint, &s3.Options{
|
|
Creds: credentials.NewStaticV4(s3Cfg.AccessKey, s3Cfg.SecretKey, ""),
|
|
Secure: s3Cfg.UseSSL,
|
|
Region: s3Cfg.Region,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create S3 client: %w", err)
|
|
}
|
|
m.s3Client = client
|
|
|
|
// Ensure bucket exists
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
exists, err := client.BucketExists(ctx, s3Cfg.Bucket)
|
|
if err != nil {
|
|
logger.Warn("Failed to check S3 bucket", "bucket", s3Cfg.Bucket, "err", err)
|
|
} else if !exists {
|
|
if err := client.MakeBucket(ctx, s3Cfg.Bucket, s3.MakeBucketOptions{Region: s3Cfg.Region}); err != nil {
|
|
logger.Warn("Failed to create S3 bucket", "bucket", s3Cfg.Bucket, "err", err)
|
|
} else {
|
|
logger.Info("Created S3 bucket", "bucket", s3Cfg.Bucket)
|
|
}
|
|
}
|
|
|
|
logger.Info("S3 backup enabled",
|
|
"endpoint", s3Cfg.Endpoint,
|
|
"bucket", s3Cfg.Bucket,
|
|
"prefix", s3Cfg.Prefix,
|
|
)
|
|
}
|
|
|
|
return m, nil
|
|
}
|
|
|
|
// Start begins the periodic backup loop
|
|
func (m *Manager) Start() {
|
|
go m.loop()
|
|
}
|
|
|
|
// Stop stops the backup manager
|
|
func (m *Manager) Stop() {
|
|
close(m.done)
|
|
}
|
|
|
|
func (m *Manager) loop() {
|
|
ticker := time.NewTicker(m.period)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-m.done:
|
|
return
|
|
case <-ticker.C:
|
|
if err := m.RunBackup(); err != nil {
|
|
logger.Error("Backup failed", err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// RunBackup executes a backup and uploads to S3 if configured
|
|
func (m *Manager) RunBackup() error {
|
|
// Get list of files before backup
|
|
beforeFiles := m.executor.SortedEncryptedBackups()
|
|
|
|
// Execute local backup
|
|
if err := m.executor.Execute(); err != nil {
|
|
return fmt.Errorf("local backup failed: %w", err)
|
|
}
|
|
|
|
// Find new files
|
|
afterFiles := m.executor.SortedEncryptedBackups()
|
|
newFiles := findNewFiles(beforeFiles, afterFiles)
|
|
|
|
if len(newFiles) == 0 {
|
|
return nil // No new backup (no changes)
|
|
}
|
|
|
|
// Upload new files to S3
|
|
if m.s3Client != nil {
|
|
for _, f := range newFiles {
|
|
if err := m.uploadToS3(f); err != nil {
|
|
logger.Error("S3 upload failed", err, "file", f)
|
|
// Don't return error - local backup succeeded
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *Manager) uploadToS3(localPath string) error {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
|
defer cancel()
|
|
|
|
filename := filepath.Base(localPath)
|
|
objectName := m.s3Config.Prefix + filename
|
|
|
|
info, err := m.s3Client.FPutObject(ctx, m.s3Config.Bucket, objectName, localPath, s3.PutObjectOptions{
|
|
ContentType: "application/octet-stream",
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("S3 upload failed: %w", err)
|
|
}
|
|
|
|
logger.Info("Backup uploaded to S3",
|
|
"file", filename,
|
|
"bucket", m.s3Config.Bucket,
|
|
"object", objectName,
|
|
"size", info.Size,
|
|
)
|
|
return nil
|
|
}
|
|
|
|
func findNewFiles(before, after []string) []string {
|
|
existing := make(map[string]bool, len(before))
|
|
for _, f := range before {
|
|
existing[f] = true
|
|
}
|
|
var newFiles []string
|
|
for _, f := range after {
|
|
if !existing[f] {
|
|
newFiles = append(newFiles, f)
|
|
}
|
|
}
|
|
return newFiles
|
|
}
|