mirror of
https://github.com/matrix-org/dendrite.git
synced 2025-12-08 07:23:10 -06:00
mediaapi: Add thumbnail support
This commit is contained in:
parent
e05f1af49a
commit
83fcf7dc1f
|
|
@ -35,16 +35,32 @@ 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) {
|
||||
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
|
||||
|
|
@ -53,10 +69,6 @@ func Setup(servMux *http.ServeMux, httpClient *http.Client, cfg *config.MediaAPI
|
|||
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)
|
||||
})),
|
||||
)
|
||||
|
||||
servMux.Handle("/metrics", prometheus.Handler())
|
||||
servMux.Handle("/api/", http.StripPrefix("/api", apiMux))
|
||||
writers.Download(w, req, gomatrixserverlib.ServerName(vars["serverName"]), types.MediaID(vars["mediaId"]), cfg, db, activeRemoteRequests, activeThumbnailGeneration, name == "thumbnail")
|
||||
}))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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
|
||||
}
|
||||
|
||||
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,
|
||||
"Content-Type": r.MediaMetadata.ContentType,
|
||||
"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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue