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/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"). 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) 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 }