From 83fcf7dc1f96fc7343132c088146b5367ca81244 Mon Sep 17 00:00:00 2001 From: Robert Swain Date: Sat, 3 Jun 2017 11:08:02 +0200 Subject: [PATCH] mediaapi: Add thumbnail support --- .../dendrite/mediaapi/routing/routing.go | 36 ++- .../dendrite/mediaapi/storage/sql.go | 8 +- .../dendrite/mediaapi/storage/storage.go | 32 ++- .../dendrite/mediaapi/types/types.go | 22 ++ .../dendrite/mediaapi/writers/download.go | 217 ++++++++++++++++-- .../dendrite/mediaapi/writers/upload.go | 20 +- 6 files changed, 287 insertions(+), 48 deletions(-) diff --git a/src/github.com/matrix-org/dendrite/mediaapi/routing/routing.go b/src/github.com/matrix-org/dendrite/mediaapi/routing/routing.go index 7641109c3..47985fc16 100644 --- a/src/github.com/matrix-org/dendrite/mediaapi/routing/routing.go +++ b/src/github.com/matrix-org/dendrite/mediaapi/routing/routing.go @@ -35,28 +35,40 @@ const pathPrefixR0 = "/_matrix/media/v1" func Setup(servMux *http.ServeMux, httpClient *http.Client, cfg *config.MediaAPI, db *storage.Database) { apiMux := mux.NewRouter() r0mux := apiMux.PathPrefix(pathPrefixR0).Subrouter() + + activeThumbnailGeneration := &types.ActiveThumbnailGeneration{ + PathToResult: map[string]*types.ThumbnailGenerationResult{}, + } + // FIXME: /upload should use common.MakeAuthAPI() r0mux.Handle("/upload", common.MakeAPI("upload", func(req *http.Request) util.JSONResponse { - return writers.Upload(req, cfg, db) + return writers.Upload(req, cfg, db, activeThumbnailGeneration) })) activeRemoteRequests := &types.ActiveRemoteRequests{ MXCToResult: map[string]*types.RemoteRequestResult{}, } r0mux.Handle("/download/{serverName}/{mediaId}", - prometheus.InstrumentHandler("download", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - req = util.RequestWithLogging(req) - - // Set common headers returned regardless of the outcome of the request - util.SetCORSHeaders(w) - // Content-Type will be overridden in case of returning file data, else we respond with JSON-formatted errors - w.Header().Set("Content-Type", "application/json") - - vars := mux.Vars(req) - writers.Download(w, req, gomatrixserverlib.ServerName(vars["serverName"]), types.MediaID(vars["mediaId"]), cfg, db, activeRemoteRequests) - })), + makeDownloadAPI("download", cfg, db, activeRemoteRequests, activeThumbnailGeneration), + ) + r0mux.Handle("/thumbnail/{serverName}/{mediaId}", + makeDownloadAPI("thumbnail", cfg, db, activeRemoteRequests, activeThumbnailGeneration), ) servMux.Handle("/metrics", prometheus.Handler()) servMux.Handle("/api/", http.StripPrefix("/api", apiMux)) } + +func makeDownloadAPI(name string, cfg *config.MediaAPI, db *storage.Database, activeRemoteRequests *types.ActiveRemoteRequests, activeThumbnailGeneration *types.ActiveThumbnailGeneration) http.HandlerFunc { + return prometheus.InstrumentHandler(name, http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + req = util.RequestWithLogging(req) + + // Set common headers returned regardless of the outcome of the request + util.SetCORSHeaders(w) + // Content-Type will be overridden in case of returning file data, else we respond with JSON-formatted errors + w.Header().Set("Content-Type", "application/json") + + vars := mux.Vars(req) + writers.Download(w, req, gomatrixserverlib.ServerName(vars["serverName"]), types.MediaID(vars["mediaId"]), cfg, db, activeRemoteRequests, activeThumbnailGeneration, name == "thumbnail") + })) +} diff --git a/src/github.com/matrix-org/dendrite/mediaapi/storage/sql.go b/src/github.com/matrix-org/dendrite/mediaapi/storage/sql.go index e992e073e..024ab8bb8 100644 --- a/src/github.com/matrix-org/dendrite/mediaapi/storage/sql.go +++ b/src/github.com/matrix-org/dendrite/mediaapi/storage/sql.go @@ -19,13 +19,17 @@ import ( ) type statements struct { - mediaStatements + media mediaStatements + thumbnail thumbnailStatements } func (s *statements) prepare(db *sql.DB) error { var err error - if err = s.mediaStatements.prepare(db); err != nil { + if err = s.media.prepare(db); err != nil { + return err + } + if err = s.thumbnail.prepare(db); err != nil { return err } diff --git a/src/github.com/matrix-org/dendrite/mediaapi/storage/storage.go b/src/github.com/matrix-org/dendrite/mediaapi/storage/storage.go index cb27ccc95..933583828 100644 --- a/src/github.com/matrix-org/dendrite/mediaapi/storage/storage.go +++ b/src/github.com/matrix-org/dendrite/mediaapi/storage/storage.go @@ -45,16 +45,44 @@ func Open(dataSourceName string) (*Database, error) { // StoreMediaMetadata inserts the metadata about the uploaded media into the database. // Returns an error if the combination of MediaID and Origin are not unique in the table. func (d *Database) StoreMediaMetadata(mediaMetadata *types.MediaMetadata) error { - return d.statements.insertMedia(mediaMetadata) + return d.statements.media.insertMedia(mediaMetadata) } // GetMediaMetadata returns metadata about media stored on this server. // The media could have been uploaded to this server or fetched from another server and cached here. // Returns nil metadata if there is no metadata associated with this media. func (d *Database) GetMediaMetadata(mediaID types.MediaID, mediaOrigin gomatrixserverlib.ServerName) (*types.MediaMetadata, error) { - mediaMetadata, err := d.statements.selectMedia(mediaID, mediaOrigin) + mediaMetadata, err := d.statements.media.selectMedia(mediaID, mediaOrigin) if err != nil && err == sql.ErrNoRows { return nil, nil } return mediaMetadata, err } + +// StoreThumbnail inserts the metadata about the thumbnail into the database. +// Returns an error if the combination of MediaID and Origin are not unique in the table. +func (d *Database) StoreThumbnail(thumbnailMetadata *types.ThumbnailMetadata) error { + return d.statements.thumbnail.insertThumbnail(thumbnailMetadata) +} + +// GetThumbnail returns metadata about a specific thumbnail. +// The media could have been uploaded to this server or fetched from another server and cached here. +// Returns nil metadata if there is no metadata associated with this thumbnail. +func (d *Database) GetThumbnail(mediaID types.MediaID, mediaOrigin gomatrixserverlib.ServerName, width, height int, resizeMethod string) (*types.ThumbnailMetadata, error) { + thumbnailMetadata, err := d.statements.thumbnail.selectThumbnail(mediaID, mediaOrigin, width, height, resizeMethod) + if err != nil && err == sql.ErrNoRows { + return nil, nil + } + return thumbnailMetadata, err +} + +// GetThumbnails returns metadata about all thumbnails for a specific media stored on this server. +// The media could have been uploaded to this server or fetched from another server and cached here. +// Returns nil metadata if there are no thumbnails associated with this media. +func (d *Database) GetThumbnails(mediaID types.MediaID, mediaOrigin gomatrixserverlib.ServerName) ([]*types.ThumbnailMetadata, error) { + thumbnails, err := d.statements.thumbnail.selectThumbnails(mediaID, mediaOrigin) + if err != nil && err == sql.ErrNoRows { + return nil, nil + } + return thumbnails, err +} diff --git a/src/github.com/matrix-org/dendrite/mediaapi/types/types.go b/src/github.com/matrix-org/dendrite/mediaapi/types/types.go index 0729c25dc..3a0888f88 100644 --- a/src/github.com/matrix-org/dendrite/mediaapi/types/types.go +++ b/src/github.com/matrix-org/dendrite/mediaapi/types/types.go @@ -89,3 +89,25 @@ type ThumbnailSize struct { // scale scales to fit the requested dimensions and one dimension may be smaller than requested. ResizeMethod string `yaml:"method,omitempty"` } + +// ThumbnailMetadata contains the metadata about an individual thumbnail +type ThumbnailMetadata struct { + MediaMetadata *MediaMetadata + ThumbnailSize ThumbnailSize +} + +// ThumbnailGenerationResult is used for broadcasting the result of thumbnail generation to routines waiting on the condition +type ThumbnailGenerationResult struct { + // Condition used for the generator to signal the result to all other routines waiting on this condition + Cond *sync.Cond + // Resulting error from the generation attempt + Err error +} + +// ActiveThumbnailGeneration is a lockable map of file paths being thumbnailed +// It is used to ensure thumbnails are only generated once. +type ActiveThumbnailGeneration struct { + sync.Mutex + // The string key is a thumbnail file path + PathToResult map[string]*ThumbnailGenerationResult +} diff --git a/src/github.com/matrix-org/dendrite/mediaapi/writers/download.go b/src/github.com/matrix-org/dendrite/mediaapi/writers/download.go index 32f4c9c4e..1d61b536a 100644 --- a/src/github.com/matrix-org/dendrite/mediaapi/writers/download.go +++ b/src/github.com/matrix-org/dendrite/mediaapi/writers/download.go @@ -24,6 +24,7 @@ import ( "path/filepath" "regexp" "strconv" + "strings" "sync" log "github.com/Sirupsen/logrus" @@ -31,6 +32,7 @@ import ( "github.com/matrix-org/dendrite/mediaapi/config" "github.com/matrix-org/dendrite/mediaapi/fileutils" "github.com/matrix-org/dendrite/mediaapi/storage" + "github.com/matrix-org/dendrite/mediaapi/thumbnailer" "github.com/matrix-org/dendrite/mediaapi/types" "github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/util" @@ -41,31 +43,51 @@ const mediaIDCharacters = "A-Za-z0-9_=-" // Note: unfortunately regex.MustCompile() cannot be assigned to a const var mediaIDRegex = regexp.MustCompile("[" + mediaIDCharacters + "]+") -// downloadRequest metadata included in or derivable from an download request +// downloadRequest metadata included in or derivable from a download or thumbnail request // https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-media-r0-download-servername-mediaid +// http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-media-r0-thumbnail-servername-mediaid type downloadRequest struct { - MediaMetadata *types.MediaMetadata - Logger *log.Entry + MediaMetadata *types.MediaMetadata + IsThumbnailRequest bool + ThumbnailSize types.ThumbnailSize + Logger *log.Entry } -// Download implements /download +// Download implements /download amd /thumbnail // Files from this server (i.e. origin == cfg.ServerName) are served directly // Files from remote servers (i.e. origin != cfg.ServerName) are cached locally. // If they are present in the cache, they are served directly. // If they are not present in the cache, they are obtained from the remote server and // simultaneously served back to the client and written into the cache. -func Download(w http.ResponseWriter, req *http.Request, origin gomatrixserverlib.ServerName, mediaID types.MediaID, cfg *config.MediaAPI, db *storage.Database, activeRemoteRequests *types.ActiveRemoteRequests) { +func Download(w http.ResponseWriter, req *http.Request, origin gomatrixserverlib.ServerName, mediaID types.MediaID, cfg *config.MediaAPI, db *storage.Database, activeRemoteRequests *types.ActiveRemoteRequests, activeThumbnailGeneration *types.ActiveThumbnailGeneration, isThumbnailRequest bool) { r := &downloadRequest{ MediaMetadata: &types.MediaMetadata{ MediaID: mediaID, Origin: origin, }, + IsThumbnailRequest: isThumbnailRequest, Logger: util.GetLogger(req.Context()).WithFields(log.Fields{ "Origin": origin, "MediaID": mediaID, }), } + if r.IsThumbnailRequest { + width, err := strconv.Atoi(req.FormValue("width")) + if err != nil { + width = -1 + } + height, err := strconv.Atoi(req.FormValue("height")) + if err != nil { + height = -1 + } + r.ThumbnailSize = types.ThumbnailSize{ + Width: width, + Height: height, + ResizeMethod: strings.ToLower(req.FormValue("method")), + } + } + // request validation if req.Method != "GET" { r.jsonErrorResponse(w, util.JSONResponse{ @@ -80,7 +102,7 @@ func Download(w http.ResponseWriter, req *http.Request, origin gomatrixserverlib return } - if resErr := r.doDownload(w, cfg, db, activeRemoteRequests); resErr != nil { + if resErr := r.doDownload(w, cfg, db, activeRemoteRequests, activeThumbnailGeneration); resErr != nil { r.jsonErrorResponse(w, *resErr) return } @@ -118,10 +140,29 @@ func (r *downloadRequest) Validate() *util.JSONResponse { JSON: jsonerror.NotFound("serverName must be a non-empty string"), } } + + if r.IsThumbnailRequest { + if r.ThumbnailSize.Width <= 0 || r.ThumbnailSize.Height <= 0 { + return &util.JSONResponse{ + Code: 400, + JSON: jsonerror.Unknown("width and height must be greater than 0"), + } + } + // Default method to scale if not set + if r.ThumbnailSize.ResizeMethod == "" { + r.ThumbnailSize.ResizeMethod = "scale" + } + if r.ThumbnailSize.ResizeMethod != "crop" && r.ThumbnailSize.ResizeMethod != "scale" { + return &util.JSONResponse{ + Code: 400, + JSON: jsonerror.Unknown("method must be one of crop or scale"), + } + } + } return nil } -func (r *downloadRequest) doDownload(w http.ResponseWriter, cfg *config.MediaAPI, db *storage.Database, activeRemoteRequests *types.ActiveRemoteRequests) *util.JSONResponse { +func (r *downloadRequest) doDownload(w http.ResponseWriter, cfg *config.MediaAPI, db *storage.Database, activeRemoteRequests *types.ActiveRemoteRequests, activeThumbnailGeneration *types.ActiveThumbnailGeneration) *util.JSONResponse { // check if we have a record of the media in our database mediaMetadata, err := db.GetMediaMetadata(r.MediaMetadata.MediaID, r.MediaMetadata.Origin) if err != nil { @@ -138,7 +179,7 @@ func (r *downloadRequest) doDownload(w http.ResponseWriter, cfg *config.MediaAPI } } // If we do not have a record and the origin is remote, we need to fetch it and respond with that file - resErr := r.getRemoteFile(cfg, db, activeRemoteRequests) + resErr := r.getRemoteFile(cfg, db, activeRemoteRequests, activeThumbnailGeneration) if resErr != nil { return resErr } @@ -146,12 +187,12 @@ func (r *downloadRequest) doDownload(w http.ResponseWriter, cfg *config.MediaAPI // If we have a record, we can respond from the local file r.MediaMetadata = mediaMetadata } - return r.respondFromLocalFile(w, cfg.AbsBasePath) + return r.respondFromLocalFile(w, cfg.AbsBasePath, activeThumbnailGeneration, db, cfg.DynamicThumbnails, cfg.ThumbnailSizes) } // respondFromLocalFile reads a file from local storage and writes it to the http.ResponseWriter // Returns a util.JSONResponse error in case of error -func (r *downloadRequest) respondFromLocalFile(w http.ResponseWriter, absBasePath types.Path) *util.JSONResponse { +func (r *downloadRequest) respondFromLocalFile(w http.ResponseWriter, absBasePath types.Path, activeThumbnailGeneration *types.ActiveThumbnailGeneration, db *storage.Database, dynamicThumbnails bool, thumbnailSizes []types.ThumbnailSize) *util.JSONResponse { filePath, err := fileutils.GetPathFromBase64Hash(r.MediaMetadata.Base64Hash, absBasePath) if err != nil { r.Logger.WithError(err).Error("Failed to get file path from metadata") @@ -181,15 +222,49 @@ func (r *downloadRequest) respondFromLocalFile(w http.ResponseWriter, absBasePat return &resErr } - r.Logger.WithFields(log.Fields{ - "UploadName": r.MediaMetadata.UploadName, - "Base64Hash": r.MediaMetadata.Base64Hash, - "FileSizeBytes": r.MediaMetadata.FileSizeBytes, - "Content-Type": r.MediaMetadata.ContentType, - }).Info("Responding with file") + var responseFile *os.File + var responseMetadata *types.MediaMetadata + if r.IsThumbnailRequest { + thumbFile, thumbMetadata, resErr := r.getThumbnailFile(types.Path(filePath), activeThumbnailGeneration, db, dynamicThumbnails, thumbnailSizes) + if thumbFile != nil { + defer thumbFile.Close() + } + if resErr != nil { + return resErr + } + if thumbFile == nil { + r.Logger.WithFields(log.Fields{ + "UploadName": r.MediaMetadata.UploadName, + "Base64Hash": r.MediaMetadata.Base64Hash, + "FileSizeBytes": r.MediaMetadata.FileSizeBytes, + "ContentType": r.MediaMetadata.ContentType, + }).Info("No good thumbnail found. Responding with original file.") + responseFile = file + responseMetadata = r.MediaMetadata + } else { + r.Logger.WithFields(log.Fields{ + "Width": thumbMetadata.ThumbnailSize.Width, + "Height": thumbMetadata.ThumbnailSize.Height, + "ResizeMethod": thumbMetadata.ThumbnailSize.ResizeMethod, + "FileSizeBytes": thumbMetadata.MediaMetadata.FileSizeBytes, + "ContentType": thumbMetadata.MediaMetadata.ContentType, + }).Info("Responding with thumbnail") + responseFile = thumbFile + responseMetadata = thumbMetadata.MediaMetadata + } + } else { + r.Logger.WithFields(log.Fields{ + "UploadName": r.MediaMetadata.UploadName, + "Base64Hash": r.MediaMetadata.Base64Hash, + "FileSizeBytes": r.MediaMetadata.FileSizeBytes, + "ContentType": r.MediaMetadata.ContentType, + }).Info("Responding with file") + responseFile = file + responseMetadata = r.MediaMetadata + } - w.Header().Set("Content-Type", string(r.MediaMetadata.ContentType)) - w.Header().Set("Content-Length", strconv.FormatInt(int64(r.MediaMetadata.FileSizeBytes), 10)) + w.Header().Set("Content-Type", string(responseMetadata.ContentType)) + w.Header().Set("Content-Length", strconv.FormatInt(int64(responseMetadata.FileSizeBytes), 10)) contentSecurityPolicy := "default-src 'none';" + " script-src 'none';" + " plugin-types application/pdf;" + @@ -197,7 +272,7 @@ func (r *downloadRequest) respondFromLocalFile(w http.ResponseWriter, absBasePat " object-src 'self';" w.Header().Set("Content-Security-Policy", contentSecurityPolicy) - if bytesResponded, err := io.Copy(w, file); err != nil { + if bytesResponded, err := io.Copy(w, responseFile); err != nil { r.Logger.WithError(err).Warn("Failed to copy from cache") if bytesResponded == 0 { resErr := jsonerror.InternalServerError() @@ -209,12 +284,99 @@ func (r *downloadRequest) respondFromLocalFile(w http.ResponseWriter, absBasePat return nil } +// Note: Thumbnail generation may be ongoing asynchronously. +func (r *downloadRequest) getThumbnailFile(filePath types.Path, activeThumbnailGeneration *types.ActiveThumbnailGeneration, db *storage.Database, dynamicThumbnails bool, thumbnailSizes []types.ThumbnailSize) (*os.File, *types.ThumbnailMetadata, *util.JSONResponse) { + var thumbnail *types.ThumbnailMetadata + var resErr *util.JSONResponse + + if dynamicThumbnails { + thumbnail, resErr = r.generateThumbnail(filePath, r.ThumbnailSize, activeThumbnailGeneration, db) + if resErr != nil { + return nil, nil, resErr + } + } else { + thumbnails, err := db.GetThumbnails(r.MediaMetadata.MediaID, r.MediaMetadata.Origin) + if err != nil { + r.Logger.WithError(err).WithFields(log.Fields{ + "Width": r.ThumbnailSize.Width, + "Height": r.ThumbnailSize.Height, + "ResizeMethod": r.ThumbnailSize.ResizeMethod, + }).Error("Error looking up thumbnails") + resErr := jsonerror.InternalServerError() + return nil, nil, &resErr + } + + // If we get a thumbnailSize, a pre-generated thumbnail would be best but it is not yet generated. + // If we get a thumbnail, we're done. + var thumbnailSize *types.ThumbnailSize + thumbnail, thumbnailSize = thumbnailer.SelectThumbnail(r.ThumbnailSize, thumbnails, thumbnailSizes) + if thumbnailSize != nil { + r.Logger.WithFields(log.Fields{ + "Width": thumbnailSize.Width, + "Height": thumbnailSize.Height, + "ResizeMethod": thumbnailSize.ResizeMethod, + }).Info("Pre-generating thumbnail for immediate response.") + thumbnail, resErr = r.generateThumbnail(filePath, *thumbnailSize, activeThumbnailGeneration, db) + if resErr != nil { + return nil, nil, resErr + } + } + } + if thumbnail == nil { + return nil, nil, nil + } + thumbPath := string(thumbnailer.GetThumbnailPath(types.Path(filePath), thumbnail.ThumbnailSize)) + thumbFile, err := os.Open(string(thumbPath)) + if err != nil { + r.Logger.WithError(err).Warn("Failed to open file") + resErr := jsonerror.InternalServerError() + return thumbFile, nil, &resErr + } + thumbStat, err := thumbFile.Stat() + if err != nil { + r.Logger.WithError(err).Warn("Failed to stat file") + resErr := jsonerror.InternalServerError() + return thumbFile, nil, &resErr + } + if types.FileSizeBytes(thumbStat.Size()) != thumbnail.MediaMetadata.FileSizeBytes { + r.Logger.WithError(err).Warn("Thumbnail file sizes on disk and in database differ") + resErr := jsonerror.InternalServerError() + return thumbFile, nil, &resErr + } + return thumbFile, thumbnail, nil +} + +func (r *downloadRequest) generateThumbnail(filePath types.Path, thumbnailSize types.ThumbnailSize, activeThumbnailGeneration *types.ActiveThumbnailGeneration, db *storage.Database) (*types.ThumbnailMetadata, *util.JSONResponse) { + var err error + if err = thumbnailer.GenerateThumbnail(filePath, thumbnailSize, r.MediaMetadata, activeThumbnailGeneration, db, r.Logger); err != nil { + r.Logger.WithError(err).WithFields(log.Fields{ + "Width": thumbnailSize.Width, + "Height": thumbnailSize.Height, + "ResizeMethod": thumbnailSize.ResizeMethod, + }).Error("Error creating thumbnail") + resErr := jsonerror.InternalServerError() + return nil, &resErr + } + var thumbnail *types.ThumbnailMetadata + thumbnail, err = db.GetThumbnail(r.MediaMetadata.MediaID, r.MediaMetadata.Origin, thumbnailSize.Width, thumbnailSize.Height, thumbnailSize.ResizeMethod) + if err != nil { + r.Logger.WithError(err).WithFields(log.Fields{ + "Width": thumbnailSize.Width, + "Height": thumbnailSize.Height, + "ResizeMethod": thumbnailSize.ResizeMethod, + }).Error("Error looking up thumbnails") + resErr := jsonerror.InternalServerError() + return nil, &resErr + } + return thumbnail, nil +} + // getRemoteFile fetches the remote file and caches it locally // A hash map of active remote requests to a struct containing a sync.Cond is used to only download remote files once, // regardless of how many download requests are received. // Note: The named errorResponse return variable is used in a deferred broadcast of the metadata and error response to waiting goroutines. // Returns a util.JSONResponse error in case of error -func (r *downloadRequest) getRemoteFile(cfg *config.MediaAPI, db *storage.Database, activeRemoteRequests *types.ActiveRemoteRequests) (errorResponse *util.JSONResponse) { +func (r *downloadRequest) getRemoteFile(cfg *config.MediaAPI, db *storage.Database, activeRemoteRequests *types.ActiveRemoteRequests, activeThumbnailGeneration *types.ActiveThumbnailGeneration) (errorResponse *util.JSONResponse) { // Note: getMediaMetadataFromActiveRequest uses mutexes and conditions from activeRemoteRequests mediaMetadata, resErr := r.getMediaMetadataFromActiveRequest(activeRemoteRequests) if resErr != nil { @@ -245,7 +407,7 @@ func (r *downloadRequest) getRemoteFile(cfg *config.MediaAPI, db *storage.Databa if mediaMetadata == nil { // If we do not have a record, we need to fetch the remote file first and then respond from the local file - resErr := r.fetchRemoteFileAndStoreMetadata(cfg.AbsBasePath, *cfg.MaxFileSizeBytes, db) + resErr := r.fetchRemoteFileAndStoreMetadata(cfg.AbsBasePath, *cfg.MaxFileSizeBytes, db, cfg.ThumbnailSizes, activeThumbnailGeneration) if resErr != nil { return resErr } @@ -307,7 +469,7 @@ func (r *downloadRequest) broadcastMediaMetadata(activeRemoteRequests *types.Act } // fetchRemoteFileAndStoreMetadata fetches the file from the remote server and stores its metadata in the database -func (r *downloadRequest) fetchRemoteFileAndStoreMetadata(absBasePath types.Path, maxFileSizeBytes types.FileSizeBytes, db *storage.Database) *util.JSONResponse { +func (r *downloadRequest) fetchRemoteFileAndStoreMetadata(absBasePath types.Path, maxFileSizeBytes types.FileSizeBytes, db *storage.Database, thumbnailSizes []types.ThumbnailSize, activeThumbnailGeneration *types.ActiveThumbnailGeneration) *util.JSONResponse { finalPath, duplicate, resErr := r.fetchRemoteFile(absBasePath, maxFileSizeBytes) if resErr != nil { return resErr @@ -317,7 +479,7 @@ func (r *downloadRequest) fetchRemoteFileAndStoreMetadata(absBasePath types.Path "Base64Hash": r.MediaMetadata.Base64Hash, "UploadName": r.MediaMetadata.UploadName, "FileSizeBytes": r.MediaMetadata.FileSizeBytes, - "Content-Type": r.MediaMetadata.ContentType, + "ContentType": r.MediaMetadata.ContentType, }).Info("Storing file metadata to media repository database") // FIXME: timeout db request @@ -335,13 +497,18 @@ func (r *downloadRequest) fetchRemoteFileAndStoreMetadata(absBasePath types.Path return &resErr } - // TODO: generate thumbnails + go func() { + err := thumbnailer.GenerateThumbnails(finalPath, thumbnailSizes, r.MediaMetadata, activeThumbnailGeneration, db, r.Logger) + if err != nil { + r.Logger.WithError(err).Warn("Error generating thumbnails") + } + }() r.Logger.WithFields(log.Fields{ "UploadName": r.MediaMetadata.UploadName, "Base64Hash": r.MediaMetadata.Base64Hash, "FileSizeBytes": r.MediaMetadata.FileSizeBytes, - "Content-Type": r.MediaMetadata.ContentType, + "ContentType": r.MediaMetadata.ContentType, }).Infof("Remote file cached") return nil diff --git a/src/github.com/matrix-org/dendrite/mediaapi/writers/upload.go b/src/github.com/matrix-org/dendrite/mediaapi/writers/upload.go index aeda72fb2..5ce708b8a 100644 --- a/src/github.com/matrix-org/dendrite/mediaapi/writers/upload.go +++ b/src/github.com/matrix-org/dendrite/mediaapi/writers/upload.go @@ -27,6 +27,7 @@ import ( "github.com/matrix-org/dendrite/mediaapi/config" "github.com/matrix-org/dendrite/mediaapi/fileutils" "github.com/matrix-org/dendrite/mediaapi/storage" + "github.com/matrix-org/dendrite/mediaapi/thumbnailer" "github.com/matrix-org/dendrite/mediaapi/types" "github.com/matrix-org/util" ) @@ -50,13 +51,13 @@ type uploadResponse struct { // This implementation supports a configurable maximum file size limit in bytes. If a user tries to upload more than this, they will receive an error that their upload is too large. // Uploaded files are processed piece-wise to avoid DoS attacks which would starve the server of memory. // TODO: We should time out requests if they have not received any data within a configured timeout period. -func Upload(req *http.Request, cfg *config.MediaAPI, db *storage.Database) util.JSONResponse { +func Upload(req *http.Request, cfg *config.MediaAPI, db *storage.Database, activeThumbnailGeneration *types.ActiveThumbnailGeneration) util.JSONResponse { r, resErr := parseAndValidateRequest(req, cfg) if resErr != nil { return *resErr } - if resErr = r.doUpload(req.Body, cfg, db); resErr != nil { + if resErr = r.doUpload(req.Body, cfg, db, activeThumbnailGeneration); resErr != nil { return *resErr } @@ -96,7 +97,7 @@ func parseAndValidateRequest(req *http.Request, cfg *config.MediaAPI) (*uploadRe return r, nil } -func (r *uploadRequest) doUpload(reqReader io.Reader, cfg *config.MediaAPI, db *storage.Database) *util.JSONResponse { +func (r *uploadRequest) doUpload(reqReader io.Reader, cfg *config.MediaAPI, db *storage.Database, activeThumbnailGeneration *types.ActiveThumbnailGeneration) *util.JSONResponse { r.Logger.WithFields(log.Fields{ "UploadName": r.MediaMetadata.UploadName, "FileSizeBytes": r.MediaMetadata.FileSizeBytes, @@ -151,9 +152,7 @@ func (r *uploadRequest) doUpload(reqReader io.Reader, cfg *config.MediaAPI, db * } } - // TODO: generate thumbnails - - if resErr := r.storeFileAndMetadata(tmpDir, cfg.AbsBasePath, db); resErr != nil { + if resErr := r.storeFileAndMetadata(tmpDir, cfg.AbsBasePath, db, cfg.ThumbnailSizes, activeThumbnailGeneration); resErr != nil { return resErr } @@ -216,7 +215,7 @@ func (r *uploadRequest) Validate(maxFileSizeBytes types.FileSizeBytes) *util.JSO // The order of operations is important as it avoids metadata entering the database before the file // is ready, and if we fail to move the file, it never gets added to the database. // Returns a util.JSONResponse error and cleans up directories in case of error. -func (r *uploadRequest) storeFileAndMetadata(tmpDir types.Path, absBasePath types.Path, db *storage.Database) *util.JSONResponse { +func (r *uploadRequest) storeFileAndMetadata(tmpDir types.Path, absBasePath types.Path, db *storage.Database, thumbnailSizes []types.ThumbnailSize, activeThumbnailGeneration *types.ActiveThumbnailGeneration) *util.JSONResponse { finalPath, duplicate, err := fileutils.MoveFileWithHashCheck(tmpDir, r.MediaMetadata, absBasePath, r.Logger) if err != nil { r.Logger.WithError(err).Error("Failed to move file.") @@ -243,5 +242,12 @@ func (r *uploadRequest) storeFileAndMetadata(tmpDir types.Path, absBasePath type } } + go func() { + err := thumbnailer.GenerateThumbnails(finalPath, thumbnailSizes, r.MediaMetadata, activeThumbnailGeneration, db, r.Logger) + if err != nil { + r.Logger.WithError(err).Warn("Error generating thumbnails") + } + }() + return nil }