repos / pico

pico services - prose.sh, pastes.sh, imgs.sh, feeds.sh, pgs.sh
git clone https://github.com/picosh/pico.git

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
M pgs/cli.go
+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+}
M pgs/uploader.go
+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-}
A pgs/web_cache.go
+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+}
M pgs/wish.go
+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")