Video thumbnail generation
This commit is contained in:
@@ -26,6 +26,14 @@ type Thumbnailer interface {
|
||||
Generate(src io.Reader, dst io.Writer, width int) error
|
||||
}
|
||||
|
||||
// PathThumbnailer is an optional capability for thumbnailers whose source must
|
||||
// be a seekable file on disk rather than a stream (e.g. ffmpeg for video). When
|
||||
// a Thumbnailer also implements this, handleThumb passes the file path directly
|
||||
// and skips reading the file into memory and content-hashing it.
|
||||
type PathThumbnailer interface {
|
||||
GenerateFromPath(srcPath string, dst io.Writer, width int) error
|
||||
}
|
||||
|
||||
var thumbnailers []Thumbnailer
|
||||
|
||||
// thumbCacheDir is set from the -cache flag at startup.
|
||||
@@ -116,11 +124,39 @@ func (h *handler) handleThumb(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
digest, data, err := sourceDigest(srcFS, srcInfo)
|
||||
if err != nil {
|
||||
log.Printf("thumb digest %s: %v", rel, err)
|
||||
http.Error(w, "thumbnail failed", http.StatusInternalServerError)
|
||||
return
|
||||
// Path thumbnailers (video/ffmpeg) take the source path directly and key
|
||||
// the cache on path+mtime+size, so the file is never read into memory.
|
||||
// Content thumbnailers (images) stay content-addressed so renames reuse
|
||||
// the cache entry. Either way, generation runs through the closure below.
|
||||
var (
|
||||
digest string
|
||||
write func(dst io.Writer) error
|
||||
)
|
||||
if pt, ok := t.(PathThumbnailer); ok {
|
||||
digest = pathDigest(srcFS, srcInfo)
|
||||
write = func(dst io.Writer) error { return pt.GenerateFromPath(srcFS, dst, width) }
|
||||
} else {
|
||||
d, data, err := sourceDigest(srcFS, srcInfo)
|
||||
if err != nil {
|
||||
log.Printf("thumb digest %s: %v", rel, err)
|
||||
http.Error(w, "thumbnail failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
digest = d
|
||||
write = func(dst io.Writer) error {
|
||||
var src io.Reader
|
||||
if data != nil {
|
||||
src = bytes.NewReader(data)
|
||||
} else {
|
||||
f, err := os.Open(srcFS)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
src = f
|
||||
}
|
||||
return t.Generate(src, dst, width)
|
||||
}
|
||||
}
|
||||
|
||||
cacheFS := filepath.Join(thumbCacheDir, digest[:2], fmt.Sprintf("%s.%d.jpg", digest, width))
|
||||
@@ -138,21 +174,7 @@ func (h *handler) handleThumb(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
var src io.Reader
|
||||
if data != nil {
|
||||
src = bytes.NewReader(data)
|
||||
} else {
|
||||
f, err := os.Open(srcFS)
|
||||
if err != nil {
|
||||
log.Printf("thumb open %s: %v", rel, err)
|
||||
http.Error(w, "thumbnail failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
src = f
|
||||
}
|
||||
|
||||
if err := generateThumb(t, src, cacheFS, width); err != nil {
|
||||
if err := generateThumb(cacheFS, write); err != nil {
|
||||
log.Printf("thumb %s: %v", rel, err)
|
||||
http.Error(w, "thumbnail failed", http.StatusInternalServerError)
|
||||
return
|
||||
@@ -160,6 +182,14 @@ func (h *handler) handleThumb(w http.ResponseWriter, r *http.Request) {
|
||||
serveThumb(w, r, cacheFS)
|
||||
}
|
||||
|
||||
// pathDigest keys the cache for path-based thumbnailers on the source path plus
|
||||
// its mtime and size, so the (potentially large) file is never read into memory
|
||||
// just to hash it. Overwriting the file changes mtime/size and busts the entry.
|
||||
func pathDigest(srcFS string, info os.FileInfo) string {
|
||||
sum := sha256.Sum256(fmt.Appendf(nil, "%s\x00%d\x00%d", srcFS, info.ModTime().UnixNano(), info.Size()))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
// sourceDigest returns the SHA-256 hex digest of a source file's content.
|
||||
// On a cache hit (path + mtime + size unchanged) the returned data is nil,
|
||||
// so the caller knows to open the file itself. On a miss the file is read
|
||||
@@ -191,7 +221,10 @@ func serveThumb(w http.ResponseWriter, r *http.Request, cacheFS string) {
|
||||
http.ServeFile(w, r, cacheFS)
|
||||
}
|
||||
|
||||
func generateThumb(t Thumbnailer, src io.Reader, cacheFS string, width int) error {
|
||||
// generateThumb writes a thumbnail to cacheFS atomically: write builds the
|
||||
// image into a temp file in the same dir, which is renamed into place only on
|
||||
// success so readers never see a partial file.
|
||||
func generateThumb(cacheFS string, write func(dst io.Writer) error) error {
|
||||
if err := os.MkdirAll(filepath.Dir(cacheFS), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -200,7 +233,7 @@ func generateThumb(t Thumbnailer, src io.Reader, cacheFS string, width int) erro
|
||||
return err
|
||||
}
|
||||
tmpName := tmp.Name()
|
||||
if err := t.Generate(src, tmp, width); err != nil {
|
||||
if err := write(tmp); err != nil {
|
||||
tmp.Close()
|
||||
os.Remove(tmpName)
|
||||
return err
|
||||
|
||||
Reference in New Issue
Block a user