file.go 20.3 KB
Newer Older
1
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
2 3 4 5 6 7 8 9 10
// See License.txt for license information.

package app

import (
	"bytes"
	"crypto/sha256"
	"encoding/base64"
	"fmt"
11 12 13
	"image"
	"image/color"
	"image/draw"
14
	_ "image/gif"
15
	"image/jpeg"
16
	"io"
17
	"mime/multipart"
18
	"net/http"
19 20 21 22
	"net/url"
	"path/filepath"
	"strings"
	"sync"
23
	"time"
24

25
	"github.com/disintegration/imaging"
26 27
	"github.com/rwcarlsen/goexif/exif"
	_ "golang.org/x/image/bmp"
28

29
	"github.com/mattermost/mattermost-server/mlog"
Christopher Speller's avatar
Christopher Speller committed
30
	"github.com/mattermost/mattermost-server/model"
31
	"github.com/mattermost/mattermost-server/plugin"
32
	"github.com/mattermost/mattermost-server/services/filesstore"
Christopher Speller's avatar
Christopher Speller committed
33
	"github.com/mattermost/mattermost-server/utils"
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
)

const (
	/*
	  EXIF Image Orientations
	  1        2       3      4         5            6           7          8

	  888888  888888      88  88      8888888888  88                  88  8888888888
	  88          88      88  88      88  88      88  88          88  88      88  88
	  8888      8888    8888  8888    88          8888888888  8888888888          88
	  88          88      88  88
	  88          88  888888  888888
	*/
	Upright            = 1
	UprightMirrored    = 2
	UpsideDown         = 3
	UpsideDownMirrored = 4
	RotatedCWMirrored  = 5
	RotatedCCW         = 6
	RotatedCCWMirrored = 7
	RotatedCW          = 8

56 57
	MaxImageSize                 = 6048 * 4032 // 24 megapixels, roughly 36MB as a raw image
	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 68 69 70 71 72 73 74
}

func (a *App) ReadFile(path string) ([]byte, *model.AppError) {
	backend, err := a.FileBackend()
	if err != nil {
		return nil, err
	}
	return backend.ReadFile(path)
}

75 76
// Caller must close the first return value
func (a *App) FileReader(path string) (io.ReadCloser, *model.AppError) {
77 78 79 80 81 82 83
	backend, err := a.FileBackend()
	if err != nil {
		return nil, err
	}
	return backend.Reader(path)
}

84 85 86 87 88 89 90 91
func (a *App) FileExists(path string) (bool, *model.AppError) {
	backend, err := a.FileBackend()
	if err != nil {
		return false, err
	}
	return backend.FileExists(path)
}

92 93 94 95 96 97 98 99
func (a *App) MoveFile(oldPath, newPath string) *model.AppError {
	backend, err := a.FileBackend()
	if err != nil {
		return err
	}
	return backend.MoveFile(oldPath, newPath)
}

100
func (a *App) WriteFile(fr io.Reader, path string) (int64, *model.AppError) {
101 102
	backend, err := a.FileBackend()
	if err != nil {
103
		return 0, err
104
	}
105 106

	return backend.WriteFile(fr, path)
107 108 109 110 111 112 113 114 115 116 117
}

func (a *App) RemoveFile(path string) *model.AppError {
	backend, err := a.FileBackend()
	if err != nil {
		return err
	}
	return backend.RemoveFile(path)
}

func (a *App) GetInfoForFilename(post *model.Post, teamId string, filename string) *model.FileInfo {
118 119 120
	// Find the path from the Filename of the form /{channelId}/{userId}/{uid}/{nameWithExtension}
	split := strings.SplitN(filename, "/", 5)
	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 123 124 125 126 127 128 129 130
		return nil
	}

	channelId := split[1]
	userId := split[2]
	oldId := split[3]
	name, _ := url.QueryUnescape(split[4])

	if split[0] != "" || split[1] != post.ChannelId || split[2] != post.UserId || strings.Contains(split[4], "/") {
131 132 133 134 135 136 137
		mlog.Warn(
			"Found an unusual filename when migrating post to use FileInfos",
			mlog.String("post_id", post.Id),
			mlog.String("channel_id", post.ChannelId),
			mlog.String("user_id", post.UserId),
			mlog.String("filename", filename),
		)
138 139 140 141 142 143 144
	}

	pathPrefix := fmt.Sprintf("teams/%s/channels/%s/users/%s/%s/", teamId, channelId, userId, oldId)
	path := pathPrefix + name

	// Open the file and populate the fields of the FileInfo
	var info *model.FileInfo
