commit - ed1a61d7093471344579fb8d0e3632134dc1dc26
commit + 635ade7fa06283937355616d5bc0d2d38e879043
blob - 6d2879c6eeac9346f577d13ae7e63840f80280d8
blob + 5760a25e191636c36032a100e12bdaf0560cf6c5
--- client.go
+++ client.go
"os"
"path"
"strconv"
+ "sync"
"time"
)
Debug bool
authToken string
instance *url.URL
+ cache *cache
ready bool
}
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
}
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)
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) {
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)
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
+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
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 {
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
)
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
}
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{
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: ".",