aboutsummaryrefslogtreecommitdiff
path: root/internal/apply/apply.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/apply/apply.go')
-rw-r--r--internal/apply/apply.go36
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 {