145
	if data, err := a.ReadFile(path); err != nil {
146 147 148 149 150 151
		mlog.Error(
			fmt.Sprintf("File not found when migrating post to use FileInfos, err=%v", err),
			mlog.String("post_id", post.Id),
			mlog.String("filename", filename),
			mlog.String("path", path),
		)
152 153 154 155 156
		return nil
	} else {
		var err *model.AppError
		info, err = model.GetInfoForBytes(name, data)
		if err != nil {
157 158 159 160 161
			mlog.Warn(
				fmt.Sprintf("Unable to fully decode file info when migrating post to use FileInfos, err=%v", err),
				mlog.String("post_id", post.Id),
				mlog.String("filename", filename),
			)
162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181
		}
	}

	// Generate a new ID because with the old system, you could very rarely get multiple posts referencing the same file
	info.Id = model.NewId()
	info.CreatorId = post.UserId
	info.PostId = post.Id
	info.CreateAt = post.CreateAt
	info.UpdateAt = post.UpdateAt
	info.Path = path

	if info.IsImage() {
		nameWithoutExtension := name[:strings.LastIndex(name, ".")]
		info.PreviewPath = pathPrefix + nameWithoutExtension + "_preview.jpg"
		info.ThumbnailPath = pathPrefix + nameWithoutExtension + "_thumb.jpg"
	}

	return info
}

Chris's avatar
Chris committed
182
func (a *App) FindTeamIdForFilename(post *model.Post, filename string) string {
183 184 185 186 187
	split := strings.SplitN(filename, "/", 5)
	id := split[3]
	name, _ := url.QueryUnescape(split[4])

	// This post is in a direct channel so we need to figure out what team the files are stored under.
Chris's avatar
Chris committed
188
	if result := <-a.Srv.Store.Team().GetTeamsByUserId(post.UserId); 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 191 192 193 194 195
	} else if teams := result.Data.([]*model.Team); len(teams) == 1 {
		// The user has only one team so the post must've been sent from it
		return teams[0].Id
	} else {
		for _, team := range teams {
			path := fmt.Sprintf("teams/%s/channels/%s/users/%s/%s/%s", team.Id, post.ChannelId, post.UserId, id, name)
196
			if _, err := a.ReadFile(path); err == nil {
197 198 199 200 201 202 203 204 205 206 207 208
				// Found the team that this file was posted from
				return team.Id
			}
		}
	}

	return ""
}

var fileMigrationLock sync.Mutex

