summaryrefslogtreecommitdiff
path: root/internal/config
diff options
context:
space:
mode:
Diffstat (limited to 'internal/config')
-rw-r--r--internal/config/dir.go38
-rw-r--r--internal/config/dir_test.go36
-rw-r--r--internal/config/main.go7
-rw-r--r--internal/config/main_test.go18
-rw-r--r--internal/config/settings.go15
-rw-r--r--internal/config/settings_test.go16
-rw-r--r--internal/config/skel/krino.conf7
-rw-r--r--internal/config/skel/template.conf6
8 files changed, 136 insertions, 7 deletions
diff --git a/internal/config/dir.go b/internal/config/dir.go
index 74c538a..461d912 100644
--- a/internal/config/dir.go
+++ b/internal/config/dir.go
@@ -19,9 +19,41 @@ type Dir struct {
PathText string // as written in the file
Settings Settings
Ignore []string // gitignore patterns, in order
+ Excludes []*Exclude
Rules []*Rule
}
+// Exclude is one (exclude COND...) form: a file for which every condition
+// holds is set aside before any rule runs. Forms may repeat, so a file is
+// excluded when any one form matches it.
+type Exclude struct {
+ Pos sexp.Pos
+ Text string // the form as written, whitespace collapsed, for check and explain
+ When []*sexp.Node // the conditions, all of which must hold
+}
+
+// parseExclude reads an (exclude COND...) form from src; nil when it has no
+// usable condition.
+func parseExclude(n *sexp.Node, src []byte, d *diags) *Exclude {
+ conds := n.Args()
+ if len(conds) == 0 {
+ d.at(n, "(exclude) needs a condition, like (exclude (type iso))")
+ return nil
+ }
+ bad := false
+ for _, c := range conds {
+ if c.Kind != sexp.List {
+ d.at(c, "exclude: a condition is a form like (type pdf), not %s", c)
+ bad = true
+ }
+ }
+ if bad {
+ return nil
+ }
+ text := strings.Join(strings.Fields(string(src[n.Pos.Offset:n.End.Offset])), " ")
+ return &Exclude{Pos: n.Pos, Text: text, When: conds}
+}
+
// Rule is a named condition with the actions it performs.
type Rule struct {
Name string
@@ -97,6 +129,10 @@ func ParseDir(name, file string, src []byte) (*Dir, []*Diag) {
}
dir.Ignore = append(dir.Ignore, a.Text)
}
+ case head == "exclude":
+ if x := parseExclude(n, src, d); x != nil {
+ dir.Excludes = append(dir.Excludes, x)
+ }
case head == "rule":
r := parseRule(n, d)
if r == nil {
@@ -111,7 +147,7 @@ func ParseDir(name, file string, src []byte) (*Dir, []*Diag) {
case isSetting(head):
dir.Settings.parse(n, d, seen)
default:
- d.at(n, "unknown form (%s ...); a directory file has path, ignore, rule and settings like (recursive yes)", head)
+ d.at(n, "unknown form (%s ...); a directory file has path, ignore, exclude, rule and settings like (recursive yes)", head)
}
}
if pathNode == nil {
diff --git a/internal/config/dir_test.go b/internal/config/dir_test.go
index 7ddfa2a..080905c 100644
--- a/internal/config/dir_test.go
+++ b/internal/config/dir_test.go
@@ -4,6 +4,7 @@ package config
import (
"reflect"
+ "strings"
"testing"
)
@@ -111,7 +112,7 @@ func TestParseDirErrors(t *testing.T) {
{`(path "/a") (rule "x" (delete) (move "y"))`, `d.conf:1:32: rule "x": (move "y") after delete would never run`},
{`(path "/a") (rule "x" (stop now))`, `d.conf:1:23: rule "x": stop takes nothing: write (stop)`},
{`(path "/a") (rule "x" (fly "y"))`, `d.conf:1:23: rule "x": unknown form (fly ...); a rule has when, copy, move, rename, delete, stop, case, fold and on-conflict`},
- {`(path "/a") (sort "x")`, `d.conf:1:13: unknown form (sort ...); a directory file has path, ignore, rule and settings like (recursive yes)`},
+ {`(path "/a") (sort "x")`, `d.conf:1:13: unknown form (sort ...); a directory file has path, ignore, exclude, rule and settings like (recursive yes)`},
}
for _, tt := range tests {
_, errs := ParseDir("d", "d.conf", []byte(tt.src))
@@ -120,3 +121,36 @@ func TestParseDirErrors(t *testing.T) {
}
}
}
+
+// TestParseExclude: (exclude COND...) may repeat; each form keeps its
+// conditions (all must hold, as in when) and its text as written, with the
+// whitespace collapsed, for check and explain to show.
+func TestParseExclude(t *testing.T) {
+ src := `(path "/tmp")
+(exclude (type iso img))
+(exclude (name "^draft")
+ (content "poufne"))
+`
+ dir, errs := ParseDir("dl", "dl.conf", []byte(src))
+ if len(errs) != 0 {
+ t.Fatal(errs)
+ }
+ if len(dir.Excludes) != 2 || len(dir.Excludes[0].When) != 1 || len(dir.Excludes[1].When) != 2 {
+ t.Fatalf("Excludes = %+v", dir.Excludes)
+ }
+ if got, want := dir.Excludes[1].Text, `(exclude (name "^draft") (content "poufne"))`; got != want {
+ t.Errorf("Text = %q, want %q", got, want)
+ }
+ if dir.Excludes[1].Pos.Line != 3 {
+ t.Errorf("Pos = %+v, want line 3", dir.Excludes[1].Pos)
+ }
+ for _, tt := range []struct{ src, want string }{
+ {"(path \"/tmp\")\n(exclude)\n", "(exclude) needs a condition"},
+ {"(path \"/tmp\")\n(exclude iso)\n", "exclude: a condition is a form like (type pdf), not iso"},
+ } {
+ _, errs := ParseDir("dl", "dl.conf", []byte(tt.src))
+ if len(errs) != 1 || !strings.Contains(errs[0].Msg, tt.want) {
+ t.Errorf("%q: errs %v, want one containing %q", tt.src, errs, tt.want)
+ }
+ }
+}
diff --git a/internal/config/main.go b/internal/config/main.go
index ee6c3df..2764c8a 100644
--- a/internal/config/main.go
+++ b/internal/config/main.go
@@ -21,6 +21,7 @@ type Main struct {
IncludeNode *sexp.Node
Log string // absolute; empty means the default
Defaults Settings
+ Excludes []*Exclude // apply to every directory, before its own
}
// nameRE is what a directory name may look like: it becomes a file name.
@@ -87,8 +88,12 @@ func ParseMain(file string, src []byte) (*Main, []*Diag) {
}
m.Defaults.parse(a, d, dseen)
}
+ case "exclude":
+ if x := parseExclude(n, src, d); x != nil {
+ m.Excludes = append(m.Excludes, x)
+ }
default:
- d.at(n, "unknown form (%s ...); krino.conf has include, log and defaults", head)
+ d.at(n, "unknown form (%s ...); krino.conf has include, log, defaults and exclude", head)
}
}
return m, d.list
diff --git a/internal/config/main_test.go b/internal/config/main_test.go
index a82dfcd..be39faf 100644
--- a/internal/config/main_test.go
+++ b/internal/config/main_test.go
@@ -4,6 +4,7 @@ package config
import (
"reflect"
+ "strings"
"testing"
"time"
)
@@ -54,7 +55,7 @@ func TestParseMainErrors(t *testing.T) {
{`(log "rel/x")`, `k:1:6: log path must be absolute or start with ~`},
{`(defaults (recursive maybe))`, `k:1:22: recursive is yes or no, not maybe`},
{`(defaults (rule "x"))`, `k:1:11: defaults holds settings like (min-age 2m); got (rule "x")`},
- {`(inlcude "a")`, `k:1:1: unknown form (inlcude ...); krino.conf has include, log and defaults`},
+ {`(inlcude "a")`, `k:1:1: unknown form (inlcude ...); krino.conf has include, log, defaults and exclude`},
{`include`, `k:1:1: expected a form like (include ...), got include`},
{`(include "a"`, `k:1:1: "(" never closed: (include "a")`},
}
@@ -65,3 +66,18 @@ func TestParseMainErrors(t *testing.T) {
}
}
}
+
+// TestParseMainExclude: krino.conf may hold (exclude ...) forms, which
+// apply to every directory.
+func TestParseMainExclude(t *testing.T) {
+ m, errs := ParseMain("krino.conf", []byte("(include \"a\")\n(exclude (type iso))\n(exclude (name \"[.]asc$\"))\n"))
+ if len(errs) != 0 {
+ t.Fatal(errs)
+ }
+ if len(m.Excludes) != 2 || m.Excludes[0].Text != "(exclude (type iso))" {
+ t.Fatalf("Excludes = %+v", m.Excludes)
+ }
+ if _, errs := ParseMain("krino.conf", []byte("(exclude)")); len(errs) != 1 || !strings.Contains(errs[0].Msg, "(exclude) needs a condition") {
+ t.Errorf("(exclude): errs %v", errs)
+ }
+}
diff --git a/internal/config/settings.go b/internal/config/settings.go
index 97355a5..efc935b 100644
--- a/internal/config/settings.go
+++ b/internal/config/settings.go
@@ -34,6 +34,7 @@ type Settings struct {
MaxDepth *int
MinAge *time.Duration
MaxRead *int64
+ MaxSize *int64
Busy *[]string
OnConflict *Conflict
}
@@ -46,6 +47,7 @@ type Resolved struct {
MaxDepth int // 0 means unlimited
MinAge time.Duration
MaxRead int64
+ MaxSize int64 // 0 means unlimited
Busy []string
OnConflict Conflict
}
@@ -83,6 +85,9 @@ func (s Settings) Over(base Resolved) Resolved {
if s.MaxRead != nil {
r.MaxRead = *s.MaxRead
}
+ if s.MaxSize != nil {
+ r.MaxSize = *s.MaxSize
+ }
if s.Busy != nil {
r.Busy = *s.Busy
}
@@ -92,7 +97,7 @@ func (s Settings) Over(base Resolved) Resolved {
return r
}
-var settingNames = []string{"case", "fold", "recursive", "max-depth", "min-age", "max-read", "busy", "on-conflict"}
+var settingNames = []string{"case", "fold", "recursive", "max-depth", "min-age", "max-read", "max-size", "busy", "on-conflict"}
// ruleSettings are the settings a rule may override.
var ruleSettings = map[string]bool{"case": true, "fold": true, "on-conflict": true}
@@ -106,6 +111,7 @@ var settingHint = map[string]string{
"max-depth": "a number, like (max-depth 3)",
"min-age": "a duration, like (min-age 2m)",
"max-read": "a size, like (max-read 50M)",
+ "max-size": "a size, like (max-size 1G)",
"on-conflict": "(on-conflict suffix), skip or overwrite",
}
@@ -183,6 +189,13 @@ func (s *Settings) parse(n *sexp.Node, d *diags, seen map[string]*sexp.Node) {
return
}
s.MaxRead = &size
+ case "max-size":
+ size, err := ParseSize(v)
+ if err != nil {
+ d.at(at, "max-size: %v", err)
+ return
+ }
+ s.MaxSize = &size
case "on-conflict":
c, ok := map[string]Conflict{"suffix": ConflictSuffix, "skip": ConflictSkip, "overwrite": ConflictOverwrite}[v]
if !ok {
diff --git a/internal/config/settings_test.go b/internal/config/settings_test.go
index e46c880..341c0f3 100644
--- a/internal/config/settings_test.go
+++ b/internal/config/settings_test.go
@@ -4,6 +4,7 @@ package config
import (
"reflect"
+ "strings"
"testing"
"time"
@@ -27,12 +28,12 @@ func parseSettings(t *testing.T, src string) (Settings, []*Diag) {
func TestSettingsResolve(t *testing.T) {
s, errs := parseSettings(t, `(case strict) (fold no) (recursive yes) (max-depth 3)
- (min-age 5m) (max-read 1G) (busy ".tmp") (on-conflict skip)`)
+ (min-age 5m) (max-read 1G) (max-size 2G) (busy ".tmp") (on-conflict skip)`)
if len(errs) > 0 {
t.Fatal(errs)
}
want := Resolved{Case: CaseStrict, Fold: false, Recursive: true, MaxDepth: 3,
- MinAge: 5 * time.Minute, MaxRead: 1 << 30, Busy: []string{".tmp"}, OnConflict: ConflictSkip}
+ MinAge: 5 * time.Minute, MaxRead: 1 << 30, MaxSize: 2 << 30, Busy: []string{".tmp"}, OnConflict: ConflictSkip}
if got := s.Over(Builtin()); !reflect.DeepEqual(got, want) {
t.Fatalf("got %+v\nwant %+v", got, want)
}
@@ -83,3 +84,14 @@ func TestSettingErrors(t *testing.T) {
}
}
}
+
+// TestMaxSizeErrors: max-size takes a size like max-read does, and is not a
+// rule-level setting.
+func TestMaxSizeErrors(t *testing.T) {
+ if _, errs := parseSettings(t, "(max-size big)"); len(errs) != 1 || !strings.Contains(errs[0].Msg, "max-size") {
+ t.Errorf("(max-size big): errs %v, want one max-size error", errs)
+ }
+ if _, errs := parseSettings(t, "(max-size 10M)"); len(errs) != 0 {
+ t.Errorf("(max-size 10M): errs %v, want none", errs)
+ }
+}
diff --git a/internal/config/skel/krino.conf b/internal/config/skel/krino.conf
index dbd883b..7008dbc 100644
--- a/internal/config/skel/krino.conf
+++ b/internal/config/skel/krino.conf
@@ -19,5 +19,12 @@
;; (recursive no)
;; (min-age 2m) ; skip files modified in the last 2 minutes
;; (max-read 50M) ; no content extraction above this size
+;; (max-size 2G) ; skip files larger than this entirely
;; (busy ".part" ".aria2" ".crdownload")
;; (on-conflict suffix)) ; suffix | skip | overwrite
+
+;; Files no rule in any directory may touch. The conditions in one form
+;; must all hold; a file matching any form is set aside. Some examples:
+;; (exclude (type iso)) ; by type or extension
+;; (exclude (name "^keep-")) ; by name, a regex
+;; (exclude (type pdf) (content "confidential")) ; by content
diff --git a/internal/config/skel/template.conf b/internal/config/skel/template.conf
index c3af09d..d366716 100644
--- a/internal/config/skel/template.conf
+++ b/internal/config/skel/template.conf
@@ -15,10 +15,16 @@
;; (fold yes) ; yes: "spolka" matches "spółka"
;; (min-age 2m) ; skip files modified in the last 2 minutes
;; (max-read 50M) ; no content extraction above this size
+;; (max-size 2G) ; skip files larger than this entirely
;; (on-conflict suffix) ; suffix | skip | overwrite
;; Files and directories to leave alone, in .gitignore syntax.
(ignore "*.part" "*.crdownload" "*.aria2" ".*")
+;; Files no rule here may touch, tested before any rule. The conditions in
+;; one form must all hold; a file matching any form is set aside.
+;; (exclude (type iso img)) ; by extension
+;; (exclude (name "^keep-")) ; by name, a regex
+;; (exclude (type pdf) (content "confidential")) ; by content
;; Rules run top to bottom. Every rule that matches a file adds its actions
;; to that file; (stop) ends the search for it. Some examples: