clipper/backend/server/handler.go

181 lines
4.6 KiB
Go

package server
import (
"context"
"io"
"net/http"
"path/filepath"
"git.netflux.io/rob/clipper/config"
"git.netflux.io/rob/clipper/filestore"
"git.netflux.io/rob/clipper/media"
"github.com/google/uuid"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/gorilla/schema"
"github.com/improbable-eng/grpc-web/go/grpcweb"
"go.uber.org/zap"
)
type httpHandler struct {
*mux.Router
grpcHandler *grpcweb.WrappedGrpcServer
mediaSetService MediaSetService
logger *zap.SugaredLogger
}
func newHTTPHandler(grpcHandler *grpcweb.WrappedGrpcServer, mediaSetService MediaSetService, c config.Config, logger *zap.SugaredLogger) *httpHandler {
fileStoreHandler := http.NotFoundHandler()
if c.FileStoreHTTPRoot != "" {
logger.With("root", c.FileStoreHTTPRoot, "baseURL", c.FileStoreHTTPBaseURL.String()).Info("Configured to serve file store over HTTP")
fileStoreHandler = http.FileServer(&indexedFileSystem{http.Dir(c.FileStoreHTTPRoot)})
}
assetsHandler := http.NotFoundHandler()
if c.AssetsHTTPRoot != "" {
logger.With("root", c.AssetsHTTPRoot).Info("Configured to serve assets over HTTP")
assetsHandler = http.FileServer(&indexedFileSystem{http.Dir(c.AssetsHTTPRoot)})
}
// If FileSystemStore AND assets serving are both enabled,
// FileStoreHTTPBaseURL *must* be set to a value other than "/" to avoid
// clobbering the assets routes.
h := &httpHandler{
Router: mux.NewRouter(),
grpcHandler: grpcHandler,
mediaSetService: mediaSetService,
logger: logger,
}
h.
Methods("POST").
Path("/api/media_sets/{id}/clip").
Handler(http.HandlerFunc(h.handleClip))
if c.FileStore == config.FileSystemStore {
h.
Methods("GET").
PathPrefix(c.FileStoreHTTPBaseURL.Path).
Handler(filestore.NewFileSystemStoreHTTPMiddleware(c.FileStoreHTTPBaseURL, fileStoreHandler))
}
h.
Methods("GET").
Handler(assetsHandler)
h.Use(handlers.CORS(handlers.AllowedOrigins(c.CORSAllowedOrigins)))
return h
}
func (h *httpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if !h.grpcHandler.IsGrpcWebRequest(r) && !h.grpcHandler.IsAcceptableGrpcCorsRequest(r) {
h.Router.ServeHTTP(w, r)
return
}
h.grpcHandler.ServeHTTP(w, r)
}
func (h *httpHandler) handleClip(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(context.Background(), getPeaksForSegmentTimeout)
defer cancel()
if err := r.ParseForm(); err != nil {
h.logger.With("err", err).Info("error parsing form")
w.WriteHeader(http.StatusBadRequest)
return
}
var params struct {
StartFrame int64 `schema:"start_frame,required"`
EndFrame int64 `schema:"end_frame,required"`
Format string `schema:"format,required"`
}
decoder := schema.NewDecoder()
if err := decoder.Decode(&params, r.PostForm); err != nil {
h.logger.With("err", err).Info("error decoding form")
w.WriteHeader(http.StatusBadRequest)
return
}
vars := mux.Vars(r)
id, err := uuid.Parse(vars["id"])
if err != nil {
h.logger.With("err", err).Info("error parsing ID")
w.WriteHeader(http.StatusNotFound)
return
}
var format media.AudioFormat
switch params.Format {
case "mp3":
format = media.AudioFormatMP3
case "wav":
format = media.AudioFormatWAV
default:
h.logger.Info("bad format")
w.WriteHeader(http.StatusBadRequest)
return
}
stream, err := h.mediaSetService.GetAudioSegment(ctx, id, params.StartFrame, params.EndFrame, format)
if err != nil {
h.logger.With("err", err).Info("error getting audio segment")
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set("content-type", "audio/"+format.String())
w.Header().Set("content-disposition", "attachment; filename=clip."+format.String())
w.WriteHeader(http.StatusOK)
var closing bool
for {
progress, err := stream.Next(ctx)
if err == io.EOF {
closing = true
} else if err != nil {
h.logger.With("err", err).Error("error reading audio segment stream")
return
}
w.Write(progress.Data)
if closing {
break
}
}
}
// indexedFileSystem is an HTTP file system which handles index.html files if
// they exist, but does not serve directory listings.
//
// Ref: https://www.alexedwards.net/blog/disable-http-fileserver-directory-listings
type indexedFileSystem struct {
httpFS http.FileSystem
}
func (ifs *indexedFileSystem) Open(path string) (http.File, error) {
f, err := ifs.httpFS.Open(path)
if err != nil {
return nil, err
}
s, err := f.Stat()
if err != nil {
return nil, err
}
if !s.IsDir() {
return f, nil
}
index := filepath.Join(path, "index.html")
if _, err := ifs.httpFS.Open(index); err != nil {
_ = f.Close() // ignore error
return nil, err
}
return f, nil
}