From bf3bd35fdb121476b69ad11a29a2f6524f3e2050 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Sun, 2 Apr 2023 13:36:50 -0600 Subject: [PATCH] Update README --- README.md | 23 +++++++++-- cmd/webfs/main.go | 102 ++++++++++++++++++++++++++++------------------ 2 files changed, 83 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 424429a..51f548d 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,25 @@ # webfs -It's a WebDAV server. - -That's all it does. It serves WebDAV. +It's a WebDAV server that generates and caches media thumbnails. I front-end it with Caddy, which handles authentication and access controls. +Because the thumbnails live at the same path as the original media, +so I don't need any special gubbins to protect thumbnails the same way. + + +## Thumbnails + +If you put `?thumbnail` at the end of the URL, +it will use `ffmpeg` to generate a WebP thumbnail that fits in a 320x200 box. + +I use WebP because it provides alpha channels, animations, +and good compression. +If your browser doesn't have WebP support, +that's a real pity, +and this isn't a good software solution for you. + +Thumbnails are cached. +No attempt is made to verify whether thumbnails are up-to-date, +or to clean up the thumbnail directory. +Something else is going to have to manage that. diff --git a/cmd/webfs/main.go b/cmd/webfs/main.go index e83d9a5..dd47751 100644 --- a/cmd/webfs/main.go +++ b/cmd/webfs/main.go @@ -20,6 +20,7 @@ type Handler struct { webdav.Handler dataRoot string thumbnailRoot string + filterVideo string } func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -28,7 +29,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if strings.HasSuffix(r.URL.Path, "/") { r.URL.Path += "index.html" } - if r.FormValue("thumbnail") != "" { + r.ParseForm() + if _, ok := r.Form["thumbnail"]; (h.thumbnailRoot != "") && ok { h.ServeThumbnail(w, r) return } @@ -51,6 +53,53 @@ func cleanPath(p string) string { return path.Clean(p) } +func (h *Handler) makeThumbnail(reqPath, thumbnailPath string) error { + srcPath := path.Join(h.dataRoot, reqPath) + + // Run ffprobe to figure out what kind of file this is + ffdata, err := ffprobe.ProbeURL(context.Background(), srcPath, "-hide_banner") + if err != nil { + return err + } + isVideo := ffdata.Format.DurationSeconds > 0.0 + + // Build up ffmpeg invocation + // XXX: some day soon you will want CommandContext + cmd := exec.Command("ffmpeg", "-hide_banner", "-loglevel", "error") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if isVideo { + skipSeconds := ffdata.Format.StartTimeSeconds + (ffdata.Format.DurationSeconds * 0.25) + cmd.Args = append(cmd.Args, + "-ss", fmt.Sprintf("%f", skipSeconds), + "-i", srcPath, + "-frames:v", "5", + "-filter:v", h.filterVideo+",fps=2", + "-loop", "0", + thumbnailPath, + ) + } else { + cmd.Args = append(cmd.Args, + "-i", srcPath, + "-filter:v", h.filterVideo, + thumbnailPath, + ) + } + + // Make sure the thumbnail has a directory to live in + if err := os.MkdirAll(path.Dir(thumbnailPath), 0755); err != nil { + return err + } + + // Convert! + if err := cmd.Run(); err != nil { + return err + } + + return nil +} + func (h *Handler) ServeThumbnail(w http.ResponseWriter, req *http.Request) { reqPath := cleanPath(req.URL.Path) reqPath = strings.TrimLeft(reqPath, "/") @@ -58,43 +107,7 @@ func (h *Handler) ServeThumbnail(w http.ResponseWriter, req *http.Request) { // If there's not already a thumbnail, make one if _, err := os.Stat(thumbnailPath); err != nil { - srcPath := path.Join(h.dataRoot, reqPath) - - ffdata, err := ffprobe.ProbeURL(context.Background(), srcPath, "-hide_banner") - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - isVideo := ffdata.Format.DurationSeconds > 0.0 - - // XXX: some day soon you will want CommandContext - cmd := exec.Command("ffmpeg", "-hide_banner", "-loglevel", "error") - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - if isVideo { - skipSeconds := ffdata.Format.StartTimeSeconds + (ffdata.Format.DurationSeconds * 0.25) - cmd.Args = append(cmd.Args, - "-ss", fmt.Sprintf("%f", skipSeconds), - "-i", srcPath, - "-frames:v", "5", - "-filter:v", "scale='min(320,iw)':'min(200,ih)':force_original_aspect_ratio=decrease,fps=2", - "-loop", "0", - thumbnailPath, - ) - } else { - cmd.Args = append(cmd.Args, - "-i", srcPath, - "-filter:v", "scale='min(320,iw)':'min(200,ih)':force_original_aspect_ratio=decrease", - thumbnailPath, - ) - } - - if err := os.MkdirAll(path.Dir(thumbnailPath), 0755); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - if err := cmd.Run(); err != nil { + if err := h.makeThumbnail(reqPath, thumbnailPath); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -107,8 +120,18 @@ func main() { address := flag.String("listen", ":8080", "Address to listen to") dataRoot := flag.String("root", ".", "Directory to serve") thumbnailRoot := flag.String("thumbnails", "/thumbnails", "Where to store thumbnails") + filterVideo := flag.String( + "filter:v", + "scale='min(320,iw)':'min(200,ih)':force_original_aspect_ratio=decrease", + "ffmpeg video filter", + ) flag.Parse() + if _, err := os.Stat(*thumbnailRoot); err != nil { + log.Println("Disabling thumbnail generation:", err) + *thumbnailRoot = "" + } + handler := &Handler{ Handler: webdav.Handler{ FileSystem: webdav.Dir(*dataRoot), @@ -116,8 +139,9 @@ func main() { }, dataRoot: *dataRoot, thumbnailRoot: *thumbnailRoot, + filterVideo: *filterVideo, } log.Println("Listening on", *address) - http.ListenAndServe(*address, handler) + log.Fatal(http.ListenAndServe(*address, handler)) }