Harden backup and restore operations
This commit is contained in:
+73
-31
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user