mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
* Prepare file upload infrastructure for Data Retention. This commit prepares the file upload infrastructure for the data retention feature that is under construction. Changes are: * Move file management code to utils to allow access to it from jobs. * From now on, store all file uploads in a top level folder which is the date of the day on which they were uploaded. This commit is based on Harrison Healey's branch, but updated to work with the latest master. * Use NewAppError
233 lines
7.5 KiB
Go
233 lines
7.5 KiB
Go
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
|
// See License.txt for license information.
|
|
|
|
package app
|
|
|
|
import (
|
|
"bytes"
|
|
"image"
|
|
"image/draw"
|
|
"image/gif"
|
|
_ "image/jpeg"
|
|
"image/png"
|
|
"io"
|
|
"mime/multipart"
|
|
"net/http"
|
|
|
|
l4g "github.com/alecthomas/log4go"
|
|
|
|
"image/color/palette"
|
|
|
|
"github.com/disintegration/imaging"
|
|
"github.com/mattermost/platform/model"
|
|
"github.com/mattermost/platform/utils"
|
|
)
|
|
|
|
const (
|
|
MaxEmojiFileSize = 1000 * 1024 // 1 MB
|
|
MaxEmojiWidth = 128
|
|
MaxEmojiHeight = 128
|
|
)
|
|
|
|
func CreateEmoji(sessionUserId string, emoji *model.Emoji, multiPartImageData *multipart.Form) (*model.Emoji, *model.AppError) {
|
|
// wipe the emoji id so that existing emojis can't get overwritten
|
|
emoji.Id = ""
|
|
|
|
// do our best to validate the emoji before committing anything to the DB so that we don't have to clean up
|
|
// orphaned files left over when validation fails later on
|
|
emoji.PreSave()
|
|
if err := emoji.IsValid(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if emoji.CreatorId != sessionUserId {
|
|
return nil, model.NewAppError("createEmoji", "api.emoji.create.other_user.app_error", nil, "", http.StatusForbidden)
|
|
}
|
|
|
|
if result := <-Srv.Store.Emoji().GetByName(emoji.Name); result.Err == nil && result.Data != nil {
|
|
return nil, model.NewAppError("createEmoji", "api.emoji.create.duplicate.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if imageData := multiPartImageData.File["image"]; len(imageData) == 0 {
|
|
err := model.NewLocAppError("Context", "api.context.invalid_body_param.app_error", map[string]interface{}{"Name": "createEmoji"}, "")
|
|
err.StatusCode = http.StatusBadRequest
|
|
return nil, err
|
|
} else if err := UploadEmojiImage(emoji.Id, imageData[0]); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if result := <-Srv.Store.Emoji().Save(emoji); result.Err != nil {
|
|
return nil, result.Err
|
|
} else {
|
|
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_EMOJI_ADDED, "", "", "", nil)
|
|
message.Add("emoji", emoji.ToJson())
|
|
|
|
Publish(message)
|
|
return result.Data.(*model.Emoji), nil
|
|
}
|
|
}
|
|
|
|
func GetEmojiList(page, perPage int) ([]*model.Emoji, *model.AppError) {
|
|
if result := <-Srv.Store.Emoji().GetList(page*perPage, perPage); result.Err != nil {
|
|
return nil, result.Err
|
|
} else {
|
|
return result.Data.([]*model.Emoji), nil
|
|
}
|
|
}
|
|
|
|
func UploadEmojiImage(id string, imageData *multipart.FileHeader) *model.AppError {
|
|
file, err := imageData.Open()
|
|
if err != nil {
|
|
return model.NewAppError("uploadEmojiImage", "api.emoji.upload.open.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
defer file.Close()
|
|
|
|
buf := bytes.NewBuffer(nil)
|
|
io.Copy(buf, file)
|
|
|
|
// make sure the file is an image and is within the required dimensions
|
|
if config, _, err := image.DecodeConfig(bytes.NewReader(buf.Bytes())); err != nil {
|
|
return model.NewAppError("uploadEmojiImage", "api.emoji.upload.image.app_error", nil, "", http.StatusBadRequest)
|
|
} else if config.Width > MaxEmojiWidth || config.Height > MaxEmojiHeight {
|
|
data := buf.Bytes()
|
|
newbuf := bytes.NewBuffer(nil)
|
|
if info, err := model.GetInfoForBytes(imageData.Filename, data); err != nil {
|
|
return err
|
|
} else if info.MimeType == "image/gif" {
|
|
if gif_data, err := gif.DecodeAll(bytes.NewReader(data)); err != nil {
|
|
return model.NewAppError("uploadEmojiImage", "api.emoji.upload.large_image.gif_decode_error", nil, "", http.StatusBadRequest)
|
|
} else {
|
|
resized_gif := resizeEmojiGif(gif_data)
|
|
if err := gif.EncodeAll(newbuf, resized_gif); err != nil {
|
|
return model.NewAppError("uploadEmojiImage", "api.emoji.upload.large_image.gif_encode_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
if err := utils.WriteFile(newbuf.Bytes(), getEmojiImagePath(id)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
} else {
|
|
if img, _, err := image.Decode(bytes.NewReader(data)); err != nil {
|
|
return model.NewAppError("uploadEmojiImage", "api.emoji.upload.large_image.decode_error", nil, "", http.StatusBadRequest)
|
|
} else {
|
|
resized_image := resizeEmoji(img, config.Width, config.Height)
|
|
if err := png.Encode(newbuf, resized_image); err != nil {
|
|
return model.NewAppError("uploadEmojiImage", "api.emoji.upload.large_image.encode_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
if err := utils.WriteFile(newbuf.Bytes(), getEmojiImagePath(id)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if err := utils.WriteFile(buf.Bytes(), getEmojiImagePath(id)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func DeleteEmoji(emoji *model.Emoji) *model.AppError {
|
|
if err := (<-Srv.Store.Emoji().Delete(emoji.Id, model.GetMillis())).Err; err != nil {
|
|
return err
|
|
}
|
|
|
|
deleteEmojiImage(emoji.Id)
|
|
deleteReactionsForEmoji(emoji.Name)
|
|
return nil
|
|
}
|
|
|
|
func GetEmoji(emojiId string) (*model.Emoji, *model.AppError) {
|
|
if !*utils.Cfg.ServiceSettings.EnableCustomEmoji {
|
|
return nil, model.NewAppError("deleteEmoji", "api.emoji.disabled.app_error", nil, "", http.StatusNotImplemented)
|
|
}
|
|
|
|
if len(utils.Cfg.FileSettings.DriverName) == 0 {
|
|
return nil, model.NewAppError("deleteImage", "api.emoji.storage.app_error", nil, "", http.StatusNotImplemented)
|
|
}
|
|
|
|
if result := <-Srv.Store.Emoji().Get(emojiId, false); result.Err != nil {
|
|
return nil, result.Err
|
|
} else {
|
|
return result.Data.(*model.Emoji), nil
|
|
}
|
|
}
|
|
|
|
func GetEmojiImage(emojiId string) (imageByte []byte, imageType string, err *model.AppError) {
|
|
if result := <-Srv.Store.Emoji().Get(emojiId, true); result.Err != nil {
|
|
return nil, "", result.Err
|
|
} else {
|
|
var img []byte
|
|
|
|
if data, err := utils.ReadFile(getEmojiImagePath(emojiId)); err != nil {
|
|
return nil, "", model.NewAppError("getEmojiImage", "api.emoji.get_image.read.app_error", nil, err.Error(), http.StatusNotFound)
|
|
} else {
|
|
img = data
|
|
}
|
|
|
|
_, imageType, err := image.DecodeConfig(bytes.NewReader(img))
|
|
if err != nil {
|
|
return nil, "", model.NewAppError("getEmojiImage", "api.emoji.get_image.decode.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return img, imageType, nil
|
|
}
|
|
}
|
|
|
|
func resizeEmojiGif(gifImg *gif.GIF) *gif.GIF {
|
|
// Create a new RGBA image to hold the incremental frames.
|
|
firstFrame := gifImg.Image[0].Bounds()
|
|
b := image.Rect(0, 0, firstFrame.Dx(), firstFrame.Dy())
|
|
img := image.NewRGBA(b)
|
|
|
|
resizedImage := image.Image(nil)
|
|
// Resize each frame.
|
|
for index, frame := range gifImg.Image {
|
|
bounds := frame.Bounds()
|
|
draw.Draw(img, bounds, frame, bounds.Min, draw.Over)
|
|
resizedImage = resizeEmoji(img, firstFrame.Dx(), firstFrame.Dy())
|
|
gifImg.Image[index] = imageToPaletted(resizedImage)
|
|
}
|
|
// Set new gif width and height
|
|
gifImg.Config.Width = resizedImage.Bounds().Dx()
|
|
gifImg.Config.Height = resizedImage.Bounds().Dy()
|
|
return gifImg
|
|
}
|
|
|
|
func getEmojiImagePath(id string) string {
|
|
return "emoji/" + id + "/image"
|
|
}
|
|
|
|
func resizeEmoji(img image.Image, width int, height int) image.Image {
|
|
emojiWidth := float64(width)
|
|
emojiHeight := float64(height)
|
|
|
|
var emoji image.Image
|
|
if emojiHeight <= MaxEmojiHeight && emojiWidth <= MaxEmojiWidth {
|
|
emoji = img
|
|
} else {
|
|
emoji = imaging.Fit(img, MaxEmojiWidth, MaxEmojiHeight, imaging.Lanczos)
|
|
}
|
|
return emoji
|
|
}
|
|
|
|
func imageToPaletted(img image.Image) *image.Paletted {
|
|
b := img.Bounds()
|
|
pm := image.NewPaletted(b, palette.Plan9)
|
|
draw.FloydSteinberg.Draw(pm, b, img, image.ZP)
|
|
return pm
|
|
}
|
|
|
|
func deleteEmojiImage(id string) {
|
|
if err := utils.MoveFile(getEmojiImagePath(id), "emoji/"+id+"/image_deleted"); err != nil {
|
|
l4g.Error("Failed to rename image when deleting emoji %v", id)
|
|
}
|
|
}
|
|
|
|
func deleteReactionsForEmoji(emojiName string) {
|
|
if result := <-Srv.Store.Reaction().DeleteAllWithEmojiName(emojiName); result.Err != nil {
|
|
l4g.Warn(utils.T("api.emoji.delete.delete_reactions.app_error"), emojiName)
|
|
l4g.Warn(result.Err)
|
|
}
|
|
}
|