// Creates and stores FileInfos for a post created before the FileInfos table existed.
Chris's avatar
Chris committed
209
func (a *App) MigrateFilenamesToFileInfos(post *model.Post) []*model.FileInfo {
210
	if len(post.Filenames) == 0 {
211
		mlog.Warn("Unable to migrate post to use FileInfos with an empty Filenames field", mlog.String("post_id", post.Id))
212 213 214
		return []*model.FileInfo{}
	}

Chris's avatar
Chris committed
215
	cchan := a.Srv.Store.Channel().Get(post.ChannelId, true)
216 217 218 219 220 221

	// There's a weird bug that rarely happens where a post ends up with duplicate Filenames so remove those
	filenames := utils.RemoveDuplicatesFromStringArray(post.Filenames)

	var channel *model.Channel
	if result := <-cchan; result.Err != nil {
222 223 224 225 226
		mlog.Error(
			fmt.Sprintf("Unable to get channel when migrating post to use FileInfos, err=%v", result.Err),
			mlog.String("post_id", post.Id),
			mlog.String("channel_id", post.ChannelId),
		)
227 228 229 230 231 232 233 234 235
		return []*model.FileInfo{}
	} else {
		channel = result.Data.(*model.Channel)
	}

	// Find the team that was used to make this post since its part of the file path that isn't saved in the Filename
	var teamId string
	if channel.TeamId == "" {
		// This post was made in a cross-team DM channel so we need to find where its files were saved
Chris's avatar
Chris committed
236
		teamId = a.FindTeamIdForFilename(post, filenames[0])
237 238 239 240 241 242 243
	} else {
		teamId = channel.TeamId
	}

	// Create FileInfo objects for this post
	infos := make([]*model.FileInfo, 0, len(filenames))
	if teamId == "" {
244 245 246 247
		mlog.Error(
			fmt.Sprintf("Unable to find team id for files when migrating post to use FileInfos, filenames=%v", filenames),
			mlog.String("post_id", post.Id),
		)
248 249
	} else {
		for _, filename := range filenames {
250
			info := a.GetInfoForFilename(post, teamId, filename)
251 252 253 254 255 256 257 258 259 260 261 262
			if info == nil {
				continue
			}

			infos = append(infos, info)
		}
	}

	// Lock to prevent only one migration thread from trying to update the post at once, preventing duplicate FileInfos from being created
	fileMigrationLock.Lock()
	defer fileMigrationLock.Unlock()

Chris's avatar
Chris committed
263
	if result := <-a.Srv.Store.Post().Get(post.Id); result.Err != nil {
264
		mlog.Error(fmt.Sprintf("Unable to get post when migrating post to use FileInfos, err=%v", result.Err), mlog.String("post_id", post.Id))
265 266 267
		return []*model.FileInfo{}
	} else if newPost := result.Data.(*model.PostList).Posts[post.Id]; len(newPost.Filenames) != len(post.Filenames) {
		// Another thread has already created FileInfos for this post, so just return those
Chris's avatar
Chris committed
268
		if result := <-a.Srv.Store.FileInfo().GetForPost(post.Id, true, false); result.Err != nil {
269
			mlog.Error(fmt.Sprintf("Unable to get FileInfos for migrated post, err=%v", result.Err), mlog.String("post_id", post.Id))
270 271
			return []*model.FileInfo{}
		} else {
272
			mlog.Debug("Post already migrated to use FileInfos", mlog.String("post_id", post.Id))
273 274 275 276
			return result.Data.([]*model.FileInfo)
		}
	}

277
	mlog.Debug("Migrating post to use FileInfos", mlog.String("post_id", post.Id))
278 279 280 281

	savedInfos := make([]*model.FileInfo, 0, len(infos))
	fileIds := make([]string, 0, len(filenames))
	for _, info := range infos {
Chris's avatar
Chris committed
282
		if result := <-a.Srv.Store.FileInfo().Save(info); result.Err != nil {
283 284 285 286 287 288
			mlog.Error(
				fmt.Sprintf("Unable to save file info when migrating post to use FileInfos, err=%v", result.Err),
				mlog.String("post_id", post.Id),
				mlog.String("file_info_id", info.Id),
				mlog.String("file_info_path", info.Path),
			)
289 290 291 292 293 294 295 296 297 298 299 300 301 302 303
			continue
		}

		savedInfos = append(savedInfos, info)
		fileIds = append(fileIds, info.Id)
	}

	// Copy and save the updated post
	newPost := &model.Post{}
	*newPost = *post

	newPost.Filenames = []string{}
	newPost.FileIds = fileIds

	// Update Posts to clear Filenames and set FileIds
Chris's avatar
Chris committed
304
	if result := <-a.Srv.Store.Post().Update(newPost, post); result.Err != nil {
305
		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))
306 307 308 309 310 311
		return []*model.FileInfo{}
	} else {
		return savedInfos
	}
}

Chris's avatar
Chris committed
312 313
func (a *App) GeneratePublicLink(siteURL string, info *model.FileInfo) string {
	hash := GeneratePublicLinkHash(info.Id, *a.Config().FileSettings.PublicLinkSalt)
314 315 316
	return fmt.Sprintf("%s/files/%v/public?h=%s", siteURL, info.Id, hash)
}

317 318 319 320 321 322 323
func GeneratePublicLinkHash(fileId, salt string) string {
	hash := sha256.New()
	hash.Write([]byte(salt))
	hash.Write([]byte(fileId))

	return base64.RawURLEncoding.EncodeToString(hash.Sum(nil))
}
324

325
func (a *App) UploadMultipartFiles(teamId string, channelId string, userId string, fileHeaders []*multipart.FileHeader, clientIds []string, now time.Time) (*model.FileUploadResponse, *model.AppError) {
326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341
	files := make([]io.ReadCloser, len(fileHeaders))
	filenames := make([]string, len(fileHeaders))

	for i, fileHeader := range fileHeaders {
		file, fileErr := fileHeader.Open()
		if fileErr != nil {
			return nil, model.NewAppError("UploadFiles", "api.file.upload_file.bad_parse.app_error", nil, fileErr.Error(), http.StatusBadRequest)
		}

		// Will be closed after UploadFiles returns
		defer file.Close()

		files[i] = file
		filenames[i] = fileHeader.Filename
	}

342
	return a.UploadFiles(teamId, channelId, userId, files, filenames, clientIds, now)
343 344 345 346 347
}

