From a80d470cbad7335fd5e534f32d935e5a49aadb24 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Thu, 17 Sep 2026 13:53:01 +0200 Subject: a file trashed to make room is logged the moment it is trashed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The displace was carried out first and logged only when the whole step finished - for a copy or a cross-device move, the entire data transfer later. A process killed in that window left the user's file in the Trash with nothing recording it: krino log said "nothing applied" and undo offered nothing. It is its own action with its own line (spec §9), so it is now written the moment trash.Put returns, through a hook ChainLogged calls before the step that needed the name begins. A displace that cannot be logged fails the step rather than compounding an unrecorded destructive act with a second one. Verified by killing krino -9 mid-copy with 600 MB in flight: before: krino log "nothing applied", 0 displace lines after: krino log "1 displaced", 1 displace line --- internal/engine/apply.go | 40 +++++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 17 deletions(-) (limited to 'internal/engine') diff --git a/internal/engine/apply.go b/internal/engine/apply.go index 1e59aa1..e2d4db8 100644 --- a/internal/engine/apply.go +++ b/internal/engine/apply.go @@ -118,6 +118,11 @@ func (e *Engine) applyFile(ctx context.Context, dirName string, c plan.Chain, ap return unloggedStep(rel, c.Steps[i], sr, err) } return nil + }, func(i int, step plan.Step, entry string) error { + // The displaced file is in the Trash from this moment, whatever + // becomes of the step that needed its name, so its line is written + // here rather than with the rest of the step. + return e.logDisplace(j, run, dirName, rel, i+1, step, entry) }) if err != nil { return FileResult{}, err @@ -125,6 +130,24 @@ func (e *Engine) applyFile(ctx context.Context, dirName string, c plan.Chain, ap return FileResult{File: c.File, Steps: results}, nil } +// logDisplace writes the line for a file trashed to make room, as soon as +// it has reached the Trash. It is its own action with its own line (spec +// §9), independent of whether the step that needed the name then succeeds. +func (e *Engine) logDisplace(j *journal.Writer, run, dirName, rel string, stepNum int, step plan.Step, entry string) error { + dst := filepath.Join(trash.Dir(), "files", entry) + size, mtime := statSizeModTime(dst) + return j.Append(journal.Entry{ + Time: e.Now(), Run: run, Dir: dirName, File: rel, Step: stepNum, + Action: "displace", Status: "ok", Rule: step.Rule, + // Detail carries the trash entry name explicitly: Dst's shape + // (trash.Dir()/files/) is internal/apply's and this file's + // own convention, not a contract undo may quietly depend on. + // PlanUndo/ApplyUndo read the name from here, never by taking + // Dst's basename. + Src: step.Displaces, Dst: dst, Size: size, ModTime: mtime, Detail: entry, + }) +} + // unloggedStep is the error for a step whose log entry could not be // written. A step that ran is named with where its file is now: undo // cannot see it, so the user must be told where to look. @@ -233,23 +256,6 @@ func actionName(k plan.Kind) string { // step's planned destination (informational only — PlanUndo never reverses // a non-"ok" entry) and Size/ModTime stay zero. func (e *Engine) logStep(j *journal.Writer, run, dirName, rel string, stepNum int, step plan.Step, sr apply.StepResult) error { - if sr.DisplacedEntry != "" { - dst := filepath.Join(trash.Dir(), "files", sr.DisplacedEntry) - size, mtime := statSizeModTime(dst) - if err := j.Append(journal.Entry{ - Time: e.Now(), Run: run, Dir: dirName, File: rel, Step: stepNum, - Action: "displace", Status: "ok", Rule: step.Rule, - // Detail carries the trash entry name explicitly: Dst's shape - // (trash.Dir()/files/) is internal/apply's and this - // file's own convention, not a contract undo may quietly - // depend on. PlanUndo/ApplyUndo read the name from here, - // never by taking Dst's basename. - Src: step.Displaces, Dst: dst, Size: size, ModTime: mtime, Detail: sr.DisplacedEntry, - }); err != nil { - return err - } - } - for _, dir := range sr.Made { size, mtime := statSizeModTime(dir) if err := j.Append(journal.Entry{ -- cgit v1.3