Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
85 lines
2.5 KiB
Go
85 lines
2.5 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/onboard"
|
|
)
|
|
|
|
// copyBackup puts an archive where server.yaml's backup_copy_to says, and
|
|
// keeps as many there as here. It says each copy, and fails on the first
|
|
// that cannot be made.
|
|
func copyBackup(ctx context.Context, archive string, targets []string, key, knownHosts string, keep int, say func(string)) error {
|
|
for _, t := range targets {
|
|
if strings.HasPrefix(t, "ssh://") {
|
|
if err := copyOverSSH(ctx, archive, t, key, knownHosts, keep); err != nil {
|
|
return fmt.Errorf("%s: %w", t, err)
|
|
}
|
|
} else {
|
|
if err := copyToDir(archive, t, keep); err != nil {
|
|
return fmt.Errorf("%s: %w", t, err)
|
|
}
|
|
}
|
|
say("copied to " + t)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func copyToDir(archive, dir string, keep int) error {
|
|
if err := os.MkdirAll(dir, 0o700); err != nil {
|
|
return err
|
|
}
|
|
b, err := os.ReadFile(archive) //nolint:gosec // the archive just written
|
|
if err != nil {
|
|
return err
|
|
}
|
|
final := filepath.Join(dir, filepath.Base(archive))
|
|
if err := os.WriteFile(final+".part", b, 0o600); err != nil {
|
|
return err
|
|
}
|
|
if err := os.Rename(final+".part", final); err != nil {
|
|
return err
|
|
}
|
|
_, err = pruneBackups(dir, keep)
|
|
return err
|
|
}
|
|
|
|
func copyOverSSH(ctx context.Context, archive, target, key, knownHosts string, keep int) error {
|
|
u, err := url.Parse(target)
|
|
if err != nil || u.User == nil || u.Host == "" || u.Path == "" {
|
|
return fmt.Errorf("not ssh://user@host/path")
|
|
}
|
|
port := 22
|
|
if p := u.Port(); p != "" {
|
|
port, _ = strconv.Atoi(p)
|
|
}
|
|
b, err := os.ReadFile(archive) //nolint:gosec // the archive just written
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c := &onboard.GoSSH{Addr: u.Hostname(), Port: port, User: u.User.Username(), KeyFile: key, KnownHosts: knownHosts}
|
|
defer c.Close()
|
|
dir, name := shellQuote(u.Path), shellQuote(filepath.Base(archive))
|
|
// Written beside its name and renamed, then the oldest beyond keep
|
|
// removed: only archives this command wrote, by their names.
|
|
script := fmt.Sprintf(`set -e
|
|
umask 077
|
|
mkdir -p %[1]s
|
|
cat > %[1]s/%[2]s.part
|
|
mv %[1]s/%[2]s.part %[1]s/%[2]s
|
|
ls -1t %[1]s | grep -E '^llmgw-backup-.*\.tar\.gz(\.age)?$' | tail -n +%[3]d | while read -r f; do rm -f %[1]s/"$f"; done
|
|
`, dir, name, keep+1)
|
|
if out, err := c.Run(ctx, script, b); err != nil {
|
|
return fmt.Errorf("%w: %s", err, strings.TrimSpace(string(out)))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func shellQuote(s string) string { return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'" }
|