Files previews: the cache is one database file, <data dir>/thumbs.db
bbolt instead of a tree of hash-named JPEGs: pure Go, so the binary stays static; one file per machine; an entry carries where the preview came from (engine, codec, size, when) beside the picture, and a count rides along so the tab can say what it holds without walking anything. A miss never creates the file, so a machine nobody browses with previews on never has one; a cache that cannot be opened is logged and previews are still made and served, just not kept. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
51c718e8e3
commit
e198471f8c
+26
-12
@@ -1,7 +1,7 @@
|
||||
package api
|
||||
|
||||
// Previews for the Files tab: GET /api/files/thumb hands back one small JPEG of an image or
|
||||
// a video, made by internal/thumbs and kept under <data dir>/thumbs/. The work is bounded
|
||||
// a video, made by internal/thumbs and kept in <data dir>/thumbs.db. The work is bounded
|
||||
// two ways — a handful of ffmpegs at once, and one per file however many rows ask for it —
|
||||
// because a folder of a thousand photos asks for every row the moment previews go on.
|
||||
//
|
||||
@@ -32,7 +32,7 @@ const thumbTimeout = 30 * time.Second
|
||||
const capsTTL = 10 * time.Minute // how long a probe is believed; a refresh after an install or a group change does not wait
|
||||
|
||||
type thumbSvc struct {
|
||||
cache thumbs.Cache
|
||||
cache *thumbs.Cache
|
||||
self string
|
||||
sem chan struct{}
|
||||
mu sync.Mutex
|
||||
@@ -61,7 +61,7 @@ func newThumbSvc(dataDir, self string) *thumbSvc {
|
||||
if n > 4 {
|
||||
n = 4
|
||||
}
|
||||
return &thumbSvc{cache: thumbs.Cache{Dir: filepath.Join(dataDir, "thumbs")}, self: self, sem: make(chan struct{}, n), caps: map[bool]*capsEntry{}, calls: map[string]*thumbCall{}}
|
||||
return &thumbSvc{cache: thumbs.NewCache(filepath.Join(dataDir, "thumbs.db")), self: self, sem: make(chan struct{}, n), caps: map[bool]*capsEntry{}, calls: map[string]*thumbCall{}}
|
||||
}
|
||||
|
||||
// capsFor is what previews can be made with, probed once and kept for a while.
|
||||
@@ -208,7 +208,9 @@ func (s *Server) filesThumb(w http.ResponseWriter, r *http.Request) {
|
||||
var de *thumbs.DecodeError
|
||||
switch {
|
||||
case errors.As(err, &de):
|
||||
_ = s.thumbs.cache.PutFailure(key, de.Msg)
|
||||
if err := s.thumbs.cache.Put(key, thumbs.Entry{Path: p, Failure: de.Msg}); err != nil {
|
||||
log.Printf("thumbs: cache: %v", err)
|
||||
}
|
||||
log.Printf("thumbs: %s: %s", p, de.Msg)
|
||||
jsonErr(w, 422, de.Msg)
|
||||
case errors.Is(err, context.DeadlineExceeded):
|
||||
@@ -221,21 +223,26 @@ func (s *Server) filesThumb(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
return
|
||||
}
|
||||
if err := s.thumbs.cache.Put(key, res.JPEG); err != nil {
|
||||
log.Printf("thumbs: cache %s: %v", key[:8], err)
|
||||
if err := s.thumbs.cache.Put(key, thumbs.Entry{Path: p, Engine: res.Engine, Codec: res.Codec, Width: res.Width, Height: res.Height, JPEG: res.JPEG}); err != nil {
|
||||
log.Printf("thumbs: cache: %v", err)
|
||||
}
|
||||
writeThumb(w, key, res.JPEG, res.Engine)
|
||||
}
|
||||
|
||||
// serveCachedThumb answers from the cache when it can. A cache that cannot be read is a
|
||||
// miss: the preview is made again rather than refused.
|
||||
func (s *Server) serveCachedThumb(w http.ResponseWriter, key string) bool {
|
||||
data, why, found := s.thumbs.cache.Get(key)
|
||||
e, found, err := s.thumbs.cache.Get(key)
|
||||
if err != nil {
|
||||
log.Printf("thumbs: cache: %v", err)
|
||||
}
|
||||
switch {
|
||||
case !found:
|
||||
return false
|
||||
case data == nil:
|
||||
jsonErr(w, 422, why)
|
||||
case e.Failure != "":
|
||||
jsonErr(w, 422, e.Failure)
|
||||
default:
|
||||
writeThumb(w, key, data, "cache")
|
||||
writeThumb(w, key, e.JPEG, "cache ("+e.Engine+")")
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -271,7 +278,13 @@ type thumbStatus struct {
|
||||
Install *thumbInstall `json:"install,omitempty"` // ffmpeg is missing and this can put it on
|
||||
GPUFix bool `json:"gpu_fix,omitempty"` // a render node is there but closed to the daemon's user, and this can open it
|
||||
User string `json:"user"`
|
||||
CacheDir string `json:"cache_dir"`
|
||||
Cache thumbCache `json:"cache"`
|
||||
}
|
||||
|
||||
type thumbCache struct {
|
||||
Path string `json:"path"`
|
||||
Entries int64 `json:"entries"`
|
||||
Bytes int64 `json:"bytes"`
|
||||
}
|
||||
|
||||
type thumbInstall struct {
|
||||
@@ -287,7 +300,8 @@ func (s *Server) thumbsStatus(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
caps, err := s.thumbs.capsFor(r.Context(), root, r.URL.Query().Get("refresh") == "1")
|
||||
st := thumbStatus{GPUs: []thumbs.GPU{}, User: s.user, CacheDir: s.thumbs.cache.Dir}
|
||||
st := thumbStatus{GPUs: []thumbs.GPU{}, User: s.user, Cache: thumbCache{Path: s.thumbs.cache.Path}}
|
||||
st.Cache.Entries, st.Cache.Bytes = s.thumbs.cache.Stats()
|
||||
if root {
|
||||
st.User = "root"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user