Commit Diff


commit - ed1a61d7093471344579fb8d0e3632134dc1dc26
commit + 635ade7fa06283937355616d5bc0d2d38e879043
blob - 6d2879c6eeac9346f577d13ae7e63840f80280d8
blob + 5760a25e191636c36032a100e12bdaf0560cf6c5
--- client.go
+++ client.go
@@ -11,6 +11,7 @@ import (
 	"os"
 	"path"
 	"strconv"
+	"sync"
 	"time"
 )
 
@@ -21,6 +22,7 @@ type Client struct {
 	Debug     bool
 	authToken string
 	instance  *url.URL
+	cache     *cache
 	ready     bool
 }
 
@@ -48,6 +50,13 @@ func (c *Client) init() error {
 	if c.Client == nil {
 		c.Client = http.DefaultClient
 	}
+	if c.cache == nil {
+		c.cache = &cache{
+			post:      make(map[int]entry),
+			community: make(map[string]entry),
+			mu:        &sync.Mutex{},
+		}
+	}
 	c.ready = true
 	return nil
 }
@@ -100,6 +109,12 @@ func (c *Client) LookupCommunity(name string) (Communi
 			return Community{}, Counts{}, err
 		}
 	}
+	if ent, ok := c.cache.community[name]; ok {
+		if time.Now().Before(ent.expiry) {
+			return ent.community, Counts{}, nil
+		}
+		c.cache.delete(ent.post, ent.community)
+	}
 
 	params := map[string]string{"name": name}
 	resp, err := c.get("community", params)
@@ -123,7 +138,16 @@ func (c *Client) LookupCommunity(name string) (Communi
 	if err := json.NewDecoder(resp.Body).Decode(&cres); err != nil {
 		return Community{}, Counts{}, fmt.Errorf("decode community response: %w", err)
 	}
-	return cres.View.Community, cres.View.Counts, nil
+	community := cres.View.Community
+	age := extractMaxAge(resp.Header)
+	if age != "" {
+		dur, err := parseMaxAge(age)
+		if err != nil {
+			return community, Counts{}, fmt.Errorf("parse cache max age from response header: %w", err)
+		}
+		c.cache.store(Post{}, community, dur)
+	}
+	return community, cres.View.Counts, nil
 }
 
 func (c *Client) Posts(community string, mode ListMode) ([]Post, error) {
@@ -171,6 +195,12 @@ func (c *Client) LookupPost(id int) (Post, error) {
 			return Post{}, err
 		}
 	}
+	if ent, ok := c.cache.post[id]; ok {
+		if time.Now().Before(ent.expiry) {
+			return ent.post, nil
+		}
+		c.cache.delete(ent.post, Community{})
+	}
 
 	params := map[string]string{"id": strconv.Itoa(id)}
 	resp, err := c.get("post", params)
@@ -184,6 +214,14 @@ func (c *Client) LookupPost(id int) (Post, error) {
 		return Post{}, fmt.Errorf("remote status %s: %w", resp.Status, decodeError(resp.Body))
 	}
 	post, _, _, err := decodePostResponse(resp.Body)
+	age := extractMaxAge(resp.Header)
+	if age != "" {
+		dur, err := parseMaxAge(age)
+		if err != nil {
+			return post, fmt.Errorf("parse cache max age from response header: %w", err)
+		}
+		c.cache.store(post, Community{}, dur)
+	}
 	return post, err
 }
 
blob - /dev/null
blob + c131343175181319f4127c2ac07bd312a89a7621 (mode 644)
--- /dev/null
+++ cache.go
@@ -0,0 +1,74 @@
+package lemmy
+
+import (
+	"fmt"
+	"net/http"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+)
+
+type cache struct {
+	post      map[int]entry
+	community map[string]entry
+	mu        *sync.Mutex
+}
+
+type entry struct {
+	post      Post
+	community Community
+	expiry    time.Time
+}
+
+func (c *cache) store(p Post, com Community, dur time.Duration) {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+	t := time.Now().Add(dur)
+	entry := entry{expiry: t}
+	if p.Name() != "" {
+		entry.post = p
+		c.post[p.ID] = entry
+	}
+	if com.Name() != "" {
+		entry.community = com
+		c.community[com.Name()] = entry
+	}
+}
+
+func (c *cache) delete(p Post, com Community) {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+	delete(c.post, p.ID)
+	delete(c.community, com.Name())
+}
+
+// max-age=50
+func parseMaxAge(s string) (time.Duration, error) {
+	var want string
+	elems := strings.Split(s, ",")
+	for i := range elems {
+		elems[i] = strings.TrimSpace(elems[i])
+		if strings.HasPrefix(elems[i], "max-age") {
+			want = elems[i]
+		}
+	}
+	_, num, found := strings.Cut(want, "=")
+	if !found {
+		return 0, fmt.Errorf("missing = separator")
+	}
+	n, err := strconv.Atoi(num)
+	if err != nil {
+		return 0, fmt.Errorf("parse seconds: %w", err)
+	}
+	return time.Duration(n) * time.Second, nil
+}
+
+// Cache-Control: public, max-age=50
+func extractMaxAge(header http.Header) string {
+	cc := header.Get("Cache-Control")
+	if !strings.Contains(cc, "max-age=") {
+		return ""
+	}
+	return cc
+}
blob - 9b645de3870778f579e1f3eab608be62c1b56010
blob + e1617c0cfee440834afd7b0c7d53a237622357c0
--- cmd/Lemmy/comment.go
+++ cmd/Lemmy/comment.go
@@ -34,7 +34,7 @@ func parseReply(r io.Reader) (*lemmy.Comment, error) {
 	return &comment, nil
 }
 
