Skip to content

Commit

Permalink
feat(pgs): cli command to manually clear cache for project
Browse files Browse the repository at this point in the history
  • Loading branch information
neurosnap committed Dec 4, 2024
1 parent 0b02e7c commit fc16c4a
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 32 deletions.
38 changes: 37 additions & 1 deletion pgs/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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().
Expand All @@ -120,6 +124,7 @@ type Cmd struct {
Styles common.Styles
Width int
Height int
Cfg *shared.ConfigSite
}

func (c *Cmd) output(out string) {
Expand Down Expand Up @@ -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
}
39 changes: 8 additions & 31 deletions pgs/uploader.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"io"
"io/fs"
"log/slog"
"net/http"
"os"
"path"
"path/filepath"
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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 {
Expand All @@ -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())
}
Expand All @@ -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: <subdomain>" 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
}
51 changes: 51 additions & 0 deletions pgs/web_cache.go
Original file line number Diff line number Diff line change
@@ -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: <subdomain>" 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, "")
}
18 changes: 18 additions & 0 deletions pgs/wish.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ func WishMiddleware(handler *UploadAssetHandler) wish.Middleware {
Styles: styles,
Width: width,
Height: height,
Cfg: handler.Cfg,
}

cmd := strings.TrimSpace(args[0])
Expand All @@ -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
Expand Down Expand Up @@ -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")
Expand Down

0 comments on commit fc16c4a

Please sign in to comment.