181 lines
4.6 KiB
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(¶ms, 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
|
|
}
|