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, "'", `'\''`) + "'" }