From b4054e26b188c8996ee285eda28cb2ad4456972b Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Sun, 2 Apr 2023 13:04:30 -0600 Subject: [PATCH] Re-add thumbnailing --- Dockerfile | 6 +-- cmd/webfs/main.go | 94 +++++++++++++++++++++++++++++++++++++++++++-- docker-compose.yaml | 6 +++ go.mod | 5 ++- go.sum | 2 + 5 files changed, 105 insertions(+), 8 deletions(-) create mode 100644 docker-compose.yaml diff --git a/Dockerfile b/Dockerfile index 7470f5b..da96329 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,16 +2,16 @@ FROM golang:1-alpine AS build WORKDIR /src COPY go.* ./ RUN go mod download -x -COPY pkg ./pkg/ COPY cmd ./cmd/ -RUN CGO_ENABLED=0 GOOS=linux go install ./... +RUN go install ./... FROM alpine AS runtime WORKDIR /target COPY web web COPY --from=build /go/bin/ . -FROM scratch +FROM alpine +RUN apk --no-cache add ffmpeg ffprobe COPY --from=runtime /target / WORKDIR /web ENTRYPOINT ["/webfs"] diff --git a/cmd/webfs/main.go b/cmd/webfs/main.go index be76dde..e83d9a5 100644 --- a/cmd/webfs/main.go +++ b/cmd/webfs/main.go @@ -1,16 +1,25 @@ package main import ( + "context" "flag" + "fmt" "log" "net/http" + "os" + "os/exec" + "path" "strings" + ffprobe "gopkg.in/vansante/go-ffprobe.v2" + "golang.org/x/net/webdav" ) type Handler struct { webdav.Handler + dataRoot string + thumbnailRoot string } func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -19,18 +28,95 @@ 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") != "" { + h.ServeThumbnail(w, r) + return + } } h.Handler.ServeHTTP(w, r) } +/* cleanPath returns a sanitized HTTP path. + * + * This removes .. from everything, + * and will never return any path above / + */ +func cleanPath(p string) string { + if p == "" { + return "/" + } + if p[0] != '/' { + p = "/" + p + } + return path.Clean(p) +} + +func (h *Handler) ServeThumbnail(w http.ResponseWriter, req *http.Request) { + reqPath := cleanPath(req.URL.Path) + reqPath = strings.TrimLeft(reqPath, "/") + thumbnailPath := path.Join(h.thumbnailRoot, reqPath) + ".webp" + + // 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 { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } + + http.ServeFile(w, req, thumbnailPath) +} + func main() { address := flag.String("listen", ":8080", "Address to listen to") - directory := flag.String("root", ".", "Directory to serve") + dataRoot := flag.String("root", ".", "Directory to serve") + thumbnailRoot := flag.String("thumbnails", "/thumbnails", "Where to store thumbnails") flag.Parse() - handler := &Handler{} - handler.FileSystem = webdav.Dir(*directory) - handler.LockSystem = webdav.NewMemLS() + handler := &Handler{ + Handler: webdav.Handler{ + FileSystem: webdav.Dir(*dataRoot), + LockSystem: webdav.NewMemLS(), + }, + dataRoot: *dataRoot, + thumbnailRoot: *thumbnailRoot, + } log.Println("Listening on", *address) http.ListenAndServe(*address, handler) diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..d0fa6c0 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,6 @@ +services: + webfs: + image: git.woozle.org/neale/webfs + build: . + ports: + - 8080:8080 diff --git a/go.mod b/go.mod index 5034048..8ec670f 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module woozle.org/neale/webfs go 1.15 -require golang.org/x/net v0.7.0 +require ( + golang.org/x/net v0.7.0 + gopkg.in/vansante/go-ffprobe.v2 v2.1.1 +) diff --git a/go.sum b/go.sum index 4b6f231..beba3ed 100644 --- a/go.sum +++ b/go.sum @@ -26,3 +26,5 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/vansante/go-ffprobe.v2 v2.1.1 h1:DIh5fMn+tlBvG7pXyUZdemVmLdERnf2xX6XOFF+0BBU= +gopkg.in/vansante/go-ffprobe.v2 v2.1.1/go.mod h1:qF0AlAjk7Nqzqf3y333Ly+KxN3cKF2JqA3JT5ZheUGE=