summaryrefslogtreecommitdiff
path: root/internal/cli
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-07-23 22:14:14 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-07-23 22:14:14 +0200
commitbcdd60c51b35750e76cc13f7251ccd3f3d018bc8 (patch)
tree157214ccb974f2f7e10402996b85b9be733412ca /internal/cli
parent390f8cb146c69d8d3a0e6e0d76ea3546f30a7611 (diff)
downloadlectio-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.go51
-rw-r--r--internal/cli/cli_test.go63
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) {