- commit
- fc16c4a
- parent
- 0b02e7c
- author
- Eric Bower
- date
- 2024-12-04 16:09:02 +0000 UTC
feat(pgs): cli command to manually clear cache for project
4 files changed,
+114,
-32
+37,
-1
1@@ -52,7 +52,7 @@ func projectTable(styles common.Styles, projects []*db.Project, width int) *tabl
2 }
3
4 func getHelpText(styles common.Styles, userName string, width int) string {
5- helpStr := "Commands: [help, stats, ls, rm, link, unlink, prune, retain, depends, acl]\n"
6+ helpStr := "Commands: [help, stats, ls, rm, link, unlink, prune, retain, depends, acl, cache]\n"
7 helpStr += styles.Note.Render("NOTICE:") + " *must* append with `--write` for the changes to persist.\n"
8
9 projectName := "projA"
10@@ -98,6 +98,10 @@ func getHelpText(styles common.Styles, userName string, width int) string {
11 fmt.Sprintf("acl %s", projectName),
12 fmt.Sprintf("access control for `%s`", projectName),
13 },
14+ {
15+ fmt.Sprintf("cache %s", projectName),
16+ fmt.Sprintf("clear http cache for `%s`", projectName),
17+ },
18 }
19
20 t := table.New().
21@@ -120,6 +124,7 @@ type Cmd struct {
22 Styles common.Styles
23 Width int
24 Height int
25+ Cfg *shared.ConfigSite
26 }
27
28 func (c *Cmd) output(out string) {
29@@ -484,3 +489,34 @@ func (c *Cmd) acl(projectName, aclType string, acls []string) error {
30 }
31 return nil
32 }
33+
34+func (c *Cmd) cache(projectName string) error {
35+ c.Log.Info(
36+ "user running `cache` command",
37+ "user", c.User.Name,
38+ "project", projectName,
39+ )
40+ c.output(fmt.Sprintf("clearing http cache for %s", projectName))
41+ if c.Write {
42+ surrogate := getSurrogateKey(c.User.Name, projectName)
43+ return purgeCache(c.Cfg, surrogate)
44+ }
45+ return nil
46+}
47+
48+func (c *Cmd) cacheAll() error {
49+ isAdmin := c.Dbpool.HasFeatureForUser(c.User.ID, "admin")
50+ if !isAdmin {
51+ return fmt.Errorf("must be admin to use this command")
52+ }
53+
54+ c.Log.Info(
55+ "admin running `cache-all` command",
56+ "user", c.User.Name,
57+ )
58+ c.output("clearing http cache for all sites")
59+ if c.Write {
60+ return purgeAllCache(c.Cfg)
61+ }
62+ return nil
63+}
+8,
-31
1@@ -6,7 +6,6 @@ import (
2 "io"
3 "io/fs"
4 "log/slog"
5- "net/http"
6 "os"
7 "path"
8 "path/filepath"
9@@ -412,7 +411,9 @@ func (h *UploadAssetHandler) Write(s ssh.Session, entry *sendutils.FileEntry) (s
10 utils.BytesToGB(maxSize),
11 (float32(nextStorageSize)/float32(maxSize))*100,
12 )
13- h.CacheClearingQueue <- fmt.Sprintf("%s-%s", user.Name, projectName)
14+
15+ surrogate := getSurrogateKey(user.Name, projectName)
16+ h.CacheClearingQueue <- surrogate
17
18 return str, nil
19 }
20@@ -480,7 +481,10 @@ func (h *UploadAssetHandler) Delete(s ssh.Session, entry *sendutils.FileEntry) e
21 }
22 }
23 err = h.Storage.DeleteObject(bucket, assetFilepath)
24- h.CacheClearingQueue <- fmt.Sprintf("%s-%s", user.Name, projectName)
25+
26+ surrogate := getSurrogateKey(user.Name, projectName)
27+ h.CacheClearingQueue <- surrogate
28+
29 return err
30 }
31
32@@ -533,7 +537,6 @@ func (h *UploadAssetHandler) writeAsset(reader io.Reader, data *FileData) (int64
33 // Repeated messages for the same site are grouped so that we only flush once
34 // per site per 5 seconds.
35 func runCacheQueue(ch chan string, cfg *shared.ConfigSite) {
36- cacheApiUrl := fmt.Sprintf("https://%s/souin-api/souin/", cfg.Domain)
37 var pendingFlushes sync.Map
38 tick := time.Tick(5 * time.Second)
39 for {
40@@ -544,7 +547,7 @@ func runCacheQueue(ch chan string, cfg *shared.ConfigSite) {
41 go func() {
42 pendingFlushes.Range(func(key, value any) bool {
43 pendingFlushes.Delete(key)
44- err := purgeCache(key.(string), cacheApiUrl, cfg.CacheUser, cfg.CachePassword)
45+ err := purgeCache(cfg, key.(string))
46 if err != nil {
47 cfg.Logger.Error("failed to clear cache", "err", err.Error())
48 }
49@@ -554,29 +557,3 @@ func runCacheQueue(ch chan string, cfg *shared.ConfigSite) {
50 }
51 }
52 }
53-
54-// purgeCache send an HTTP request to the pgs Caddy instance which purges
55-// cached entries for a given subdomain (like "fakeuser-www-proj"). We set a
56-// "surrogate-key: <subdomain>" header on every pgs response which ensures all
57-// cached assets for a given subdomain are grouped under a single key (which is
58-// separate from the "GET-https-example.com-/path" key used for serving files
59-// from the cache).
60-func purgeCache(subdomain string, cacheApiUrl string, username string, password string) error {
61- client := &http.Client{
62- Timeout: time.Second * 5,
63- }
64- req, err := http.NewRequest("PURGE", cacheApiUrl, nil)
65- if err != nil {
66- return err
67- }
68- req.Header.Add("Surrogate-Key", subdomain)
69- req.SetBasicAuth(username, password)
70- resp, err := client.Do(req)
71- if err != nil {
72- return err
73- }
74- if resp.StatusCode != 204 {
75- return fmt.Errorf("received unexpected response code %d", resp.StatusCode)
76- }
77- return nil
78-}
+51,
-0
1@@ -0,0 +1,51 @@
2+package pgs
3+
4+import (
5+ "fmt"
6+ "net/http"
7+ "time"
8+
9+ "github.com/picosh/pico/shared"
10+)
11+
12+func getSurrogateKey(userName, projectName string) string {
13+ return fmt.Sprintf("%s-%s", userName, projectName)
14+}
15+
16+func getCacheApiUrl(cfg *shared.ConfigSite) string {
17+ return fmt.Sprintf("%s://%s/souin-api/souin/", cfg.Protocol, cfg.Domain)
18+}
19+
20+// purgeCache send an HTTP request to the pgs Caddy instance which purges
21+// cached entries for a given subdomain (like "fakeuser-www-proj"). We set a
22+// "surrogate-key: <subdomain>" header on every pgs response which ensures all
23+// cached assets for a given subdomain are grouped under a single key (which is
24+// separate from the "GET-https-example.com-/path" key used for serving files
25+// from the cache).
26+func purgeCache(cfg *shared.ConfigSite, surrogate string) error {
27+ cacheApiUrl := getCacheApiUrl(cfg)
28+ cfg.Logger.Info("purging cache", "url", cacheApiUrl, "surrogate", surrogate)
29+ client := &http.Client{
30+ Timeout: time.Second * 5,
31+ }
32+ req, err := http.NewRequest("PURGE", cacheApiUrl, nil)
33+ if err != nil {
34+ return err
35+ }
36+ if surrogate != "" {
37+ req.Header.Add("Surrogate-Key", surrogate)
38+ }
39+ req.SetBasicAuth(cfg.CacheUser, cfg.CachePassword)
40+ resp, err := client.Do(req)
41+ if err != nil {
42+ return err
43+ }
44+ if resp.StatusCode != 204 {
45+ return fmt.Errorf("received unexpected response code %d", resp.StatusCode)
46+ }
47+ return nil
48+}
49+
50+func purgeAllCache(cfg *shared.ConfigSite) error {
51+ return purgeCache(cfg, "")
52+}
+18,
-0
1@@ -106,6 +106,7 @@ func WishMiddleware(handler *UploadAssetHandler) wish.Middleware {
2 Styles: styles,
3 Width: width,
4 Height: height,
5+ Cfg: handler.Cfg,
6 }
7
8 cmd := strings.TrimSpace(args[0])
9@@ -121,6 +122,12 @@ func WishMiddleware(handler *UploadAssetHandler) wish.Middleware {
10 err := opts.ls()
11 opts.bail(err)
12 return
13+ } else if cmd == "cache-all" {
14+ opts.Write = true
15+ err := opts.cacheAll()
16+ opts.notice()
17+ opts.bail(err)
18+ return
19 } else {
20 next(sesh)
21 return
22@@ -212,6 +219,17 @@ func WishMiddleware(handler *UploadAssetHandler) wish.Middleware {
23 opts.notice()
24 opts.bail(err)
25 return
26+ } else if cmd == "cache" {
27+ cacheCmd, write := flagSet("cache", sesh)
28+ if !flagCheck(cacheCmd, projectName, cmdArgs) {
29+ return
30+ }
31+ opts.Write = *write
32+
33+ err := opts.cache(projectName)
34+ opts.notice()
35+ opts.bail(err)
36+ return
37 } else if cmd == "acl" {
38 aclCmd, write := flagSet("acl", sesh)
39 aclType := aclCmd.String("type", "", "access type: public, pico, pubkeys")