// Uploads some files to the given team and channel as the given user. files and filenames should have
// the same length. clientIds should either not be provided or have the same length as files and filenames.
// The provided files should be closed by the caller so that they are not leaked.
348
func (a *App) UploadFiles(teamId string, channelId string, userId string, files []io.ReadCloser, filenames []string, clientIds []string, now time.Time) (*model.FileUploadResponse, *model.AppError) {
Chris's avatar
Chris committed
349
	if len(*a.Config().FileSettings.DriverName) == 0 {
350 351 352
		return nil, model.NewAppError("uploadFile", "api.file.upload_file.storage.app_error", nil, "", http.StatusNotImplemented)
	}

353 354 355 356
	if len(filenames) != len(files) || (len(clientIds) > 0 && len(clientIds) != len(files)) {
		return nil, model.NewAppError("UploadFiles", "api.file.upload_file.incorrect_number_of_files.app_error", nil, "", http.StatusBadRequest)
	}

357 358 359 360 361 362 363 364 365
	resStruct := &model.FileUploadResponse{
		FileInfos: []*model.FileInfo{},
		ClientIds: []string{},
	}

	previewPathList := []string{}
	thumbnailPathList := []string{}
	imageDataList := [][]byte{}

366
	for i, file := range files {
367 368 369 370
		buf := bytes.NewBuffer(nil)
		io.Copy(buf, file)
		data := buf.Bytes()

371
		info, data, err := a.DoUploadFileExpectModification(now, teamId, channelId, userId, filenames[i], data)
372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388
		if err != nil {
			return nil, err
		}

		if info.PreviewPath != "" || info.ThumbnailPath != "" {
			previewPathList = append(previewPathList, info.PreviewPath)
			thumbnailPathList = append(thumbnailPathList, info.ThumbnailPath)
			imageDataList = append(imageDataList, data)
		}

		resStruct.FileInfos = append(resStruct.FileInfos, info)

		if len(clientIds) > 0 {
			resStruct.ClientIds = append(resStruct.ClientIds, clientIds[i])
		}
	}

389
	a.HandleImages(previewPathList, thumbnailPathList, imageDataList)
390 391 392 393

	return resStruct, nil
}

394
func (a *App) DoUploadFile(now time.Time, rawTeamId string, rawChannelId string, rawUserId string, rawFilename string, data []byte) (*model.FileInfo, *model.AppError) {
395 396 397 398 399
	info, _, err := a.DoUploadFileExpectModification(now, rawTeamId, rawChannelId, rawUserId, rawFilename, data)
	return info, err
}

func (a *App) DoUploadFileExpectModification(now time.Time, rawTeamId string, rawChannelId string, rawUserId string, rawFilename string, data []byte) (*model.FileInfo, []byte, *model.AppError) {
400
	filename := filepath.Base(rawFilename)
401 402 403
	teamId := filepath.Base(rawTeamId)
	channelId := filepath.Base(rawChannelId)
	userId := filepath.Base(rawUserId)
404 405 406 407

	info, err := model.GetInfoForBytes(filename, data)
	if err != nil {
		err.StatusCode = http.StatusBadRequest
408
		return nil, data, err
409 410
	}

411 412 413 414 415 416 417 418
	if orientation, err := getImageOrientation(bytes.NewReader(data)); err == nil &&
		(orientation == RotatedCWMirrored ||
			orientation == RotatedCCW ||
			orientation == RotatedCCWMirrored ||
			orientation == RotatedCW) {
		info.Width, info.Height = info.Height, info.Width
	}

419 420
	info.Id = model.NewId()
	info.CreatorId = userId
421
	info.CreateAt = now.UnixNano() / int64(time.Millisecond)
422

423
	pathPrefix := now.Format("20060102") + "/teams/" + teamId + "/channels/" + channelId + "/users/" + userId + "/" + info.Id + "/"
424 425 426 427 428
	info.Path = pathPrefix + filename

	if info.IsImage() {
		// Check dimensions before loading the whole thing into memory later on
		if info.Width*info.Height > MaxImageSize {
429
			err := model.NewAppError("uploadFile", "api.file.upload_file.large_image.app_error", map[string]interface{}{"Filename": filename}, "", http.StatusBadRequest)
430
			return nil, data, err
431 432 433 434 435 436 437
		}

		nameWithoutExtension := filename[:strings.LastIndex(filename, ".")]
		info.PreviewPath = pathPrefix + nameWithoutExtension + "_preview.jpg"
		info.ThumbnailPath = pathPrefix + nameWithoutExtension + "_thumb.jpg"
	}

438
	if a.PluginsReady() {
439
		var rejectionError *model.AppError
440 441 442
		pluginContext := &plugin.Context{}
		a.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
			var newBytes bytes.Buffer
443 444 445 446 447 448 449 450 451
			replacementInfo, rejectionReason := hooks.FileWillBeUploaded(pluginContext, info, bytes.NewReader(data), &newBytes)
			if rejectionReason != "" {
				rejectionError = model.NewAppError("DoUploadFile", "File rejected by plugin. "+rejectionReason, nil, "", http.StatusBadRequest)
				return false
			}
			if replacementInfo != nil {
				info = replacementInfo
			}
			if newBytes.Len() != 0 {
452 453 454
				data = newBytes.Bytes()
				info.Size = int64(len(data))
			}
455 456

			return true
457
		}, plugin.FileWillBeUploadedId)
