diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-07-23 22:14:14 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-07-23 22:14:14 +0200 |
| commit | bcdd60c51b35750e76cc13f7251ccd3f3d018bc8 (patch) | |
| tree | 157214ccb974f2f7e10402996b85b9be733412ca /internal/cli | |
| parent | 390f8cb146c69d8d3a0e6e0d76ea3546f30a7611 (diff) | |
| download | lectio-bcdd60c51b35750e76cc13f7251ccd3f3d018bc8.tar.gz lectio-bcdd60c51b35750e76cc13f7251ccd3f3d018bc8.zip | |
cli,liturgy: lectio --clean prunes readings cache older than one year
Diffstat (limited to 'internal/cli')
| -rw-r--r-- | internal/cli/cli.go | 51 | ||||
| -rw-r--r-- | internal/cli/cli_test.go | 63 |
2 files changed, 113 insertions, 1 deletions
diff --git a/internal/cli/cli.go b/internal/cli/cli.go index a13f2f1..5c05713 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -39,6 +39,7 @@ Flags: -l, --lectionary WHICH new|trad (trad -> traditional) -g, --lang LANG traditional lectionary language: pl|en -u, --update harvest sigla maximally to the horizon (idempotent) + -C, --clean prune cached readings older than a year -v, --version print the version and exit -h, --help this help @@ -97,7 +98,7 @@ func Run(args []string, stdin io.Reader, stdout, stderr io.Writer) int { return 2 } - var all, raw, refresh, offline, update bool + var all, raw, refresh, offline, update, clean bool var bibleVer, compareList, lectionary, lang string var width int @@ -125,6 +126,8 @@ func Run(args []string, stdin io.Reader, stdout, stderr io.Writer) int { fs.StringVar(&lang, "lang", "", "pl|en") fs.BoolVar(&update, "u", false, "harvest sigla maximally to the horizon") fs.BoolVar(&update, "update", false, "harvest sigla maximally to the horizon") + fs.BoolVar(&clean, "C", false, "prune cached readings older than a year") + fs.BoolVar(&clean, "clean", false, "prune cached readings older than a year") if err := fs.Parse(rest); err != nil { return 2 @@ -144,6 +147,9 @@ func Run(args []string, stdin io.Reader, stdout, stderr io.Writer) int { return 2 } + if clean { + return runClean(stdout, stderr) + } if update { return runHarvest(date, stdout, stderr) } @@ -247,6 +253,49 @@ func runHarvest(date string, stdout, stderr io.Writer) int { return 0 } +// runClean handles -C/--clean: prune cached readings older than one year +// (relative to today()) and print a human-readable summary of what was +// removed. Like -u/--update, this is a maintenance mode -- it ignores DATE +// and every render flag, and is dispatched before the render paths. If both +// --clean and -u/--update are given, --clean takes precedence (see Run). +func runClean(stdout, stderr io.Writer) int { + now, err := time.Parse("2006-01-02", today()) + if err != nil { + fmt.Fprintln(stderr, "lectio:", err) + return 1 + } + before := now.AddDate(-1, 0, 0) + + removed, freed, err := liturgy.CleanCache(before) + if err != nil { + fmt.Fprintln(stderr, "lectio:", err) + return 1 + } + + cutoff := before.Format("2006-01-02") + if removed == 0 { + fmt.Fprintf(stdout, "cache already clean (nothing older than %s)\n", cutoff) + return 0 + } + entries := "entries" + if removed == 1 { + entries = "entry" + } + fmt.Fprintf(stdout, "cleaned %d cache %s older than %s (freed %s)\n", removed, entries, cutoff, formatFreed(freed)) + return 0 +} + +// formatFreed renders a byte count the way -C/--clean's summary line wants +// it: megabytes with one decimal once it's a meaningful size, kilobytes +// (also one decimal) for anything smaller. +func formatFreed(bytes int64) string { + const mb = 1024 * 1024 + if bytes >= mb { + return fmt.Sprintf("%.1f MB", float64(bytes)/mb) + } + return fmt.Sprintf("%.1f KB", float64(bytes)/1024) +} + // fetchAndPrint is the shared single-version render path (default version or // -b/--bible): fetch via the readings router, apply the offline version // swap, then render each section's heading and render.GatherVersion blocks. diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index d0c8791..5004af4 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -2,6 +2,8 @@ package cli import ( "bytes" + "os" + "path/filepath" "strings" "testing" ) @@ -126,6 +128,67 @@ func TestBannerForLang(t *testing.T) { } } +// TestCleanEmptyCache exercises -C/--clean's dispatch as a maintenance mode: +// it must be handled before any render path (and before the network-hitting +// -u/--update path) is reached. Pointing XDG_CACHE_HOME at an empty temp dir +// keeps liturgy.CleanCache's directory-read hermetic (no network), and an +// empty dir exercises the "nothing to remove" branch of the summary line. +func TestCleanEmptyCache(t *testing.T) { + t.Setenv("XDG_CACHE_HOME", t.TempDir()) + var out, errb bytes.Buffer + code := Run([]string{"-C"}, nil, &out, &errb) + if code != 0 { + t.Fatalf("--clean code=%d want 0 (stderr=%q)", code, errb.String()) + } + if !strings.Contains(out.String(), "clean") { + t.Errorf("--clean stdout=%q, want a clean/nothing message", out.String()) + } +} + +// TestCleanRemovesOldEntries exercises the non-empty summary branch: a stale +// cache pair sitting in XDG_CACHE_HOME must be reported as removed, entirely +// via the filesystem (no network involved). +func TestCleanRemovesOldEntries(t *testing.T) { + dir := t.TempDir() + t.Setenv("XDG_CACHE_HOME", dir) + cacheDir := filepath.Join(dir, "lectio") + if err := os.MkdirAll(cacheDir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(cacheDir, "2020-01-01.html"), []byte("<html>old</html>"), 0o644); err != nil { + t.Fatal(err) + } + + var out, errb bytes.Buffer + code := Run([]string{"--clean"}, nil, &out, &errb) + if code != 0 { + t.Fatalf("--clean code=%d want 0 (stderr=%q)", code, errb.String()) + } + if !strings.Contains(out.String(), "cleaned 1 cache entry") { + t.Errorf("--clean stdout=%q, want a \"cleaned 1 cache entry\" message", out.String()) + } + if _, err := os.Stat(filepath.Join(cacheDir, "2020-01-01.html")); !os.IsNotExist(err) { + t.Errorf("2020-01-01.html still exists after --clean") + } +} + +// TestCleanPreferredOverUpdate exercises Run's stated precedence: when both +// --clean and -u/--update are given, --clean wins and -u's network-hitting +// harvest path is never reached (proven here by the empty-cache dir plus a +// zero exit code -- a network attempt against no test server would either +// hang or return an error/non-zero code). +func TestCleanPreferredOverUpdate(t *testing.T) { + t.Setenv("XDG_CACHE_HOME", t.TempDir()) + var out, errb bytes.Buffer + code := Run([]string{"--clean", "-u"}, nil, &out, &errb) + if code != 0 { + t.Fatalf("--clean -u code=%d want 0 (stderr=%q)", code, errb.String()) + } + if !strings.Contains(out.String(), "clean") { + t.Errorf("--clean -u stdout=%q, want a clean/nothing message", out.String()) + } +} + // TestDateTokenAnyPosition exercises extractDate directly: the date token // is found regardless of where it appears among other flags. func TestDateTokenAnyPosition(t *testing.T) { |
