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