mediaapi: Add thumbnail support

This commit is contained in:
Robert Swain 2017-06-03 11:08:02 +02:00
parent e05f1af49a
commit 83fcf7dc1f
6 changed files with 287 additions and 48 deletions

View file

@ -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")
}))
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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

View file

@ -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
}