aboutsummaryrefslogtreecommitdiff
path: root/internal/plan
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-09-14 10:56:21 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-09-14 10:56:21 +0200
commit1506c7dcd6032c04c1df6f5785b6dd3dfe4511cc (patch)
treefb6d573ca8726c1b1d91ba273a284ba666cde3f6 /internal/plan
parentebdd7bb254a0f19f815a644d26ce232de7be0adb (diff)
downloadkrino-1506c7dcd6032c04c1df6f5785b6dd3dfe4511cc.tar.gz
krino-1506c7dcd6032c04c1df6f5785b6dd3dfe4511cc.zip
krino: duplicates are found, never deleted
A rule combining (duplicate) with a delete action is refused at load, and a file that is a duplicate under any duplicate scope its directory uses gets no delete step from any rule: the plan shows it skipped and the chain continues. A failed duplicate check blocks the delete too. Tests cover (matched), another rule's own condition, a test evaluation skipped, two scopes and a failed check, each checking every copy is still on disk.
Diffstat (limited to 'internal/plan')
-rw-r--r--internal/plan/chain.go8
-rw-r--r--internal/plan/chain_test.go28
2 files changed, 36 insertions, 0 deletions
diff --git a/internal/plan/chain.go b/internal/plan/chain.go
index bfa2484..e504504 100644
--- a/internal/plan/chain.go
+++ b/internal/plan/chain.go
@@ -32,6 +32,10 @@ func NewClaims() *Claims {
type Input struct {
File scan.File
Rules []RuleMatch
+ // NoDelete, when non-empty, skips every delete step of this file with
+ // this text as the step's Skip, without ending the chain (spec §5.5
+ // rule 2, §7.1).
+ NoDelete string
}
// Build turns each file's matching rules into a chain. root is the
@@ -166,6 +170,10 @@ func buildOne(root string, in Input, now time.Time, d Disk, claim claimed) Chain
}
case config.Delete, config.DeletePermanent:
+ if in.NoDelete != "" {
+ step.Skip = in.NoDelete
+ break
+ }
deletedBy = rule.Name
}
diff --git a/internal/plan/chain_test.go b/internal/plan/chain_test.go
index b6e3dfe..28c208f 100644
--- a/internal/plan/chain_test.go
+++ b/internal/plan/chain_test.go
@@ -141,3 +141,31 @@ func TestBuildKeepsSteplessChains(t *testing.T) {
}
}
}
+
+// TestBuildNoDeleteSkipsDeletesAndContinues is spec §5.5 rule 2 and §7.1:
+// with Input.NoDelete set, every delete step is skipped with that reason,
+// and the chain carries on from the file's current path instead of ending.
+func TestBuildNoDeleteSkipsDeletesAndContinues(t *testing.T) {
+ in := []Input{{
+ File: file("/r", "b.pdf"),
+ NoDelete: "a duplicate is never deleted",
+ Rules: []RuleMatch{
+ {Name: "old", Actions: []config.Action{act(config.DeletePermanent, "")}},
+ {Name: "dupes", Actions: []config.Action{act(config.Move, "Dupes")}},
+ {Name: "cleanup", Actions: []config.Action{act(config.Delete, "")}},
+ },
+ }}
+ steps := Build("/r", in, time.Now(), NoDisk{}, NewClaims())[0].Steps
+ if len(steps) != 3 {
+ t.Fatalf("steps = %+v", steps)
+ }
+ if steps[0].Kind != DeletePermanent || steps[0].Skip != "a duplicate is never deleted" {
+ t.Errorf("step 0 = %+v, want a skipped permanent delete", steps[0])
+ }
+ if steps[1].Kind != Move || steps[1].Skip != "" || steps[1].Dst != "/r/Dupes/b.pdf" {
+ t.Errorf("step 1 = %+v, want the move to run", steps[1])
+ }
+ if steps[2].Kind != Trash || steps[2].Skip != "a duplicate is never deleted" || steps[2].Src != "/r/Dupes/b.pdf" {
+ t.Errorf("step 2 = %+v, want a skipped trash from the moved path", steps[2])
+ }
+}