diff options
Diffstat (limited to 'internal/config')
| -rw-r--r-- | internal/config/dir.go | 38 | ||||
| -rw-r--r-- | internal/config/dir_test.go | 36 | ||||
| -rw-r--r-- | internal/config/main.go | 7 | ||||
| -rw-r--r-- | internal/config/main_test.go | 18 | ||||
| -rw-r--r-- | internal/config/settings.go | 15 | ||||
| -rw-r--r-- | internal/config/settings_test.go | 16 | ||||
| -rw-r--r-- | internal/config/skel/krino.conf | 7 | ||||
| -rw-r--r-- | internal/config/skel/template.conf | 6 |
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: |