-func printThread(w io.Writer, prefix string,  parent int, comments []lemmy.Comment) {
+func printThread(w io.Writer, prefix string, parent int, comments []lemmy.Comment) {
 	for _, child := range children(parent, comments) {
 		fprintComment(w, prefix, child)
 		if len(children(child.ID, comments)) > 0 {
@@ -47,7 +47,6 @@ func fprintComment(w io.Writer, prefix string, c lemmy
 	fmt.Fprintln(w, prefix, "From:", c.Creator)
 	fmt.Fprintln(w, prefix, "Archived-At:", c.ActivityURL)
 	fmt.Fprintln(w, prefix, c.Content)
-	println()
 }
 
 func children(parent int, pool []lemmy.Comment) []lemmy.Comment {
blob - c22a508e7f453da8e6d42bfc052b08f902576ece
blob + ca21a0e9e64a8765914273f3a210540437c7754d
--- fs/fs.go
+++ fs/fs.go
@@ -14,35 +14,14 @@ import (
 )
 
 type FS struct {
-	Client *lemmy.Client
-	// Communities holds a cache of communities.
-	Communities map[string]lemmy.Community
-	Posts       map[int]lemmy.Post
-	started     bool
+	Client  *lemmy.Client
+	started bool
 }
 
 func (fsys *FS) start() error {
 	if fsys.Client == nil {
 		fsys.Client = &lemmy.Client{}
 	}
-
-	if fsys.Communities == nil {
-		fsys.Communities = make(map[string]lemmy.Community)
-		if fsys.Client.Authenticated() {
-			subscribed, err := fsys.Client.Communities(lemmy.ListSubscribed)
-			if err != nil {
-				return fmt.Errorf("load subscriptions: %w", err)
-			}
-			for _, c := range subscribed {
-				fsys.Communities[c.Name()] = c
-			}
-		}
-	}
-
-	if fsys.Posts == nil {
-		fsys.Posts = make(map[int]lemmy.Post)
-	}
-
 	fsys.started = true
 	return nil
 }
@@ -71,16 +50,11 @@ func (fsys *FS) Open(name string) (fs.File, error) {
 		return nil, &fs.PathError{"open", name, fs.ErrNotExist}
 	}
 
-	community, ok := fsys.Communities[elems[0]]
-	if !ok {
-		var err error
-		community, _, err = fsys.Client.LookupCommunity(elems[0])
-		if errors.Is(err, lemmy.ErrNotFound) {
-			return nil, &fs.PathError{"open", name, fs.ErrNotExist}
-		} else if err != nil {
-			return nil, &fs.PathError{"open", name, err}
-		}
-		fsys.Communities[elems[0]] = community
+	community, _, err := fsys.Client.LookupCommunity(elems[0])
+	if errors.Is(err, lemmy.ErrNotFound) {
+		return nil, &fs.PathError{"open", name, fs.ErrNotExist}
+	} else if err != nil {
+		return nil, &fs.PathError{"open", name, err}
 	}
 	if len(elems) == 1 {
 		return &lFile{
@@ -138,10 +112,14 @@ func (fsys *FS) Open(name string) (fs.File, error) {
 
 func (fsys *FS) openRoot() (fs.File, error) {
 	dirinfo := new(dirInfo)
-	for _, c := range fsys.Communities {
+	communities, err := fsys.Client.Communities(lemmy.ListAll)
+	if err != nil {
+		return nil, err
+	}
+	for _, c := range communities {
 		c := c
-		de := fs.FileInfoToDirEntry(&c)
-		dirinfo.entries = append(dirinfo.entries, de)
+		dent := fs.FileInfoToDirEntry(&c)
+		dirinfo.entries = append(dirinfo.entries, dent)
 	}
 	return &dummy{
 		name:     ".",