diff options
Diffstat (limited to 'internal/apply/apply.go')
| -rw-r--r-- | internal/apply/apply.go | 36 |
1 files changed, 29 insertions, 7 deletions
diff --git a/internal/apply/apply.go b/internal/apply/apply.go index c0055a2..b36035b 100644 --- a/internal/apply/apply.go +++ b/internal/apply/apply.go @@ -45,7 +45,7 @@ type StepResult struct { // marking the rest skipped. It never touches a file whose size or mtime no // longer matches what the plan recorded. It is ChainLogged with no done. func Chain(c plan.Chain) []StepResult { - results, _ := ChainLogged(context.Background(), c, nil) + results, _ := ChainLogged(context.Background(), c, nil, nil) return results } @@ -63,7 +63,7 @@ func Chain(c plan.Chain) []StepResult { // Once ctx is cancelled (an interrupt), the step already under way finishes // and every later step is skipped as "interrupted": an interrupt stops after // the current step, not after the file's whole chain (spec ยง11). -func ChainLogged(ctx context.Context, c plan.Chain, done func(i int, sr StepResult) error) ([]StepResult, error) { +func ChainLogged(ctx context.Context, c plan.Chain, done func(i int, sr StepResult) error, displaced func(i int, step plan.Step, entry string) error) ([]StepResult, error) { results := make([]StepResult, len(c.Steps)) stopWhy := "" @@ -86,7 +86,12 @@ func ChainLogged(ctx context.Context, c plan.Chain, done func(i int, sr StepResu stopWhy = "an earlier step in this chain failed" break } - res := runStep(step) + res := runStep(step, func(entry string) error { + if displaced == nil { + return nil + } + return displaced(i, step, entry) + }) results[i] = res switch { case res.Status == "failed": @@ -133,11 +138,13 @@ func checkUnchanged(src string, f scan.File) error { } // runStep dispatches one already-checked, non-skipped step to the code that -// actually carries it out. -func runStep(step plan.Step) StepResult { +// actually carries it out. reportDisplace is called the moment a displaced +// file has reached the Trash, before the step that needed its name begins; +// a step whose displace cannot be reported does not go on to use the name. +func runStep(step plan.Step, reportDisplace func(entry string) error) StepResult { switch step.Kind { case plan.Copy, plan.Move, plan.Rename: - return runFileStep(step) + return runFileStep(step, reportDisplace) case plan.Trash: return runTrashStep(step) case plan.DeletePermanent: @@ -155,7 +162,13 @@ func runStep(step plan.Step) StepResult { // overwriting a file the plan never accounted for. Missing destination // directories are created and recorded in Made, outermost first, whether or // not the step that needed them goes on to succeed. -func runFileStep(step plan.Step) StepResult { +// +// The displace is reported through reportDisplace as soon as trash.Put +// returns, not when this step finishes: the user's file is in the Trash +// from that moment, durably, and for a copy or a cross-device move the rest +// of the step is the whole data transfer. A process killed in that window +// used to leave the file in the Trash with nothing in the log to say so. +func runFileStep(step plan.Step, reportDisplace func(entry string) error) StepResult { dst := step.Dst var displacedEntry string @@ -178,6 +191,15 @@ func runFileStep(step plan.Step) StepResult { return StepResult{Step: step, Status: "failed", Detail: "displacing the existing file: " + err.Error()} } displacedEntry = entry + if reportDisplace != nil { + if err := reportDisplace(entry); err != nil { + // The file is already in the Trash and cannot be recorded. + // Using the name now would compound an unlogged destructive + // act with a second one. + return StepResult{Step: step, Status: "failed", DisplacedEntry: entry, + Detail: "the file it replaces went to the Trash but could not be logged, so undo cannot see it: " + err.Error()} + } + } } else if _, err := os.Lstat(dst); err == nil { free, err := nextFreeName(dst) if err != nil { |
