Commit d32eeb3

Eric Bower  ·  2025-08-08 21:43:58 -0400 EDT
parent 92e3a32
refactor(feeds): deprecate `digest_interval`; use `cron` instead

Reference: https://pico.sh/feeds#cron
5 files changed,  +64, -83
+10, -10
......@@ -6,7 +6,7 @@ DB_CONTAINER?=pico-postgres-1
66 DOCKER_TAG?=$(shell git log --format="%H" -n 1)
77 DOCKER_PLATFORM?=linux/amd64,linux/arm64
88 DOCKER_CMD?=docker
9-DOCKER_BUILDX_BUILD?=$(DOCKER_CMD) buildx build --push --platform $(DOCKER_PLATFORM)
9+DOCKER_BUILDX_BUILD?=$(DOCKER_CMD) buildx build --push --platform $(DOCKER_PLATFORM) -t
1010 WRITE?=0
1111
1212 smol:
......@@ -42,36 +42,36 @@ endif
4242 .PHONY: bp-setup
4343
4444 bp-caddy: bp-setup
45- $(DOCKER_BUILDX_BUILD) -t ghcr.io/picosh/pico/caddy:$(DOCKER_TAG) ./caddy
45+ $(DOCKER_BUILDX_BUILD) ghcr.io/picosh/pico/caddy:$(DOCKER_TAG) ./caddy
4646 .PHONY: bp-caddy
4747
4848 bp-auth: bp-setup
49- $(DOCKER_BUILDX_BUILD) -t ghcr.io/picosh/pico/auth-web:$(DOCKER_TAG) --build-arg APP=auth --target release-web .
49+ $(DOCKER_BUILDX_BUILD) ghcr.io/picosh/pico/auth-web:$(DOCKER_TAG) --build-arg APP=auth --target release-web .
5050 .PHONY: bp-auth
5151
5252 bp-pgs-cdn: bp-setup
53- $(DOCKER_BUILDX_BUILD) -t ghcr.io/picosh/pico/pgs-cdn:$(DOCKER_TAG) --target release-web -f Dockerfile.cdn .
53+ $(DOCKER_BUILDX_BUILD) ghcr.io/picosh/pico/pgs-cdn:$(DOCKER_TAG) --target release-web -f Dockerfile.cdn .
5454 .PHONY: bp-pgs-cdn
5555
5656 bp-pgs-standalone: bp-setup
57- $(DOCKER_BUILDX_BUILD) --manifest ghcr.io/picosh/pgs:$(DOCKER_TAG) --target release -f Dockerfile.standalone .
57+ $(DOCKER_BUILDX_BUILD) ghcr.io/picosh/pgs:$(DOCKER_TAG) --target release -f Dockerfile.standalone .
5858 .PHONY: bp-pgs-standalone
5959
6060 bp-pico: bp-setup
61- $(DOCKER_BUILDX_BUILD) -t ghcr.io/picosh/pico/pico-ssh:$(DOCKER_TAG) --build-arg APP=pico --target release-ssh .
61+ $(DOCKER_BUILDX_BUILD) ghcr.io/picosh/pico/pico-ssh:$(DOCKER_TAG) --build-arg APP=pico --target release-ssh .
6262 .PHONY: bp-auth
6363
6464 bp-bouncer: bp-setup
65- $(DOCKER_BUILDX_BUILD) -t ghcr.io/picosh/pico/bouncer:$(DOCKER_TAG) ./bouncer
65+ $(DOCKER_BUILDX_BUILD) ghcr.io/picosh/pico/bouncer:$(DOCKER_TAG) ./bouncer
6666 .PHONY: bp-bouncer
6767
6868 bp-ssh-%: bp-setup
69- $(DOCKER_BUILDX_BUILD) --build-arg "APP=$*" -t "ghcr.io/picosh/pico/$*-ssh:$(DOCKER_TAG)" --target release-ssh .
69+ $(DOCKER_BUILDX_BUILD) "ghcr.io/picosh/pico/$*-ssh:$(DOCKER_TAG)" --build-arg "APP=$*" --target release-ssh .
7070 .PHONY: pgs-ssh
7171
7272 bp-%: bp-setup
73- $(DOCKER_BUILDX_BUILD) --build-arg "APP=$*" -t "ghcr.io/picosh/pico/$*-ssh:$(DOCKER_TAG)" --target release-ssh .
74- $(DOCKER_BUILDX_BUILD) --build-arg "APP=$*" -t "ghcr.io/picosh/pico/$*-web:$(DOCKER_TAG)" --target release-web .
73+ $(DOCKER_BUILDX_BUILD) "ghcr.io/picosh/pico/$*-ssh:$(DOCKER_TAG)" --build-arg "APP=$*" --target release-ssh .
74+ $(DOCKER_BUILDX_BUILD) "ghcr.io/picosh/pico/$*-web:$(DOCKER_TAG)" --build-arg "APP=$*" --target release-web .
7575 .PHONY: bp-%
7676
7777 bp-all: bp-prose bp-pastes bp-feeds bp-pgs bp-auth bp-bouncer bp-pipe bp-pgs-cdn
+8, -7
......@@ -5,6 +5,7 @@ import (
55 "text/tabwriter"
66 "time"
77
8+ "github.com/adhocore/gronx"
89 "github.com/picosh/pico/pkg/db"
910 "github.com/picosh/pico/pkg/pssh"
1011 "github.com/picosh/pico/pkg/shared"
......@@ -67,24 +68,24 @@ func Middleware(dbpool db.DB, cfg *shared.ConfigSite) pssh.SSHServerMiddleware {
6768 }
6869
6970 writer := tabwriter.NewWriter(sesh, 0, 0, 1, ' ', tabwriter.TabIndent)
70- _, _ = fmt.Fprintln(writer, "Filename\tLast Digest\tNext Digest\tInterval\tFailed Attempts")
71+ _, _ = fmt.Fprintln(writer, "Filename\tLast Digest\tNext Digest\tCron\tFailed Attempts")
7172 for _, post := range posts.Data {
7273 parsed := shared.ListParseText(post.Text)
7374
7475 nextDigest := ""
75- if parsed.Cron != "" {
76- nextDigest = parsed.Cron
77- } else {
78- digestOption := DigestOptionToTime(*post.Data.LastDigest, parsed.DigestInterval)
79- nextDigest = digestOption.Format(time.RFC3339)
76+ cron := parsed.Cron
77+ if parsed.DigestInterval != "" {
78+ cron = DigestIntervalToCron(parsed.DigestInterval)
8079 }
80+ nd, _ := gronx.NextTickAfter(cron, DateToMin(time.Now()), true)
81+ nextDigest = nd.Format(time.RFC3339)
8182 _, _ = fmt.Fprintf(
8283 writer,
8384 "%s\t%s\t%s\t%s\t%d/10\r\n",
8485 post.Filename,
8586 post.Data.LastDigest.Format(time.RFC3339),
8687 nextDigest,
87- parsed.DigestInterval,
88+ cron,
8889 post.Data.Attempts,
8990 )
9091 }
+35, -35
......@@ -84,25 +84,24 @@ func itemToTemplate(item *gofeed.Item) *FeedItemTmpl {
8484 }
8585 }
8686
87-func DigestOptionToTime(lastDigest time.Time, interval string) time.Time {
88- day := 24 * time.Hour
87+func DigestIntervalToCron(interval string) string {
8988 switch interval {
9089 case "10min":
91- return lastDigest.Add(10 * time.Minute)
90+ return "*/10 * * * *"
9291 case "1hour":
93- return lastDigest.Add(1 * time.Hour)
92+ return "0 * * * *"
9493 case "6hour":
95- return lastDigest.Add(6 * time.Hour)
94+ return "0 */6 * * *"
9695 case "12hour":
97- return lastDigest.Add(12 * time.Hour)
96+ return "0 */12 * * *"
9897 case "1day", "":
99- return lastDigest.Add(1 * day)
98+ return "0 13 * * *"
10099 case "7day":
101- return lastDigest.Add(7 * day)
100+ return "0 13 * * 0"
102101 case "30day":
103- return lastDigest.Add(30 * day)
102+ return "0 13 1 * *"
104103 default:
105- return lastDigest
104+ return "0 13 * * *"
106105 }
107106 }
108107
......@@ -146,19 +145,16 @@ func NewFetcher(dbpool db.DB, cfg *shared.ConfigSite) *Fetcher {
146145 }
147146 }
148147
149-func (f *Fetcher) Validate(post *db.Post, parsed *shared.ListParsedText, now time.Time) error {
150- lastDigest := post.Data.LastDigest
151- if lastDigest == nil {
152- return nil
153- }
154-
155- toTheMin := time.Date(
148+func DateToMin(now time.Time) time.Time {
149+ return time.Date(
156150 now.Year(), now.Month(), now.Day(),
157151 now.Hour(), now.Minute(),
158152 0, 0, // zero out second and nano-second for cron
159153 now.Location(),
160154 )
155+}
161156
157+func (f *Fetcher) Validate(post *db.Post, parsed *shared.ListParsedText, now time.Time) error {
162158 expiresAt := post.ExpiresAt
163159 if expiresAt != nil {
164160 if post.ExpiresAt.Before(now) {
......@@ -166,24 +162,28 @@ func (f *Fetcher) Validate(post *db.Post, parsed *shared.ListParsedText, now tim
166162 }
167163 }
168164
169- if parsed.Cron != "" {
170- isDue, err := f.gron.IsDue(parsed.Cron, toTheMin)
171- if err != nil {
172- return fmt.Errorf("cron error, skipping; err: %w", err)
173- }
174- if !isDue {
175- nextTime, _ := gronx.NextTick(parsed.Cron, true)
176- return fmt.Errorf(
177- "cron not time to digest, skipping; cur run: %s, next run: %s",
178- f.gron.C.GetRef(),
179- nextTime,
180- )
181- }
182- } else if parsed.DigestInterval != "" {
183- digestAt := DigestOptionToTime(*lastDigest, parsed.DigestInterval)
184- if digestAt.After(now) {
185- return fmt.Errorf("(%s) not time to digest, skipping", digestAt.Format(time.RFC3339))
186- }
165+ cron := parsed.Cron
166+ // support for posts with deprecated `digest_interval` property
167+ if parsed.DigestInterval != "" {
168+ cron = DigestIntervalToCron(parsed.DigestInterval)
169+ }
170+
171+ if !f.gron.IsValid(cron) {
172+ return fmt.Errorf("(%s) is invalid `cron`, skipping", cron)
173+ }
174+
175+ dt := DateToMin(now)
176+ isDue, err := f.gron.IsDue(cron, dt)
177+ if err != nil {
178+ return fmt.Errorf("cron error, skipping; err: %w", err)
179+ }
180+ if !isDue {
181+ nextTime, _ := gronx.NextTickAfter(cron, dt, true)
182+ return fmt.Errorf(
183+ "cron not time to digest, skipping; cur run: %s, next run: %s",
184+ f.gron.C.GetRef(),
185+ nextTime,
186+ )
187187 }
188188 return nil
189189 }
+11, -9
......@@ -4,10 +4,9 @@ import (
44 "errors"
55 "fmt"
66 "net/url"
7-
87 "strings"
9- "time"
108
9+ "github.com/adhocore/gronx"
1110 "github.com/picosh/pico/pkg/db"
1211 "github.com/picosh/pico/pkg/filehandlers"
1312 "github.com/picosh/pico/pkg/pssh"
......@@ -51,6 +50,16 @@ func (p *FeedHooks) FileValidate(s *pssh.SSHServerConnSession, data *filehandler
5150 return false, fmt.Errorf("ERROR: no email variable detected for %s, check the format of your file, skipping", data.Filename)
5251 }
5352
53+ if parsed.DigestInterval != "" {
54+ return false, fmt.Errorf("ERROR: `digest_interval` is deprecated; use `cron`: https://pico.sh/feeds#cron")
55+ }
56+
57+ if parsed.Cron != "" {
58+ if !gronx.IsValid(parsed.Cron) {
59+ return false, fmt.Errorf("ERROR: `cron` is invalid, reference: https://github.com/adhocore/gronx?tab=readme-ov-file#cron-expression")
60+ }
61+ }
62+
5463 var allErr error
5564 for _, txt := range parsed.Items {
5665 u := ""
......@@ -74,12 +83,5 @@ func (p *FeedHooks) FileValidate(s *pssh.SSHServerConnSession, data *filehandler
7483 }
7584
7685 func (p *FeedHooks) FileMeta(s *pssh.SSHServerConnSession, data *filehandlers.PostMetaData) error {
77- if data.Data.LastDigest == nil {
78- now := time.Now()
79- // let it run on the next loop
80- dd := now.AddDate(0, 0, -31)
81- data.Data.LastDigest = &dd
82- }
83-
8486 return nil
8587 }
+0, -22
......@@ -9,22 +9,10 @@ import (
99 "strings"
1010 "time"
1111
12- "slices"
13-
14- "github.com/adhocore/gronx"
1512 "github.com/araddon/dateparse"
1613 )
1714
1815 var reIndent = regexp.MustCompile(`^[[:blank:]]+`)
19-var DigestIntervalOpts = []string{
20- "10min",
21- "1hour",
22- "6hour",
23- "12hour",
24- "1day",
25- "7day",
26- "30day",
27-}
2816
2917 type ListParsedText struct {
3018 Items []*ListItem
......@@ -124,18 +112,8 @@ func TokenToMetaField(meta *ListMetaData, token *SplitToken) error {
124112 case "layout":
125113 meta.Layout = token.Value
126114 case "digest_interval":
127- if !slices.Contains(DigestIntervalOpts, token.Value) {
128- return fmt.Errorf(
129- "(%s) is not a valid option, choose from [%s]",
130- token.Value,
131- strings.Join(DigestIntervalOpts, ","),
132- )
133- }
134115 meta.DigestInterval = token.Value
135116 case "cron":
136- if !gronx.IsValid(token.Value) {
137- return fmt.Errorf("(%s) is not in a valid cron format: https://github.com/adhocore/gronx?tab=readme-ov-file#cron-expression", token.Value)
138- }
139117 meta.Cron = token.Value
140118 case "email":
141119 meta.Email = token.Value