Update README

This commit is contained in:
Neale Pickett 2023-04-02 13:36:50 -06:00
parent b4054e26b1
commit bf3bd35fdb
2 changed files with 83 additions and 42 deletions

View File

@ -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.

View File

@ -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))
}