From fc16c4a5a53a606536f20b48f6129db254ab929d Mon Sep 17 00:00:00 2001 From: Eric Bower Date: Wed, 4 Dec 2024 11:09:02 -0500 Subject: [PATCH] feat(pgs): cli command to manually clear cache for project --- pgs/cli.go | 38 +++++++++++++++++++++++++++++++++++- pgs/uploader.go | 39 ++++++++---------------------------- pgs/web_cache.go | 51 ++++++++++++++++++++++++++++++++++++++++++++++++ pgs/wish.go | 18 +++++++++++++++++ 4 files changed, 114 insertions(+), 32 deletions(-) create mode 100644 pgs/web_cache.go diff --git a/pgs/cli.go b/pgs/cli.go index 267a5364..5e93926c 100644 --- a/pgs/cli.go +++ b/pgs/cli.go @@ -52,7 +52,7 @@ func projectTable(styles common.Styles, projects []*db.Project, width int) *tabl } func getHelpText(styles common.Styles, userName string, width int) string { - helpStr := "Commands: [help, stats, ls, rm, link, unlink, prune, retain, depends, acl]\n" + helpStr := "Commands: [help, stats, ls, rm, link, unlink, prune, retain, depends, acl, cache]\n" helpStr += styles.Note.Render("NOTICE:") + " *must* append with `--write` for the changes to persist.\n" projectName := "projA" @@ -98,6 +98,10 @@ func getHelpText(styles common.Styles, userName string, width int) string { fmt.Sprintf("acl %s", projectName), fmt.Sprintf("access control for `%s`", projectName), }, + { + fmt.Sprintf("cache %s", projectName), + fmt.Sprintf("clear http cache for `%s`", projectName), + }, } t := table.New(). @@ -120,6 +124,7 @@ type Cmd struct { Styles common.Styles Width int Height int + Cfg *shared.ConfigSite } func (c *Cmd) output(out string) { @@ -484,3 +489,34 @@ func (c *Cmd) acl(projectName, aclType string, acls []string) error { } return nil } + +func (c *Cmd) cache(projectName string) error { + c.Log.Info( + "user running `cache` command", + "user", c.User.Name, + "project", projectName, + ) + c.output(fmt.Sprintf("clearing http cache for %s", projectName)) + if c.Write { + surrogate := getSurrogateKey(c.User.Name, projectName) + return purgeCache(c.Cfg, surrogate) + } + return nil +} + +func (c *Cmd) cacheAll() error { + isAdmin := c.Dbpool.HasFeatureForUser(c.User.ID, "admin") + if !isAdmin { + return fmt.Errorf("must be admin to use this command") + } + + c.Log.Info( + "admin running `cache-all` command", + "user", c.User.Name, + ) + c.output("clearing http cache for all sites") + if c.Write { + return purgeAllCache(c.Cfg) + } + return nil +} diff --git a/pgs/uploader.go b/pgs/uploader.go index bd1f6bc8..44f9aa46 100644 --- a/pgs/uploader.go +++ b/pgs/uploader.go @@ -6,7 +6,6 @@ import ( "io" "io/fs" "log/slog" - "net/http" "os" "path" "path/filepath" @@ -412,7 +411,9 @@ func (h *UploadAssetHandler) Write(s ssh.Session, entry *sendutils.FileEntry) (s utils.BytesToGB(maxSize), (float32(nextStorageSize)/float32(maxSize))*100, ) - h.CacheClearingQueue <- fmt.Sprintf("%s-%s", user.Name, projectName) + + surrogate := getSurrogateKey(user.Name, projectName) + h.CacheClearingQueue <- surrogate return str, nil } @@ -480,7 +481,10 @@ func (h *UploadAssetHandler) Delete(s ssh.Session, entry *sendutils.FileEntry) e } } err = h.Storage.DeleteObject(bucket, assetFilepath) - h.CacheClearingQueue <- fmt.Sprintf("%s-%s", user.Name, projectName) + + surrogate := getSurrogateKey(user.Name, projectName) + h.CacheClearingQueue <- surrogate + return err } @@ -533,7 +537,6 @@ func (h *UploadAssetHandler) writeAsset(reader io.Reader, data *FileData) (int64 // Repeated messages for the same site are grouped so that we only flush once // per site per 5 seconds. func runCacheQueue(ch chan string, cfg *shared.ConfigSite) { - cacheApiUrl := fmt.Sprintf("https://%s/souin-api/souin/", cfg.Domain) var pendingFlushes sync.Map tick := time.Tick(5 * time.Second) for { @@ -544,7 +547,7 @@ func runCacheQueue(ch chan string, cfg *shared.ConfigSite) { go func() { pendingFlushes.Range(func(key, value any) bool { pendingFlushes.Delete(key) - err := purgeCache(key.(string), cacheApiUrl, cfg.CacheUser, cfg.CachePassword) + err := purgeCache(cfg, key.(string)) if err != nil { cfg.Logger.Error("failed to clear cache", "err", err.Error()) } @@ -554,29 +557,3 @@ func runCacheQueue(ch chan string, cfg *shared.ConfigSite) { } } } - -// purgeCache send an HTTP request to the pgs Caddy instance which purges -// cached entries for a given subdomain (like "fakeuser-www-proj"). We set a -// "surrogate-key: " header on every pgs response which ensures all -// cached assets for a given subdomain are grouped under a single key (which is -// separate from the "GET-https-example.com-/path" key used for serving files -// from the cache). -func purgeCache(subdomain string, cacheApiUrl string, username string, password string) error { - client := &http.Client{ - Timeout: time.Second * 5, - } - req, err := http.NewRequest("PURGE", cacheApiUrl, nil) - if err != nil { - return err - } - req.Header.Add("Surrogate-Key", subdomain) - req.SetBasicAuth(username, password) - resp, err := client.Do(req) - if err != nil { - return err - } - if resp.StatusCode != 204 { - return fmt.Errorf("received unexpected response code %d", resp.StatusCode) - } - return nil -} diff --git a/pgs/web_cache.go b/pgs/web_cache.go new file mode 100644 index 00000000..d6b15c46 --- /dev/null +++ b/pgs/web_cache.go @@ -0,0 +1,51 @@ +package pgs + +import ( + "fmt" + "net/http" + "time" + + "github.com/picosh/pico/shared" +) + +func getSurrogateKey(userName, projectName string) string { + return fmt.Sprintf("%s-%s", userName, projectName) +} + +func getCacheApiUrl(cfg *shared.ConfigSite) string { + return fmt.Sprintf("%s://%s/souin-api/souin/", cfg.Protocol, cfg.Domain) +} + +// purgeCache send an HTTP request to the pgs Caddy instance which purges +// cached entries for a given subdomain (like "fakeuser-www-proj"). We set a +// "surrogate-key: " header on every pgs response which ensures all +// cached assets for a given subdomain are grouped under a single key (which is +// separate from the "GET-https-example.com-/path" key used for serving files +// from the cache). +func purgeCache(cfg *shared.ConfigSite, surrogate string) error { + cacheApiUrl := getCacheApiUrl(cfg) + cfg.Logger.Info("purging cache", "url", cacheApiUrl, "surrogate", surrogate) + client := &http.Client{ + Timeout: time.Second * 5, + } + req, err := http.NewRequest("PURGE", cacheApiUrl, nil) + if err != nil { + return err + } + if surrogate != "" { + req.Header.Add("Surrogate-Key", surrogate) + } + req.SetBasicAuth(cfg.CacheUser, cfg.CachePassword) + resp, err := client.Do(req) + if err != nil { + return err + } + if resp.StatusCode != 204 { + return fmt.Errorf("received unexpected response code %d", resp.StatusCode) + } + return nil +} + +func purgeAllCache(cfg *shared.ConfigSite) error { + return purgeCache(cfg, "") +} diff --git a/pgs/wish.go b/pgs/wish.go index e0771369..5a569089 100644 --- a/pgs/wish.go +++ b/pgs/wish.go @@ -106,6 +106,7 @@ func WishMiddleware(handler *UploadAssetHandler) wish.Middleware { Styles: styles, Width: width, Height: height, + Cfg: handler.Cfg, } cmd := strings.TrimSpace(args[0]) @@ -121,6 +122,12 @@ func WishMiddleware(handler *UploadAssetHandler) wish.Middleware { err := opts.ls() opts.bail(err) return + } else if cmd == "cache-all" { + opts.Write = true + err := opts.cacheAll() + opts.notice() + opts.bail(err) + return } else { next(sesh) return @@ -212,6 +219,17 @@ func WishMiddleware(handler *UploadAssetHandler) wish.Middleware { opts.notice() opts.bail(err) return + } else if cmd == "cache" { + cacheCmd, write := flagSet("cache", sesh) + if !flagCheck(cacheCmd, projectName, cmdArgs) { + return + } + opts.Write = *write + + err := opts.cache(projectName) + opts.notice() + opts.bail(err) + return } else if cmd == "acl" { aclCmd, write := flagSet("acl", sesh) aclType := aclCmd.String("type", "", "access type: public, pico, pubkeys")