package thumbs import ( "crypto/sha256" "encoding/hex" "errors" "os" "path/filepath" "strconv" ) // Cache keeps finished previews on disk, under Ham Lab's own data directory: one JPEG per // (path, size, mtime), so a file that changes gets a new preview and an unchanged one is // never decoded twice. A file ffmpeg could not decode is remembered the same way, with // ffmpeg's reason beside it, so a folder of odd files does not re-run ffmpeg on every // visit. Nothing expires: a preview is a few kilobytes, and only files somebody actually // scrolled past are ever made. type Cache struct{ Dir string } // Key identifies one file's preview. "v1" is the preview format: bump it to retire every // cached preview at once. func Key(path string, size, mtime int64) string { h := sha256.Sum256([]byte("v1\x00" + path + "\x00" + strconv.FormatInt(size, 10) + "\x00" + strconv.FormatInt(mtime, 10))) return hex.EncodeToString(h[:]) } func (c Cache) file(key, ext string) string { return filepath.Join(c.Dir, key[:2], key+ext) } // Get returns the preview, or the remembered reason it could not be made. func (c Cache) Get(key string) (jpeg []byte, failure string, found bool) { if len(key) < 2 { return nil, "", false } if b, err := os.ReadFile(c.file(key, ".jpg")); err == nil && len(b) > 0 { return b, "", true } if b, err := os.ReadFile(c.file(key, ".err")); err == nil { return nil, string(b), true } return nil, "", false } // Put keeps a preview; PutFailure keeps the reason there is none. func (c Cache) Put(key string, jpeg []byte) error { if len(jpeg) == 0 { return errors.New("empty preview") } return c.write(c.file(key, ".jpg"), jpeg) } func (c Cache) PutFailure(key, reason string) error { return c.write(c.file(key, ".err"), []byte(reason)) } // write lands the file whole or not at all: a reader never sees a half-written JPEG. func (c Cache) write(dst string, b []byte) error { if err := os.MkdirAll(filepath.Dir(dst), 0o700); err != nil { return err } tmp, err := os.CreateTemp(filepath.Dir(dst), ".tmp-*") if err != nil { return err } if _, err := tmp.Write(b); err != nil { tmp.Close() os.Remove(tmp.Name()) return err } if err := tmp.Close(); err != nil { os.Remove(tmp.Name()) return err } if err := os.Rename(tmp.Name(), dst); err != nil { os.Remove(tmp.Name()) return err } return nil }