mirror of
https://github.com/MeshCore-Beacon/beacon-server.git
synced 2026-10-16 20:20:07 +00:00
93 lines
3.1 KiB
Go
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
|
|
}
|