Harden backup and restore operations

This commit is contained in:
Mikei386
2026-07-13 19:47:43 +02:00
parent 6cbf170d65
commit bbf063c157
26 changed files with 914 additions and 184 deletions
+73 -31
View File
@@ -30,6 +30,18 @@ type Runner struct {
processes map[string]*os.Process
}
func (r *Runner) SetBinary(path string) {
r.mu.Lock()
r.Binary = path
r.mu.Unlock()
}
func (r *Runner) binary() string {
r.mu.Lock()
defer r.mu.Unlock()
return r.Binary
}
type runIDKey struct{}
func WithRunID(ctx context.Context, runID string) context.Context {
@@ -270,28 +282,32 @@ func decodeStats(output []byte) (Stats, error) {
}
func (r *Runner) List(ctx context.Context, repo model.Repository, snapshot, path string) ([]map[string]any, error) {
items := []map[string]any{}
err := r.WalkList(ctx, repo, snapshot, path, func(item map[string]any) { items = append(items, item) })
return items, err
}
func (r *Runner) WalkList(ctx context.Context, repo model.Repository, snapshot, path string, onItem func(map[string]any)) error {
args := []string{"ls", snapshot, "--json"}
if path != "" {
args = append(args, path)
}
items := []map[string]any{}
if err := r.run(ctx, repo, args, func(line []byte) {
return r.run(ctx, repo, args, func(line []byte) {
var item map[string]any
if json.Unmarshal(line, &item) == nil && item["struct_type"] == "node" {
items = append(items, item)
onItem(item)
}
}, nil); err != nil {
return nil, err
}
return items, nil
}, nil)
}
func (r *Runner) Diff(ctx context.Context, repo model.Repository, before, after string, limit int) (DiffResult, error) {
var output []byte
if err := r.run(ctx, repo, []string{"diff", "--metadata", before, after}, nil, &output); err != nil {
result := DiffResult{}
if err := r.run(ctx, repo, []string{"diff", "--metadata", before, after}, func(line []byte) {
appendDiffLine(&result, string(line), limit)
}, nil); err != nil {
return DiffResult{}, err
}
return parseDiffOutput(output, limit), nil
return result, nil
}
func parseDiffOutput(output []byte, limit int) DiffResult {
@@ -302,29 +318,35 @@ func parseDiffOutput(output []byte, limit int) DiffResult {
scanner := bufio.NewScanner(bytes.NewReader(output))
scanner.Buffer(make([]byte, 0, 64*1024), 2*1024*1024)
for scanner.Scan() {
line := scanner.Text()
if len(line) < 2 {
continue
}
kind := diffKind(line[0])
if kind == "" {
continue
}
path := strings.TrimSpace(line[1:])
if path == "" || !strings.HasPrefix(path, "/") {
continue
}
result.Total++
if len(result.Entries) == limit {
copy(result.Entries, result.Entries[1:])
result.Entries[len(result.Entries)-1] = DiffEntry{Kind: kind, Path: path}
continue
}
result.Entries = append(result.Entries, DiffEntry{Kind: kind, Path: path})
appendDiffLine(&result, scanner.Text(), limit)
}
return result
}
func appendDiffLine(result *DiffResult, line string, limit int) {
if limit <= 0 {
limit = 50
}
if len(line) < 2 {
return
}
kind := diffKind(line[0])
if kind == "" {
return
}
path := strings.TrimSpace(line[1:])
if path == "" || !strings.HasPrefix(path, "/") {
return
}
result.Total++
if len(result.Entries) == limit {
copy(result.Entries, result.Entries[1:])
result.Entries[len(result.Entries)-1] = DiffEntry{Kind: kind, Path: path}
return
}
result.Entries = append(result.Entries, DiffEntry{Kind: kind, Path: path})
}
func diffKind(marker byte) string {
switch marker {
case '+':
@@ -404,7 +426,7 @@ func (r *Runner) runWithInputEnvPassword(ctx context.Context, repo model.Reposit
globalArgs = append(globalArgs, "-o", "sftp.command="+command)
}
fullArgs := append(globalArgs, args...)
cmd := exec.CommandContext(ctx, r.Binary, fullArgs...)
cmd := exec.CommandContext(ctx, r.binary(), fullArgs...)
cmd.Stdin = input
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
cmd.Cancel = func() error {
@@ -451,9 +473,11 @@ func (r *Runner) runWithInputEnvPassword(ctx context.Context, repo model.Reposit
}
close(done)
}()
errBytes, _ := io.ReadAll(io.LimitReader(stderr, 1024*1024))
stderrDone := make(chan []byte, 1)
go func() { stderrDone <- drainLimited(stderr, 1024*1024) }()
err = cmd.Wait()
<-done
errBytes := <-stderrDone
if capture != nil {
*capture = captured
}
@@ -470,6 +494,24 @@ func (r *Runner) runWithInputEnvPassword(ctx context.Context, repo model.Reposit
return nil
}
func drainLimited(reader io.Reader, limit int) []byte {
result := make([]byte, 0, limit)
buffer := make([]byte, 32*1024)
for {
count, err := reader.Read(buffer)
if count > 0 && len(result) < limit {
keep := count
if keep > limit-len(result) {
keep = limit - len(result)
}
result = append(result, buffer[:keep]...)
}
if err != nil {
return result
}
}
}
func (r *Runner) writePasswordFile(pattern, password string) (string, func(), error) {
secretDir := filepath.Join(r.RuntimeDir, "secrets")
if err := os.MkdirAll(secretDir, 0700); err != nil {