458 459
		if rejectionError != nil {
			return nil, data, rejectionError
460 461 462
		}
	}

463
	if _, err := a.WriteFile(bytes.NewReader(data), info.Path); err != nil {
464
		return nil, data, err
465 466
	}

Chris's avatar
Chris committed
467
	if result := <-a.Srv.Store.FileInfo().Save(info); result.Err != nil {
468
		return nil, data, result.Err
469 470
	}

471
	return info, data, nil
472 473
}

474
func (a *App) HandleImages(previewPathList []string, thumbnailPathList []string, fileData [][]byte) {
475 476 477 478 479 480 481 482
	wg := new(sync.WaitGroup)

	for i := range fileData {
		img, width, height := prepareImage(fileData[i])
		if img != nil {
			wg.Add(2)
			go func(img *image.Image, path string, width int, height int) {
				defer wg.Done()
483
				a.generateThumbnailImage(*img, path, width, height)
484
			}(img, thumbnailPathList[i], width, height)
485 486 487

			go func(img *image.Image, path string, width int) {
				defer wg.Done()
488
				a.generatePreviewImage(*img, path, width)
489 490
			}(img, previewPathList[i], width)
		}
491
	}
492
	wg.Wait()
493 494 495 496 497 498
}

func prepareImage(fileData []byte) (*image.Image, int, int) {
	// Decode image bytes into Image object
	img, imgType, err := image.Decode(bytes.NewReader(fileData))
	if err != nil {
499
		mlog.Error(fmt.Sprintf("Unable to decode image err=%v", err))
500 501 502 503 504 505 506 507 508 509 510 511 512 513 514
		return nil, 0, 0
	}

	width := img.Bounds().Dx()
	height := img.Bounds().Dy()

	// Fill in the background of a potentially-transparent png file as white
	if imgType == "png" {
		dst := image.NewRGBA(img.Bounds())
		draw.Draw(dst, dst.Bounds(), image.NewUniform(color.White), image.Point{}, draw.Src)
		draw.Draw(dst, dst.Bounds(), img, img.Bounds().Min, draw.Over)
		img = dst
	}

	// Flip the image to be upright
515 516
	orientation, _ := getImageOrientation(bytes.NewReader(fileData))
	img = makeImageUpright(img, orientation)
517

518 519 520 521
	return &img, width, height
}

func makeImageUpright(img image.Image, orientation int) image.Image {
522 523
	switch orientation {
	case UprightMirrored:
524
		return imaging.FlipH(img)
525
	case UpsideDown:
526
		return imaging.Rotate180(img)
527
	case UpsideDownMirrored:
528
		return imaging.FlipV(img)
529
	case RotatedCWMirrored:
530
		return imaging.Transpose(img)
531
	case RotatedCCW:
532
		return imaging.Rotate270(img)
533
	case RotatedCCWMirrored:
534
		return imaging.Transverse(img)
535
	case RotatedCW:
536 537 538
		return imaging.Rotate90(img)
	default:
		return img
539 540 541
	}
}

