github.com/adacta-ru/mattermost-server/v6@v6.0.0/app/upload.go (about)

     1  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
     2  // See LICENSE.txt for license information.
     3  
     4  package app
     5  
     6  import (
     7  	"errors"
     8  	"io"
     9  	"net/http"
    10  	"path/filepath"
    11  	"strings"
    12  	"sync"
    13  	"time"
    14  
    15  	"github.com/adacta-ru/mattermost-server/v6/mlog"
    16  	"github.com/adacta-ru/mattermost-server/v6/model"
    17  	"github.com/adacta-ru/mattermost-server/v6/plugin"
    18  	"github.com/adacta-ru/mattermost-server/v6/store"
    19  )
    20  
    21  const minFirstPartSize = 5 * 1024 * 1024 // 5MB
    22  const incompleteUploadSuffix = ".tmp"
    23  
    24  func (a *App) runPluginsHook(info *model.FileInfo, file io.Reader) *model.AppError {
    25  	pluginsEnvironment := a.GetPluginsEnvironment()
    26  	if pluginsEnvironment == nil {
    27  		return nil
    28  	}
    29  
    30  	filePath := info.Path
    31  	// using a pipe to avoid loading the whole file content in memory.
    32  	r, w := io.Pipe()
    33  	errChan := make(chan *model.AppError, 1)
    34  	hookHasRunCh := make(chan struct{})
    35  
    36  	go func() {
    37  		defer w.Close()
    38  		defer close(hookHasRunCh)
    39  		defer close(errChan)
    40  		var rejErr *model.AppError
    41  		var once sync.Once
    42  		pluginContext := a.PluginContext()
    43  		pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
    44  			once.Do(func() {
    45  				hookHasRunCh <- struct{}{}
    46  			})
    47  			newInfo, rejStr := hooks.FileWillBeUploaded(pluginContext, info, file, w)
    48  			if rejStr != "" {
    49  				rejErr = model.NewAppError("runPluginsHook", "app.upload.run_plugins_hook.rejected",
    50  					map[string]interface{}{"Filename": info.Name, "Reason": rejStr}, "", http.StatusBadRequest)
    51  				return false
    52  			}
    53  			if newInfo != nil {
    54  				info = newInfo
    55  			}
    56  			return true
    57  		}, plugin.FileWillBeUploadedId)
    58  		if rejErr != nil {
    59  			errChan <- rejErr
    60  		}
    61  	}()
    62  
    63  	// If the plugin hook has not run we can return early.
    64  	if _, ok := <-hookHasRunCh; !ok {
    65  		return nil
    66  	}
    67  
    68  	tmpPath := filePath + ".tmp"
    69  	written, err := a.WriteFile(r, tmpPath)
    70  	if err != nil {
    71  		if fileErr := a.RemoveFile(tmpPath); fileErr != nil {
    72  			mlog.Error("Failed to remove file", mlog.Err(fileErr))
    73  		}
    74  		return err
    75  	}
    76  
    77  	if err = <-errChan; err != nil {
    78  		if fileErr := a.RemoveFile(info.Path); fileErr != nil {
    79  			mlog.Error("Failed to remove file", mlog.Err(fileErr))
    80  		}
    81  		if fileErr := a.RemoveFile(tmpPath); fileErr != nil {
    82  			mlog.Error("Failed to remove file", mlog.Err(fileErr))
    83  		}
    84  		return err
    85  	}
    86  
    87  	if written > 0 {
    88  		info.Size = written
    89  		if fileErr := a.MoveFile(tmpPath, info.Path); fileErr != nil {
    90  			mlog.Error("Failed to move file", mlog.Err(fileErr))
    91  			return model.NewAppError("runPluginsHook", "app.upload.run_plugins_hook.move_fail",
    92  				nil, fileErr.Error(), http.StatusInternalServerError)
    93  		}
    94  	} else {
    95  		if fileErr := a.RemoveFile(tmpPath); fileErr != nil {
    96  			mlog.Error("Failed to remove file", mlog.Err(fileErr))
    97  		}
    98  	}
    99  
   100  	return nil
   101  }
   102  
   103  func (a *App) CreateUploadSession(us *model.UploadSession) (*model.UploadSession, *model.AppError) {
   104  	if us.FileSize > *a.Config().FileSettings.MaxFileSize {
   105  		return nil, model.NewAppError("CreateUploadSession", "app.upload.create.upload_too_large.app_error",
   106  			map[string]interface{}{"channelId": us.ChannelId}, "", http.StatusRequestEntityTooLarge)
   107  	}
   108  
   109  	us.FileOffset = 0
   110  	now := time.Now()
   111  	us.CreateAt = model.GetMillisForTime(now)
   112  	if us.Type == model.UploadTypeAttachment {
   113  		us.Path = now.Format("20060102") + "/teams/noteam/channels/" + us.ChannelId + "/users/" + us.UserId + "/" + us.Id + "/" + filepath.Base(us.Filename)
   114  	} else if us.Type == model.UploadTypeImport {
   115  		us.Path = *a.Config().ImportSettings.Directory + "/" + us.Id + "_" + filepath.Base(us.Filename)
   116  	}
   117  	if err := us.IsValid(); err != nil {
   118  		return nil, err
   119  	}
   120  
   121  	if us.Type == model.UploadTypeAttachment {
   122  		channel, err := a.GetChannel(us.ChannelId)
   123  		if err != nil {
   124  			return nil, model.NewAppError("CreateUploadSession", "app.upload.create.incorrect_channel_id.app_error",
   125  				map[string]interface{}{"channelId": us.ChannelId}, "", http.StatusBadRequest)
   126  		}
   127  		if channel.DeleteAt != 0 {
   128  			return nil, model.NewAppError("CreateUploadSession", "app.upload.create.cannot_upload_to_deleted_channel.app_error",
   129  				map[string]interface{}{"channelId": us.ChannelId}, "", http.StatusBadRequest)
   130  		}
   131  	}
   132  
   133  	us, storeErr := a.Srv().Store.UploadSession().Save(us)
   134  	if storeErr != nil {
   135  		return nil, model.NewAppError("CreateUploadSession", "app.upload.create.save.app_error", nil, storeErr.Error(), http.StatusInternalServerError)
   136  	}
   137  
   138  	return us, nil
   139  }
   140  
   141  func (a *App) GetUploadSession(uploadId string) (*model.UploadSession, *model.AppError) {
   142  	us, err := a.Srv().Store.UploadSession().Get(uploadId)
   143  	if err != nil {
   144  		var nfErr *store.ErrNotFound
   145  		switch {
   146  		case errors.As(err, &nfErr):
   147  			return nil, model.NewAppError("GetUpload", "app.upload.get.app_error",
   148  				nil, nfErr.Error(), http.StatusNotFound)
   149  		default:
   150  			return nil, model.NewAppError("GetUpload", "app.upload.get.app_error",
   151  				nil, err.Error(), http.StatusInternalServerError)
   152  		}
   153  	}
   154  	return us, nil
   155  }
   156  
   157  func (a *App) GetUploadSessionsForUser(userId string) ([]*model.UploadSession, *model.AppError) {
   158  	uss, err := a.Srv().Store.UploadSession().GetForUser(userId)
   159  	if err != nil {
   160  		return nil, model.NewAppError("GetUploadsForUser", "app.upload.get_for_user.app_error",
   161  			nil, err.Error(), http.StatusInternalServerError)
   162  	}
   163  	return uss, nil
   164  }
   165  
   166  func (a *App) UploadData(us *model.UploadSession, rd io.Reader) (*model.FileInfo, *model.AppError) {
   167  	// prevent more than one caller to upload data at the same time for a given upload session.
   168  	// This is to avoid possible inconsistencies.
   169  	a.Srv().uploadLockMapMut.Lock()
   170  	locked := a.Srv().uploadLockMap[us.Id]
   171  	if locked {
   172  		// session lock is already taken, return error.
   173  		a.Srv().uploadLockMapMut.Unlock()
   174  		return nil, model.NewAppError("UploadData", "app.upload.upload_data.concurrent.app_error",
   175  			nil, "", http.StatusBadRequest)
   176  	}
   177  	// grab the session lock.
   178  	a.Srv().uploadLockMap[us.Id] = true
   179  	a.Srv().uploadLockMapMut.Unlock()
   180  
   181  	// reset the session lock on exit.
   182  	defer func() {
   183  		a.Srv().uploadLockMapMut.Lock()
   184  		delete(a.Srv().uploadLockMap, us.Id)
   185  		a.Srv().uploadLockMapMut.Unlock()
   186  	}()
   187  
   188  	// fetch the session from store to check for inconsistencies.
   189  	if storedSession, err := a.GetUploadSession(us.Id); err != nil {
   190  		return nil, err
   191  	} else if us.FileOffset != storedSession.FileOffset {
   192  		return nil, model.NewAppError("UploadData", "app.upload.upload_data.concurrent.app_error",
   193  			nil, "FileOffset mismatch", http.StatusBadRequest)
   194  	}
   195  
   196  	uploadPath := us.Path
   197  	if us.Type == model.UploadTypeImport {
   198  		uploadPath += incompleteUploadSuffix
   199  	}
   200  
   201  	// make sure it's not possible to upload more data than what is expected.
   202  	lr := &io.LimitedReader{
   203  		R: rd,
   204  		N: us.FileSize - us.FileOffset,
   205  	}
   206  	var err *model.AppError
   207  	var written int64
   208  	if us.FileOffset == 0 {
   209  		// new upload
   210  		written, err = a.WriteFile(lr, uploadPath)
   211  		if err != nil && written == 0 {
   212  			return nil, err
   213  		}
   214  		if written < minFirstPartSize && written != us.FileSize {
   215  			a.RemoveFile(uploadPath)
   216  			var errStr string
   217  			if err != nil {
   218  				errStr = err.Error()
   219  			}
   220  			return nil, model.NewAppError("UploadData", "app.upload.upload_data.first_part_too_small.app_error",
   221  				map[string]interface{}{"Size": minFirstPartSize}, errStr, http.StatusBadRequest)
   222  		}
   223  	} else if us.FileOffset < us.FileSize {
   224  		// resume upload
   225  		written, err = a.AppendFile(lr, uploadPath)
   226  	}
   227  	if written > 0 {
   228  		us.FileOffset += written
   229  		if storeErr := a.Srv().Store.UploadSession().Update(us); storeErr != nil {
   230  			return nil, model.NewAppError("UploadData", "app.upload.upload_data.update.app_error", nil, storeErr.Error(), http.StatusInternalServerError)
   231  		}
   232  	}
   233  	if err != nil {
   234  		return nil, err
   235  	}
   236  
   237  	// upload is incomplete
   238  	if us.FileOffset != us.FileSize {
   239  		return nil, nil
   240  	}
   241  
   242  	// upload is done, create FileInfo
   243  	file, err := a.FileReader(uploadPath)
   244  	if err != nil {
   245  		return nil, model.NewAppError("UploadData", "app.upload.upload_data.read_file.app_error", nil, err.Error(), http.StatusInternalServerError)
   246  	}
   247  
   248  	info, err := model.GetInfoForBytes(us.Filename, file, int(us.FileSize))
   249  	file.Close()
   250  	if err != nil {
   251  		return nil, err
   252  	}
   253  
   254  	info.CreatorId = us.UserId
   255  	info.Path = us.Path
   256  
   257  	// run plugins upload hook
   258  	if err := a.runPluginsHook(info, file); err != nil {
   259  		return nil, err
   260  	}
   261  
   262  	// image post-processing
   263  	if info.IsImage() {
   264  		// Check dimensions before loading the whole thing into memory later on
   265  		// This casting is done to prevent overflow on 32 bit systems (not needed
   266  		// in 64 bits systems because images can't have more than 32 bits height or
   267  		// width)
   268  		if int64(info.Width)*int64(info.Height) > MaxImageSize {
   269  			return nil, model.NewAppError("uploadData", "app.upload.upload_data.large_image.app_error",
   270  				map[string]interface{}{"Filename": us.Filename, "Width": info.Width, "Height": info.Height}, "", http.StatusBadRequest)
   271  		}
   272  		nameWithoutExtension := info.Name[:strings.LastIndex(info.Name, ".")]
   273  		info.PreviewPath = filepath.Dir(info.Path) + "/" + nameWithoutExtension + "_preview.jpg"
   274  		info.ThumbnailPath = filepath.Dir(info.Path) + "/" + nameWithoutExtension + "_thumb.jpg"
   275  		imgData, fileErr := a.ReadFile(uploadPath)
   276  		if fileErr != nil {
   277  			return nil, fileErr
   278  		}
   279  		a.HandleImages([]string{info.PreviewPath}, []string{info.ThumbnailPath}, [][]byte{imgData})
   280  	}
   281  
   282  	if us.Type == model.UploadTypeImport {
   283  		if err := a.MoveFile(uploadPath, us.Path); err != nil {
   284  			return nil, model.NewAppError("UploadData", "app.upload.upload_data.move_file.app_error", nil, err.Error(), http.StatusInternalServerError)
   285  		}
   286  	}
   287  
   288  	var storeErr error
   289  	if info, storeErr = a.Srv().Store.FileInfo().Save(info); storeErr != nil {
   290  		var appErr *model.AppError
   291  		switch {
   292  		case errors.As(storeErr, &appErr):
   293  			return nil, appErr
   294  		default:
   295  			return nil, model.NewAppError("uploadData", "app.upload.upload_data.save.app_error", nil, storeErr.Error(), http.StatusInternalServerError)
   296  		}
   297  	}
   298  
   299  	// delete upload session
   300  	if storeErr := a.Srv().Store.UploadSession().Delete(us.Id); storeErr != nil {
   301  		mlog.Error("Failed to delete UploadSession", mlog.Err(storeErr))
   302  	}
   303  
   304  	return info, nil
   305  }