Files
beacon-server/cmd/beacon-backup/main.go
T

93 lines
3.1 KiB
Go

// Copyright 2026 Beacon Contributors
// SPDX-License-Identifier: AGPL-3.0-or-later
// beacon-backup creates or verifies a private database and saved-config export.
package main
import (
"context"
"errors"
"flag"
"fmt"
"io"
"os"
"os/signal"
"syscall"
"github.com/MeshCore-Beacon/beacon-server/internal/backup"
)
var version = "dev"
func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
code := run(ctx, os.Args[1:], os.Stdout, os.Stderr)
stop()
os.Exit(code)
}
func run(ctx context.Context, args []string, stdout, stderr io.Writer) int {
var opts backup.Options
var verifyPath string
flags := flag.NewFlagSet("beacon-backup", flag.ContinueOnError)
flags.SetOutput(stderr)
flags.StringVar(&opts.ConfigPath, "config", "config.yaml", "saved YAML file to export verbatim (may contain secrets)")
flags.StringVar(&opts.OutputPath, "output", "", "new private .tar.gz export destination (never overwritten)")
flags.StringVar(&verifyPath, "verify", "", "verify an existing .tar.gz without extraction or database access")
flags.Int64Var(&opts.MaxBytes, "max-bytes", backup.DefaultMaxBytes, "maximum uncompressed database dump size")
flags.DurationVar(&opts.Timeout, "timeout", backup.DefaultTimeout, "maximum export or verification duration")
if err := flags.Parse(args); err != nil {
if errors.Is(err, flag.ErrHelp) {
return 0
}
return 2
}
if flags.NArg() != 0 || opts.Timeout <= 0 {
fmt.Fprintln(stderr, "positive timeout and no positional arguments required")
return 2
}
verifyMode, exportFlag := false, false
flags.Visit(func(f *flag.Flag) {
verifyMode = verifyMode || f.Name == "verify"
exportFlag = exportFlag || f.Name == "config" || f.Name == "output"
})
if verifyMode {
if verifyPath == "" || exportFlag {
fmt.Fprintln(stderr, "verify requires an archive path and cannot be combined with config or output")
return 2
}
// Reject directories/devices/FIFOs before opening, and check the opened
// descriptor too. A symlink to a regular, operator-selected file is fine.
info, err := os.Stat(verifyPath)
if err != nil || !info.Mode().IsRegular() {
fmt.Fprintln(stderr, "verification requires a readable regular archive file")
return 1
}
file, err := os.Open(verifyPath)
if err != nil {
fmt.Fprintln(stderr, "cannot open backup archive")
return 1
}
defer file.Close()
if info, err = file.Stat(); err != nil || !info.Mode().IsRegular() {
fmt.Fprintln(stderr, "verification requires a regular archive file")
return 1
}
ctx, cancel := context.WithTimeout(ctx, opts.Timeout)
defer cancel()
if _, err := backup.Verify(ctx, file, opts.MaxBytes); err != nil {
fmt.Fprintln(stderr, "verification failed:", err)
return 1
}
fmt.Fprintln(stdout, "Archive structure and checksums verified. No files extracted or SQL executed.")
return 0
}
opts.Version = version
if err := backup.Export(ctx, opts); err != nil {
fmt.Fprintln(stderr, "backup failed:", err)
return 1
}
fmt.Fprintln(stdout, "Backup complete. Store this bundle privately; it may contain keys and message data.")
return 0
}