542 543
func getImageOrientation(input io.Reader) (int, error) {
	if exifData, err := exif.Decode(input); err != nil {
544 545 546 547 548 549 550 551 552 553 554 555 556 557 558
		return Upright, err
	} else {
		if tag, err := exifData.Get("Orientation"); err != nil {
			return Upright, err
		} else {
			orientation, err := tag.Int(0)
			if err != nil {
				return Upright, err
			} else {
				return orientation, nil
			}
		}
	}
}

559
func (a *App) generateThumbnailImage(img image.Image, thumbnailPath string, width int, height int) {
560 561 562 563 564
	thumbWidth := float64(IMAGE_THUMBNAIL_PIXEL_WIDTH)
	thumbHeight := float64(IMAGE_THUMBNAIL_PIXEL_HEIGHT)
	imgWidth := float64(width)
	imgHeight := float64(height)

565
	var thumbnail image.Image
566
	if imgHeight < IMAGE_THUMBNAIL_PIXEL_HEIGHT && imgWidth < thumbWidth {
567
		thumbnail = img
568 569
	} else if imgHeight/imgWidth < thumbHeight/thumbWidth {
		thumbnail = imaging.Resize(img, 0, IMAGE_THUMBNAIL_PIXEL_HEIGHT, imaging.Lanczos)
570
	} else {
571
		thumbnail = imaging.Resize(img, IMAGE_THUMBNAIL_PIXEL_WIDTH, 0, imaging.Lanczos)
572 573 574 575
	}

	buf := new(bytes.Buffer)
	if err := jpeg.Encode(buf, thumbnail, &jpeg.Options{Quality: 90}); err != nil {
576
		mlog.Error(fmt.Sprintf("Unable to encode image as jpeg path=%v err=%v", thumbnailPath, err))
577 578 579
		return
	}

580
	if _, err := a.WriteFile(buf, thumbnailPath); err != nil {
581
		mlog.Error(fmt.Sprintf("Unable to upload thumbnail path=%v err=%v", thumbnailPath, err))
582 583 584 585
		return
	}
}

586
func (a *App) generatePreviewImage(img image.Image, previewPath string, width int) {
587
	var preview image.Image
588 589 590

	if width > IMAGE_PREVIEW_PIXEL_WIDTH {
		preview = imaging.Resize(img, IMAGE_PREVIEW_PIXEL_WIDTH, 0, imaging.Lanczos)
591 592 593 594 595 596 597
	} else {
		preview = img
	}

	buf := new(bytes.Buffer)

	if err := jpeg.Encode(buf, preview, &jpeg.Options{Quality: 90}); err != nil {
598
		mlog.Error(fmt.Sprintf("Unable to encode image as preview jpg err=%v", err), mlog.String("path", previewPath))
599 600 601
		return
	}

602
	if _, err := a.WriteFile(buf, previewPath); err != nil {
603
		mlog.Error(fmt.Sprintf("Unable to upload preview err=%v", err), mlog.String("path", previewPath))
604 605 606
		return
	}
}
607

Chris's avatar
Chris committed
608 609
func (a *App) GetFileInfo(fileId string) (*model.FileInfo, *model.AppError) {
	if result := <-a.Srv.Store.FileInfo().Get(fileId); result.Err != nil {
610 611 612 613 614
		return nil, result.Err
	} else {
		return result.Data.(*model.FileInfo), nil
	}
}
615 616

func (a *App) CopyFileInfos(userId string, fileIds []string) ([]string, *model.AppError) {
617
	var newFileIds []string
618 619 620 621

	now := model.GetMillis()

	for _, fileId := range fileIds {
622
		result := <-a.Srv.Store.FileInfo().Get(fileId)
623

624
		if result.Err != nil {
625 626 627
			return nil, result.Err
		}

628
		fileInfo := result.Data.(*model.FileInfo)
629 630 631 632 633 634 635 636 637 638 639 640 641 642 643
		fileInfo.Id = model.NewId()
		fileInfo.CreatorId = userId
		fileInfo.CreateAt = now
		fileInfo.UpdateAt = now
		fileInfo.PostId = ""

		if result := <-a.Srv.Store.FileInfo().Save(fileInfo); result.Err != nil {
			return newFileIds, result.Err
		}

		newFileIds = append(newFileIds, fileInfo.Id)
	}

	return newFileIds, nil
}