We move these fields to Channels: ``` uploadLockMapMut sync.Mutex uploadLockMap map[string]bool imgDecoder *imaging.Decoder imgEncoder *imaging.Encoder dndTaskMut sync.Mutex dndTask *model.ScheduledTask ``` I think this PR should conclue the initial phase of migrating stuff from Server to Channels. The remaining task would be to focus on continue to create the remaining services from the common things like users, teams, push notifications, clustering for other products to consume. https://community-daily.mattermost.com/boards/workspace/zyoahc9uapdn3xdptac6jb69ic/285b80a3-257d-41f6-8cf4-ed80ca9d92e5/495cdb4d-c13a-4992-8eb9-80cfee2819a4/87df1e15-588e-49ff-8bd1-ffa9651b8c82 ```release-note NONE ```
317 строки
10 KiB
Go
317 строки
10 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package app
|
|
|
|
import (
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/mattermost/mattermost-server/v6/app/request"
|
|
"github.com/mattermost/mattermost-server/v6/model"
|
|
"github.com/mattermost/mattermost-server/v6/plugin"
|
|
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
|
"github.com/mattermost/mattermost-server/v6/store"
|
|
)
|
|
|
|
const minFirstPartSize = 5 * 1024 * 1024 // 5MB
|
|
const IncompleteUploadSuffix = ".tmp"
|
|
|
|
func (a *App) runPluginsHook(c *request.Context, info *model.FileInfo, file io.Reader) *model.AppError {
|
|
pluginsEnvironment := a.GetPluginsEnvironment()
|
|
if pluginsEnvironment == nil {
|
|
return nil
|
|
}
|
|
|
|
filePath := info.Path
|
|
// using a pipe to avoid loading the whole file content in memory.
|
|
r, w := io.Pipe()
|
|
errChan := make(chan *model.AppError, 1)
|
|
hookHasRunCh := make(chan struct{})
|
|
|
|
go func() {
|
|
defer w.Close()
|
|
defer close(hookHasRunCh)
|
|
defer close(errChan)
|
|
var rejErr *model.AppError
|
|
var once sync.Once
|
|
pluginContext := pluginContext(c)
|
|
pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
|
once.Do(func() {
|
|
hookHasRunCh <- struct{}{}
|
|
})
|
|
newInfo, rejStr := hooks.FileWillBeUploaded(pluginContext, info, file, w)
|
|
if rejStr != "" {
|
|
rejErr = model.NewAppError("runPluginsHook", "app.upload.run_plugins_hook.rejected",
|
|
map[string]interface{}{"Filename": info.Name, "Reason": rejStr}, "", http.StatusBadRequest)
|
|
return false
|
|
}
|
|
if newInfo != nil {
|
|
info = newInfo
|
|
}
|
|
return true
|
|
}, plugin.FileWillBeUploadedID)
|
|
if rejErr != nil {
|
|
errChan <- rejErr
|
|
}
|
|
}()
|
|
|
|
// If the plugin hook has not run we can return early.
|
|
if _, ok := <-hookHasRunCh; !ok {
|
|
return nil
|
|
}
|
|
|
|
tmpPath := filePath + ".tmp"
|
|
written, err := a.WriteFile(r, tmpPath)
|
|
if err != nil {
|
|
if fileErr := a.RemoveFile(tmpPath); fileErr != nil {
|
|
mlog.Warn("Failed to remove file", mlog.Err(fileErr))
|
|
}
|
|
return err
|
|
}
|
|
|
|
if err = <-errChan; err != nil {
|
|
if fileErr := a.RemoveFile(info.Path); fileErr != nil {
|
|
mlog.Warn("Failed to remove file", mlog.Err(fileErr))
|
|
}
|
|
if fileErr := a.RemoveFile(tmpPath); fileErr != nil {
|
|
mlog.Warn("Failed to remove file", mlog.Err(fileErr))
|
|
}
|
|
return err
|
|
}
|
|
|
|
if written > 0 {
|
|
info.Size = written
|
|
if fileErr := a.MoveFile(tmpPath, info.Path); fileErr != nil {
|
|
return model.NewAppError("runPluginsHook", "app.upload.run_plugins_hook.move_fail",
|
|
nil, fileErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
} else {
|
|
if fileErr := a.RemoveFile(tmpPath); fileErr != nil {
|
|
mlog.Warn("Failed to remove file", mlog.Err(fileErr))
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) CreateUploadSession(us *model.UploadSession) (*model.UploadSession, *model.AppError) {
|
|
if us.FileSize > *a.Config().FileSettings.MaxFileSize {
|
|
return nil, model.NewAppError("CreateUploadSession", "app.upload.create.upload_too_large.app_error",
|
|
map[string]interface{}{"channelId": us.ChannelId}, "", http.StatusRequestEntityTooLarge)
|
|
}
|
|
|
|
us.FileOffset = 0
|
|
now := time.Now()
|
|
us.CreateAt = model.GetMillisForTime(now)
|
|
if us.Type == model.UploadTypeAttachment {
|
|
us.Path = now.Format("20060102") + "/teams/noteam/channels/" + us.ChannelId + "/users/" + us.UserId + "/" + us.Id + "/" + filepath.Base(us.Filename)
|
|
} else if us.Type == model.UploadTypeImport {
|
|
us.Path = filepath.Clean(*a.Config().ImportSettings.Directory) + "/" + us.Id + "_" + filepath.Base(us.Filename)
|
|
}
|
|
if err := us.IsValid(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if us.Type == model.UploadTypeAttachment {
|
|
channel, err := a.GetChannel(us.ChannelId)
|
|
if err != nil {
|
|
return nil, model.NewAppError("CreateUploadSession", "app.upload.create.incorrect_channel_id.app_error",
|
|
map[string]interface{}{"channelId": us.ChannelId}, "", http.StatusBadRequest)
|
|
}
|
|
if channel.DeleteAt != 0 {
|
|
return nil, model.NewAppError("CreateUploadSession", "app.upload.create.cannot_upload_to_deleted_channel.app_error",
|
|
map[string]interface{}{"channelId": us.ChannelId}, "", http.StatusBadRequest)
|
|
}
|
|
}
|
|
|
|
us, storeErr := a.Srv().Store.UploadSession().Save(us)
|
|
if storeErr != nil {
|
|
return nil, model.NewAppError("CreateUploadSession", "app.upload.create.save.app_error", nil, storeErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return us, nil
|
|
}
|
|
|
|
func (a *App) GetUploadSession(uploadId string) (*model.UploadSession, *model.AppError) {
|
|
us, err := a.Srv().Store.UploadSession().Get(uploadId)
|
|
if err != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(err, &nfErr):
|
|
return nil, model.NewAppError("GetUpload", "app.upload.get.app_error",
|
|
nil, nfErr.Error(), http.StatusNotFound)
|
|
default:
|
|
return nil, model.NewAppError("GetUpload", "app.upload.get.app_error",
|
|
nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
return us, nil
|
|
}
|
|
|
|
func (a *App) GetUploadSessionsForUser(userID string) ([]*model.UploadSession, *model.AppError) {
|
|
uss, err := a.Srv().Store.UploadSession().GetForUser(userID)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetUploadsForUser", "app.upload.get_for_user.app_error",
|
|
nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
return uss, nil
|
|
}
|
|
|
|
func (a *App) UploadData(c *request.Context, us *model.UploadSession, rd io.Reader) (*model.FileInfo, *model.AppError) {
|
|
// prevent more than one caller to upload data at the same time for a given upload session.
|
|
// This is to avoid possible inconsistencies.
|
|
a.ch.uploadLockMapMut.Lock()
|
|
locked := a.ch.uploadLockMap[us.Id]
|
|
if locked {
|
|
// session lock is already taken, return error.
|
|
a.ch.uploadLockMapMut.Unlock()
|
|
return nil, model.NewAppError("UploadData", "app.upload.upload_data.concurrent.app_error",
|
|
nil, "", http.StatusBadRequest)
|
|
}
|
|
// grab the session lock.
|
|
a.ch.uploadLockMap[us.Id] = true
|
|
a.ch.uploadLockMapMut.Unlock()
|
|
|
|
// reset the session lock on exit.
|
|
defer func() {
|
|
a.ch.uploadLockMapMut.Lock()
|
|
delete(a.ch.uploadLockMap, us.Id)
|
|
a.ch.uploadLockMapMut.Unlock()
|
|
}()
|
|
|
|
// fetch the session from store to check for inconsistencies.
|
|
if storedSession, err := a.GetUploadSession(us.Id); err != nil {
|
|
return nil, err
|
|
} else if us.FileOffset != storedSession.FileOffset {
|
|
return nil, model.NewAppError("UploadData", "app.upload.upload_data.concurrent.app_error",
|
|
nil, "FileOffset mismatch", http.StatusBadRequest)
|
|
}
|
|
|
|
uploadPath := us.Path
|
|
if us.Type == model.UploadTypeImport {
|
|
uploadPath += IncompleteUploadSuffix
|
|
}
|
|
|
|
// make sure it's not possible to upload more data than what is expected.
|
|
lr := &io.LimitedReader{
|
|
R: rd,
|
|
N: us.FileSize - us.FileOffset,
|
|
}
|
|
var err *model.AppError
|
|
var written int64
|
|
if us.FileOffset == 0 {
|
|
// new upload
|
|
written, err = a.WriteFile(lr, uploadPath)
|
|
if err != nil && written == 0 {
|
|
return nil, err
|
|
}
|
|
if written < minFirstPartSize && written != us.FileSize {
|
|
a.RemoveFile(uploadPath)
|
|
var errStr string
|
|
if err != nil {
|
|
errStr = err.Error()
|
|
}
|
|
return nil, model.NewAppError("UploadData", "app.upload.upload_data.first_part_too_small.app_error",
|
|
map[string]interface{}{"Size": minFirstPartSize}, errStr, http.StatusBadRequest)
|
|
}
|
|
} else if us.FileOffset < us.FileSize {
|
|
// resume upload
|
|
written, err = a.AppendFile(lr, uploadPath)
|
|
}
|
|
if written > 0 {
|
|
us.FileOffset += written
|
|
if storeErr := a.Srv().Store.UploadSession().Update(us); storeErr != nil {
|
|
return nil, model.NewAppError("UploadData", "app.upload.upload_data.update.app_error", nil, storeErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// upload is incomplete
|
|
if us.FileOffset != us.FileSize {
|
|
return nil, nil
|
|
}
|
|
|
|
// upload is done, create FileInfo
|
|
file, err := a.FileReader(uploadPath)
|
|
if err != nil {
|
|
return nil, model.NewAppError("UploadData", "app.upload.upload_data.read_file.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
info, err := model.GetInfoForBytes(us.Filename, file, int(us.FileSize))
|
|
file.Close()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
info.CreatorId = us.UserId
|
|
info.Path = us.Path
|
|
info.RemoteId = model.NewString(us.RemoteId)
|
|
if us.ReqFileId != "" {
|
|
info.Id = us.ReqFileId
|
|
}
|
|
|
|
// run plugins upload hook
|
|
if err := a.runPluginsHook(c, info, file); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// image post-processing
|
|
if info.IsImage() {
|
|
if limitErr := checkImageResolutionLimit(info.Width, info.Height, *a.Config().FileSettings.MaxImageResolution); limitErr != nil {
|
|
return nil, model.NewAppError("uploadData", "app.upload.upload_data.large_image.app_error",
|
|
map[string]interface{}{"Filename": us.Filename, "Width": info.Width, "Height": info.Height}, "", http.StatusBadRequest)
|
|
}
|
|
|
|
nameWithoutExtension := info.Name[:strings.LastIndex(info.Name, ".")]
|
|
info.PreviewPath = filepath.Dir(info.Path) + "/" + nameWithoutExtension + "_preview.jpg"
|
|
info.ThumbnailPath = filepath.Dir(info.Path) + "/" + nameWithoutExtension + "_thumb.jpg"
|
|
imgData, fileErr := a.ReadFile(uploadPath)
|
|
if fileErr != nil {
|
|
return nil, fileErr
|
|
}
|
|
a.HandleImages([]string{info.PreviewPath}, []string{info.ThumbnailPath}, [][]byte{imgData})
|
|
}
|
|
|
|
if us.Type == model.UploadTypeImport {
|
|
if err := a.MoveFile(uploadPath, us.Path); err != nil {
|
|
return nil, model.NewAppError("UploadData", "app.upload.upload_data.move_file.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
var storeErr error
|
|
if info, storeErr = a.Srv().Store.FileInfo().Save(info); storeErr != nil {
|
|
var appErr *model.AppError
|
|
switch {
|
|
case errors.As(storeErr, &appErr):
|
|
return nil, appErr
|
|
default:
|
|
return nil, model.NewAppError("uploadData", "app.upload.upload_data.save.app_error", nil, storeErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
if *a.Config().FileSettings.ExtractContent {
|
|
infoCopy := *info
|
|
a.Srv().Go(func() {
|
|
err := a.ExtractContentFromFileInfo(&infoCopy)
|
|
if err != nil {
|
|
mlog.Error("Failed to extract file content", mlog.Err(err), mlog.String("fileInfoId", infoCopy.Id))
|
|
}
|
|
})
|
|
}
|
|
|
|
// delete upload session
|
|
if storeErr := a.Srv().Store.UploadSession().Delete(us.Id); storeErr != nil {
|
|
mlog.Warn("Failed to delete UploadSession", mlog.Err(storeErr))
|
|
}
|
|
|
|
return info, nil
|
|
}
|