github.com/gigforks/mattermost-server@v4.9.1-0.20180619094218-800d97fa55d0+incompatible/app/file.go (about)

     1  // Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
     2  // See License.txt for license information.
     3  
     4  package app
     5  
     6  import (
     7  	"bytes"
     8  	"crypto/sha256"
     9  	"encoding/base64"
    10  	"fmt"
    11  	"image"
    12  	"image/color"
    13  	"image/draw"
    14  	_ "image/gif"
    15  	"image/jpeg"
    16  	"io"
    17  	"mime/multipart"
    18  	"net/http"
    19  	"net/url"
    20  	"path/filepath"
    21  	"strings"
    22  	"sync"
    23  	"time"
    24  
    25  	"github.com/disintegration/imaging"
    26  	"github.com/rwcarlsen/goexif/exif"
    27  	_ "golang.org/x/image/bmp"
    28  
    29  	"github.com/mattermost/mattermost-server/mlog"
    30  	"github.com/mattermost/mattermost-server/model"
    31  	"github.com/mattermost/mattermost-server/utils"
    32  )
    33  
    34  const (
    35  	/*
    36  	  EXIF Image Orientations
    37  	  1        2       3      4         5            6           7          8
    38  
    39  	  888888  888888      88  88      8888888888  88                  88  8888888888
    40  	  88          88      88  88      88  88      88  88          88  88      88  88
    41  	  8888      8888    8888  8888    88          8888888888  8888888888          88
    42  	  88          88      88  88
    43  	  88          88  888888  888888
    44  	*/
    45  	Upright            = 1
    46  	UprightMirrored    = 2
    47  	UpsideDown         = 3
    48  	UpsideDownMirrored = 4
    49  	RotatedCWMirrored  = 5
    50  	RotatedCCW         = 6
    51  	RotatedCCWMirrored = 7
    52  	RotatedCW          = 8
    53  
    54  	MaxImageSize                 = 6048 * 4032 // 24 megapixels, roughly 36MB as a raw image
    55  	IMAGE_THUMBNAIL_PIXEL_WIDTH  = 120
    56  	IMAGE_THUMBNAIL_PIXEL_HEIGHT = 100
    57  	IMAGE_PREVIEW_PIXEL_WIDTH    = 1024
    58  )
    59  
    60  func (a *App) FileBackend() (utils.FileBackend, *model.AppError) {
    61  	license := a.License()
    62  	return utils.NewFileBackend(&a.Config().FileSettings, license != nil && *license.Features.Compliance)
    63  }
    64  
    65  func (a *App) ReadFile(path string) ([]byte, *model.AppError) {
    66  	backend, err := a.FileBackend()
    67  	if err != nil {
    68  		return nil, err
    69  	}
    70  	return backend.ReadFile(path)
    71  }
    72  
    73  func (a *App) MoveFile(oldPath, newPath string) *model.AppError {
    74  	backend, err := a.FileBackend()
    75  	if err != nil {
    76  		return err
    77  	}
    78  	return backend.MoveFile(oldPath, newPath)
    79  }
    80  
    81  func (a *App) WriteFile(fr io.Reader, path string) (int64, *model.AppError) {
    82  	backend, err := a.FileBackend()
    83  	if err != nil {
    84  		return 0, err
    85  	}
    86  
    87  	return backend.WriteFile(fr, path)
    88  }
    89  
    90  func (a *App) RemoveFile(path string) *model.AppError {
    91  	backend, err := a.FileBackend()
    92  	if err != nil {
    93  		return err
    94  	}
    95  	return backend.RemoveFile(path)
    96  }
    97  
    98  func (a *App) GetInfoForFilename(post *model.Post, teamId string, filename string) *model.FileInfo {
    99  	// Find the path from the Filename of the form /{channelId}/{userId}/{uid}/{nameWithExtension}
   100  	split := strings.SplitN(filename, "/", 5)
   101  	if len(split) < 5 {
   102  		mlog.Error("Unable to decipher filename when migrating post to use FileInfos", mlog.String("post_id", post.Id), mlog.String("filename", filename))
   103  		return nil
   104  	}
   105  
   106  	channelId := split[1]
   107  	userId := split[2]
   108  	oldId := split[3]
   109  	name, _ := url.QueryUnescape(split[4])
   110  
   111  	if split[0] != "" || split[1] != post.ChannelId || split[2] != post.UserId || strings.Contains(split[4], "/") {
   112  		mlog.Warn(
   113  			"Found an unusual filename when migrating post to use FileInfos",
   114  			mlog.String("post_id", post.Id),
   115  			mlog.String("channel_id", post.ChannelId),
   116  			mlog.String("user_id", post.UserId),
   117  			mlog.String("filename", filename),
   118  		)
   119  	}
   120  
   121  	pathPrefix := fmt.Sprintf("teams/%s/channels/%s/users/%s/%s/", teamId, channelId, userId, oldId)
   122  	path := pathPrefix + name
   123  
   124  	// Open the file and populate the fields of the FileInfo
   125  	var info *model.FileInfo
   126  	if data, err := a.ReadFile(path); err != nil {
   127  		mlog.Error(
   128  			fmt.Sprintf("File not found when migrating post to use FileInfos, err=%v", err),
   129  			mlog.String("post_id", post.Id),
   130  			mlog.String("filename", filename),
   131  			mlog.String("path", path),
   132  		)
   133  		return nil
   134  	} else {
   135  		var err *model.AppError
   136  		info, err = model.GetInfoForBytes(name, data)
   137  		if err != nil {
   138  			mlog.Warn(
   139  				fmt.Sprintf("Unable to fully decode file info when migrating post to use FileInfos, err=%v", err),
   140  				mlog.String("post_id", post.Id),
   141  				mlog.String("filename", filename),
   142  			)
   143  		}
   144  	}
   145  
   146  	// Generate a new ID because with the old system, you could very rarely get multiple posts referencing the same file
   147  	info.Id = model.NewId()
   148  	info.CreatorId = post.UserId
   149  	info.PostId = post.Id
   150  	info.CreateAt = post.CreateAt
   151  	info.UpdateAt = post.UpdateAt
   152  	info.Path = path
   153  
   154  	if info.IsImage() {
   155  		nameWithoutExtension := name[:strings.LastIndex(name, ".")]
   156  		info.PreviewPath = pathPrefix + nameWithoutExtension + "_preview.jpg"
   157  		info.ThumbnailPath = pathPrefix + nameWithoutExtension + "_thumb.jpg"
   158  	}
   159  
   160  	return info
   161  }
   162  
   163  func (a *App) FindTeamIdForFilename(post *model.Post, filename string) string {
   164  	split := strings.SplitN(filename, "/", 5)
   165  	id := split[3]
   166  	name, _ := url.QueryUnescape(split[4])
   167  
   168  	// This post is in a direct channel so we need to figure out what team the files are stored under.
   169  	if result := <-a.Srv.Store.Team().GetTeamsByUserId(post.UserId); result.Err != nil {
   170  		mlog.Error(fmt.Sprintf("Unable to get teams when migrating post to use FileInfo, err=%v", result.Err), mlog.String("post_id", post.Id))
   171  	} else if teams := result.Data.([]*model.Team); len(teams) == 1 {
   172  		// The user has only one team so the post must've been sent from it
   173  		return teams[0].Id
   174  	} else {
   175  		for _, team := range teams {
   176  			path := fmt.Sprintf("teams/%s/channels/%s/users/%s/%s/%s", team.Id, post.ChannelId, post.UserId, id, name)
   177  			if _, err := a.ReadFile(path); err == nil {
   178  				// Found the team that this file was posted from
   179  				return team.Id
   180  			}
   181  		}
   182  	}
   183  
   184  	return ""
   185  }
   186  
   187  var fileMigrationLock sync.Mutex
   188  
   189  // Creates and stores FileInfos for a post created before the FileInfos table existed.
   190  func (a *App) MigrateFilenamesToFileInfos(post *model.Post) []*model.FileInfo {
   191  	if len(post.Filenames) == 0 {
   192  		mlog.Warn("Unable to migrate post to use FileInfos with an empty Filenames field", mlog.String("post_id", post.Id))
   193  		return []*model.FileInfo{}
   194  	}
   195  
   196  	cchan := a.Srv.Store.Channel().Get(post.ChannelId, true)
   197  
   198  	// There's a weird bug that rarely happens where a post ends up with duplicate Filenames so remove those
   199  	filenames := utils.RemoveDuplicatesFromStringArray(post.Filenames)
   200  
   201  	var channel *model.Channel
   202  	if result := <-cchan; result.Err != nil {
   203  		mlog.Error(
   204  			fmt.Sprintf("Unable to get channel when migrating post to use FileInfos, err=%v", result.Err),
   205  			mlog.String("post_id", post.Id),
   206  			mlog.String("channel_id", post.ChannelId),
   207  		)
   208  		return []*model.FileInfo{}
   209  	} else {
   210  		channel = result.Data.(*model.Channel)
   211  	}
   212  
   213  	// Find the team that was used to make this post since its part of the file path that isn't saved in the Filename
   214  	var teamId string
   215  	if channel.TeamId == "" {
   216  		// This post was made in a cross-team DM channel so we need to find where its files were saved
   217  		teamId = a.FindTeamIdForFilename(post, filenames[0])
   218  	} else {
   219  		teamId = channel.TeamId
   220  	}
   221  
   222  	// Create FileInfo objects for this post
   223  	infos := make([]*model.FileInfo, 0, len(filenames))
   224  	if teamId == "" {
   225  		mlog.Error(
   226  			fmt.Sprintf("Unable to find team id for files when migrating post to use FileInfos, filenames=%v", filenames),
   227  			mlog.String("post_id", post.Id),
   228  		)
   229  	} else {
   230  		for _, filename := range filenames {
   231  			info := a.GetInfoForFilename(post, teamId, filename)
   232  			if info == nil {
   233  				continue
   234  			}
   235  
   236  			infos = append(infos, info)
   237  		}
   238  	}
   239  
   240  	// Lock to prevent only one migration thread from trying to update the post at once, preventing duplicate FileInfos from being created
   241  	fileMigrationLock.Lock()
   242  	defer fileMigrationLock.Unlock()
   243  
   244  	if result := <-a.Srv.Store.Post().Get(post.Id); result.Err != nil {
   245  		mlog.Error(fmt.Sprintf("Unable to get post when migrating post to use FileInfos, err=%v", result.Err), mlog.String("post_id", post.Id))
   246  		return []*model.FileInfo{}
   247  	} else if newPost := result.Data.(*model.PostList).Posts[post.Id]; len(newPost.Filenames) != len(post.Filenames) {
   248  		// Another thread has already created FileInfos for this post, so just return those
   249  		if result := <-a.Srv.Store.FileInfo().GetForPost(post.Id, true, false); result.Err != nil {
   250  			mlog.Error(fmt.Sprintf("Unable to get FileInfos for migrated post, err=%v", result.Err), mlog.String("post_id", post.Id))
   251  			return []*model.FileInfo{}
   252  		} else {
   253  			mlog.Debug("Post already migrated to use FileInfos", mlog.String("post_id", post.Id))
   254  			return result.Data.([]*model.FileInfo)
   255  		}
   256  	}
   257  
   258  	mlog.Debug("Migrating post to use FileInfos", mlog.String("post_id", post.Id))
   259  
   260  	savedInfos := make([]*model.FileInfo, 0, len(infos))
   261  	fileIds := make([]string, 0, len(filenames))
   262  	for _, info := range infos {
   263  		if result := <-a.Srv.Store.FileInfo().Save(info); result.Err != nil {
   264  			mlog.Error(
   265  				fmt.Sprintf("Unable to save file info when migrating post to use FileInfos, err=%v", result.Err),
   266  				mlog.String("post_id", post.Id),
   267  				mlog.String("file_info_id", info.Id),
   268  				mlog.String("file_info_path", info.Path),
   269  			)
   270  			continue
   271  		}
   272  
   273  		savedInfos = append(savedInfos, info)
   274  		fileIds = append(fileIds, info.Id)
   275  	}
   276  
   277  	// Copy and save the updated post
   278  	newPost := &model.Post{}
   279  	*newPost = *post
   280  
   281  	newPost.Filenames = []string{}
   282  	newPost.FileIds = fileIds
   283  
   284  	// Update Posts to clear Filenames and set FileIds
   285  	if result := <-a.Srv.Store.Post().Update(newPost, post); result.Err != nil {
   286  		mlog.Error(fmt.Sprintf("Unable to save migrated post when migrating to use FileInfos, new_file_ids=%v, old_filenames=%v, err=%v", newPost.FileIds, post.Filenames, result.Err), mlog.String("post_id", post.Id))
   287  		return []*model.FileInfo{}
   288  	} else {
   289  		return savedInfos
   290  	}
   291  }
   292  
   293  func (a *App) GeneratePublicLink(siteURL string, info *model.FileInfo) string {
   294  	hash := GeneratePublicLinkHash(info.Id, *a.Config().FileSettings.PublicLinkSalt)
   295  	return fmt.Sprintf("%s/files/%v/public?h=%s", siteURL, info.Id, hash)
   296  }
   297  
   298  func (a *App) GeneratePublicLinkV3(siteURL string, info *model.FileInfo) string {
   299  	hash := GeneratePublicLinkHash(info.Id, *a.Config().FileSettings.PublicLinkSalt)
   300  	return fmt.Sprintf("%s%s/public/files/%v/get?h=%s", siteURL, model.API_URL_SUFFIX_V3, info.Id, hash)
   301  }
   302  
   303  func GeneratePublicLinkHash(fileId, salt string) string {
   304  	hash := sha256.New()
   305  	hash.Write([]byte(salt))
   306  	hash.Write([]byte(fileId))
   307  
   308  	return base64.RawURLEncoding.EncodeToString(hash.Sum(nil))
   309  }
   310  
   311  func (a *App) UploadMultipartFiles(teamId string, channelId string, userId string, fileHeaders []*multipart.FileHeader, clientIds []string) (*model.FileUploadResponse, *model.AppError) {
   312  	files := make([]io.ReadCloser, len(fileHeaders))
   313  	filenames := make([]string, len(fileHeaders))
   314  
   315  	for i, fileHeader := range fileHeaders {
   316  		file, fileErr := fileHeader.Open()
   317  		if fileErr != nil {
   318  			return nil, model.NewAppError("UploadFiles", "api.file.upload_file.bad_parse.app_error", nil, fileErr.Error(), http.StatusBadRequest)
   319  		}
   320  
   321  		// Will be closed after UploadFiles returns
   322  		defer file.Close()
   323  
   324  		files[i] = file
   325  		filenames[i] = fileHeader.Filename
   326  	}
   327  
   328  	return a.UploadFiles(teamId, channelId, userId, files, filenames, clientIds)
   329  }
   330  
   331  // Uploads some files to the given team and channel as the given user. files and filenames should have
   332  // the same length. clientIds should either not be provided or have the same length as files and filenames.
   333  // The provided files should be closed by the caller so that they are not leaked.
   334  func (a *App) UploadFiles(teamId string, channelId string, userId string, files []io.ReadCloser, filenames []string, clientIds []string) (*model.FileUploadResponse, *model.AppError) {
   335  	if len(*a.Config().FileSettings.DriverName) == 0 {
   336  		return nil, model.NewAppError("uploadFile", "api.file.upload_file.storage.app_error", nil, "", http.StatusNotImplemented)
   337  	}
   338  
   339  	if len(filenames) != len(files) || (len(clientIds) > 0 && len(clientIds) != len(files)) {
   340  		return nil, model.NewAppError("UploadFiles", "api.file.upload_file.incorrect_number_of_files.app_error", nil, "", http.StatusBadRequest)
   341  	}
   342  
   343  	resStruct := &model.FileUploadResponse{
   344  		FileInfos: []*model.FileInfo{},
   345  		ClientIds: []string{},
   346  	}
   347  
   348  	previewPathList := []string{}
   349  	thumbnailPathList := []string{}
   350  	imageDataList := [][]byte{}
   351  
   352  	for i, file := range files {
   353  		buf := bytes.NewBuffer(nil)
   354  		io.Copy(buf, file)
   355  		data := buf.Bytes()
   356  
   357  		info, err := a.DoUploadFile(time.Now(), teamId, channelId, userId, filenames[i], data)
   358  		if err != nil {
   359  			return nil, err
   360  		}
   361  
   362  		if info.PreviewPath != "" || info.ThumbnailPath != "" {
   363  			previewPathList = append(previewPathList, info.PreviewPath)
   364  			thumbnailPathList = append(thumbnailPathList, info.ThumbnailPath)
   365  			imageDataList = append(imageDataList, data)
   366  		}
   367  
   368  		resStruct.FileInfos = append(resStruct.FileInfos, info)
   369  
   370  		if len(clientIds) > 0 {
   371  			resStruct.ClientIds = append(resStruct.ClientIds, clientIds[i])
   372  		}
   373  	}
   374  
   375  	a.HandleImages(previewPathList, thumbnailPathList, imageDataList)
   376  
   377  	return resStruct, nil
   378  }
   379  
   380  func (a *App) DoUploadFile(now time.Time, rawTeamId string, rawChannelId string, rawUserId string, rawFilename string, data []byte) (*model.FileInfo, *model.AppError) {
   381  	filename := filepath.Base(rawFilename)
   382  	teamId := filepath.Base(rawTeamId)
   383  	channelId := filepath.Base(rawChannelId)
   384  	userId := filepath.Base(rawUserId)
   385  
   386  	info, err := model.GetInfoForBytes(filename, data)
   387  	if err != nil {
   388  		err.StatusCode = http.StatusBadRequest
   389  		return nil, err
   390  	}
   391  
   392  	if orientation, err := getImageOrientation(bytes.NewReader(data)); err == nil &&
   393  		(orientation == RotatedCWMirrored ||
   394  			orientation == RotatedCCW ||
   395  			orientation == RotatedCCWMirrored ||
   396  			orientation == RotatedCW) {
   397  		info.Width, info.Height = info.Height, info.Width
   398  	}
   399  
   400  	info.Id = model.NewId()
   401  	info.CreatorId = userId
   402  
   403  	pathPrefix := now.Format("20060102") + "/teams/" + teamId + "/channels/" + channelId + "/users/" + userId + "/" + info.Id + "/"
   404  	info.Path = pathPrefix + filename
   405  
   406  	if info.IsImage() {
   407  		// Check dimensions before loading the whole thing into memory later on
   408  		if info.Width*info.Height > MaxImageSize {
   409  			err := model.NewAppError("uploadFile", "api.file.upload_file.large_image.app_error", map[string]interface{}{"Filename": filename}, "", http.StatusBadRequest)
   410  			return nil, err
   411  		}
   412  
   413  		nameWithoutExtension := filename[:strings.LastIndex(filename, ".")]
   414  		info.PreviewPath = pathPrefix + nameWithoutExtension + "_preview.jpg"
   415  		info.ThumbnailPath = pathPrefix + nameWithoutExtension + "_thumb.jpg"
   416  	}
   417  
   418  	if _, err := a.WriteFile(bytes.NewReader(data), info.Path); err != nil {
   419  		return nil, err
   420  	}
   421  
   422  	if result := <-a.Srv.Store.FileInfo().Save(info); result.Err != nil {
   423  		return nil, result.Err
   424  	}
   425  
   426  	return info, nil
   427  }
   428  
   429  func (a *App) HandleImages(previewPathList []string, thumbnailPathList []string, fileData [][]byte) {
   430  	wg := new(sync.WaitGroup)
   431  
   432  	for i := range fileData {
   433  		img, width, height := prepareImage(fileData[i])
   434  		if img != nil {
   435  			wg.Add(2)
   436  			go func(img *image.Image, path string, width int, height int) {
   437  				defer wg.Done()
   438  				a.generateThumbnailImage(*img, path, width, height)
   439  			}(img, thumbnailPathList[i], width, height)
   440  
   441  			go func(img *image.Image, path string, width int) {
   442  				defer wg.Done()
   443  				a.generatePreviewImage(*img, path, width)
   444  			}(img, previewPathList[i], width)
   445  		}
   446  	}
   447  	wg.Wait()
   448  }
   449  
   450  func prepareImage(fileData []byte) (*image.Image, int, int) {
   451  	// Decode image bytes into Image object
   452  	img, imgType, err := image.Decode(bytes.NewReader(fileData))
   453  	if err != nil {
   454  		mlog.Error(fmt.Sprintf("Unable to decode image err=%v", err))
   455  		return nil, 0, 0
   456  	}
   457  
   458  	width := img.Bounds().Dx()
   459  	height := img.Bounds().Dy()
   460  
   461  	// Fill in the background of a potentially-transparent png file as white
   462  	if imgType == "png" {
   463  		dst := image.NewRGBA(img.Bounds())
   464  		draw.Draw(dst, dst.Bounds(), image.NewUniform(color.White), image.Point{}, draw.Src)
   465  		draw.Draw(dst, dst.Bounds(), img, img.Bounds().Min, draw.Over)
   466  		img = dst
   467  	}
   468  
   469  	// Flip the image to be upright
   470  	orientation, _ := getImageOrientation(bytes.NewReader(fileData))
   471  	img = makeImageUpright(img, orientation)
   472  
   473  	return &img, width, height
   474  }
   475  
   476  func makeImageUpright(img image.Image, orientation int) image.Image {
   477  	switch orientation {
   478  	case UprightMirrored:
   479  		return imaging.FlipH(img)
   480  	case UpsideDown:
   481  		return imaging.Rotate180(img)
   482  	case UpsideDownMirrored:
   483  		return imaging.FlipV(img)
   484  	case RotatedCWMirrored:
   485  		return imaging.Transpose(img)
   486  	case RotatedCCW:
   487  		return imaging.Rotate270(img)
   488  	case RotatedCCWMirrored:
   489  		return imaging.Transverse(img)
   490  	case RotatedCW:
   491  		return imaging.Rotate90(img)
   492  	default:
   493  		return img
   494  	}
   495  }
   496  
   497  func getImageOrientation(input io.Reader) (int, error) {
   498  	if exifData, err := exif.Decode(input); err != nil {
   499  		return Upright, err
   500  	} else {
   501  		if tag, err := exifData.Get("Orientation"); err != nil {
   502  			return Upright, err
   503  		} else {
   504  			orientation, err := tag.Int(0)
   505  			if err != nil {
   506  				return Upright, err
   507  			} else {
   508  				return orientation, nil
   509  			}
   510  		}
   511  	}
   512  }
   513  
   514  func (a *App) generateThumbnailImage(img image.Image, thumbnailPath string, width int, height int) {
   515  	thumbWidth := float64(IMAGE_THUMBNAIL_PIXEL_WIDTH)
   516  	thumbHeight := float64(IMAGE_THUMBNAIL_PIXEL_HEIGHT)
   517  	imgWidth := float64(width)
   518  	imgHeight := float64(height)
   519  
   520  	var thumbnail image.Image
   521  	if imgHeight < IMAGE_THUMBNAIL_PIXEL_HEIGHT && imgWidth < thumbWidth {
   522  		thumbnail = img
   523  	} else if imgHeight/imgWidth < thumbHeight/thumbWidth {
   524  		thumbnail = imaging.Resize(img, 0, IMAGE_THUMBNAIL_PIXEL_HEIGHT, imaging.Lanczos)
   525  	} else {
   526  		thumbnail = imaging.Resize(img, IMAGE_THUMBNAIL_PIXEL_WIDTH, 0, imaging.Lanczos)
   527  	}
   528  
   529  	buf := new(bytes.Buffer)
   530  	if err := jpeg.Encode(buf, thumbnail, &jpeg.Options{Quality: 90}); err != nil {
   531  		mlog.Error(fmt.Sprintf("Unable to encode image as jpeg path=%v err=%v", thumbnailPath, err))
   532  		return
   533  	}
   534  
   535  	if _, err := a.WriteFile(buf, thumbnailPath); err != nil {
   536  		mlog.Error(fmt.Sprintf("Unable to upload thumbnail path=%v err=%v", thumbnailPath, err))
   537  		return
   538  	}
   539  }
   540  
   541  func (a *App) generatePreviewImage(img image.Image, previewPath string, width int) {
   542  	var preview image.Image
   543  
   544  	if width > IMAGE_PREVIEW_PIXEL_WIDTH {
   545  		preview = imaging.Resize(img, IMAGE_PREVIEW_PIXEL_WIDTH, 0, imaging.Lanczos)
   546  	} else {
   547  		preview = img
   548  	}
   549  
   550  	buf := new(bytes.Buffer)
   551  
   552  	if err := jpeg.Encode(buf, preview, &jpeg.Options{Quality: 90}); err != nil {
   553  		mlog.Error(fmt.Sprintf("Unable to encode image as preview jpg err=%v", err), mlog.String("path", previewPath))
   554  		return
   555  	}
   556  
   557  	if _, err := a.WriteFile(buf, previewPath); err != nil {
   558  		mlog.Error(fmt.Sprintf("Unable to upload preview err=%v", err), mlog.String("path", previewPath))
   559  		return
   560  	}
   561  }
   562  
   563  func (a *App) GetFileInfo(fileId string) (*model.FileInfo, *model.AppError) {
   564  	if result := <-a.Srv.Store.FileInfo().Get(fileId); result.Err != nil {
   565  		return nil, result.Err
   566  	} else {
   567  		return result.Data.(*model.FileInfo), nil
   568  	}
   569  }