github.com/mattermosttest/mattermost-server/v5@v5.0.0-20200917143240-9dfa12e121f9/app/file.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 "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 "regexp" 22 "strings" 23 "sync" 24 "time" 25 26 "github.com/disintegration/imaging" 27 _ "github.com/oov/psd" 28 "github.com/rwcarlsen/goexif/exif" 29 _ "golang.org/x/image/bmp" 30 _ "golang.org/x/image/tiff" 31 32 "github.com/mattermost/mattermost-server/v5/mlog" 33 "github.com/mattermost/mattermost-server/v5/model" 34 "github.com/mattermost/mattermost-server/v5/plugin" 35 "github.com/mattermost/mattermost-server/v5/services/filesstore" 36 "github.com/mattermost/mattermost-server/v5/utils" 37 ) 38 39 const ( 40 /* 41 EXIF Image Orientations 42 1 2 3 4 5 6 7 8 43 44 888888 888888 88 88 8888888888 88 88 8888888888 45 88 88 88 88 88 88 88 88 88 88 88 88 46 8888 8888 8888 8888 88 8888888888 8888888888 88 47 88 88 88 88 48 88 88 888888 888888 49 */ 50 Upright = 1 51 UprightMirrored = 2 52 UpsideDown = 3 53 UpsideDownMirrored = 4 54 RotatedCWMirrored = 5 55 RotatedCCW = 6 56 RotatedCCWMirrored = 7 57 RotatedCW = 8 58 59 MaxImageSize = int64(6048 * 4032) // 24 megapixels, roughly 36MB as a raw image 60 ImageThumbnailWidth = 120 61 ImageThumbnailHeight = 100 62 ImageThumbnailRatio = float64(ImageThumbnailHeight) / float64(ImageThumbnailWidth) 63 ImagePreviewWidth = 1920 64 65 maxUploadInitialBufferSize = 1024 * 1024 // 1Mb 66 67 // Deprecated 68 IMAGE_THUMBNAIL_PIXEL_WIDTH = 120 69 IMAGE_THUMBNAIL_PIXEL_HEIGHT = 100 70 IMAGE_PREVIEW_PIXEL_WIDTH = 1920 71 ) 72 73 func (a *App) FileBackend() (filesstore.FileBackend, *model.AppError) { 74 return a.Srv().FileBackend() 75 } 76 77 func (a *App) ReadFile(path string) ([]byte, *model.AppError) { 78 backend, err := a.FileBackend() 79 if err != nil { 80 return nil, err 81 } 82 return backend.ReadFile(path) 83 } 84 85 // Caller must close the first return value 86 func (a *App) FileReader(path string) (filesstore.ReadCloseSeeker, *model.AppError) { 87 backend, err := a.FileBackend() 88 if err != nil { 89 return nil, err 90 } 91 return backend.Reader(path) 92 } 93 94 func (a *App) FileExists(path string) (bool, *model.AppError) { 95 backend, err := a.FileBackend() 96 if err != nil { 97 return false, err 98 } 99 return backend.FileExists(path) 100 } 101 102 func (a *App) MoveFile(oldPath, newPath string) *model.AppError { 103 backend, err := a.FileBackend() 104 if err != nil { 105 return err 106 } 107 return backend.MoveFile(oldPath, newPath) 108 } 109 110 func (a *App) WriteFile(fr io.Reader, path string) (int64, *model.AppError) { 111 backend, err := a.FileBackend() 112 if err != nil { 113 return 0, err 114 } 115 116 return backend.WriteFile(fr, path) 117 } 118 119 func (a *App) RemoveFile(path string) *model.AppError { 120 backend, err := a.FileBackend() 121 if err != nil { 122 return err 123 } 124 return backend.RemoveFile(path) 125 } 126 127 func (a *App) ListDirectory(path string) ([]string, *model.AppError) { 128 backend, err := a.FileBackend() 129 if err != nil { 130 return nil, err 131 } 132 paths, err := backend.ListDirectory(path) 133 if err != nil { 134 return nil, err 135 } 136 137 return *paths, nil 138 } 139 140 func (a *App) getInfoForFilename(post *model.Post, teamId, channelId, userId, oldId, filename string) *model.FileInfo { 141 name, _ := url.QueryUnescape(filename) 142 pathPrefix := fmt.Sprintf("teams/%s/channels/%s/users/%s/%s/", teamId, channelId, userId, oldId) 143 path := pathPrefix + name 144 145 // Open the file and populate the fields of the FileInfo 146 data, err := a.ReadFile(path) 147 if err != nil { 148 mlog.Error( 149 "File not found when migrating post to use FileInfos", 150 mlog.String("post_id", post.Id), 151 mlog.String("filename", filename), 152 mlog.String("path", path), 153 mlog.Err(err), 154 ) 155 return nil 156 } 157 158 info, err := model.GetInfoForBytes(name, data) 159 if err != nil { 160 mlog.Warn( 161 "Unable to fully decode file info when migrating post to use FileInfos", 162 mlog.String("post_id", post.Id), 163 mlog.String("filename", filename), 164 mlog.Err(err), 165 ) 166 } 167 168 // Generate a new ID because with the old system, you could very rarely get multiple posts referencing the same file 169 info.Id = model.NewId() 170 info.CreatorId = post.UserId 171 info.PostId = post.Id 172 info.CreateAt = post.CreateAt 173 info.UpdateAt = post.UpdateAt 174 info.Path = path 175 176 if info.IsImage() { 177 nameWithoutExtension := name[:strings.LastIndex(name, ".")] 178 info.PreviewPath = pathPrefix + nameWithoutExtension + "_preview.jpg" 179 info.ThumbnailPath = pathPrefix + nameWithoutExtension + "_thumb.jpg" 180 } 181 182 return info 183 } 184 185 func (a *App) findTeamIdForFilename(post *model.Post, id, filename string) string { 186 name, _ := url.QueryUnescape(filename) 187 188 // This post is in a direct channel so we need to figure out what team the files are stored under. 189 teams, err := a.Srv().Store.Team().GetTeamsByUserId(post.UserId) 190 if err != nil { 191 mlog.Error("Unable to get teams when migrating post to use FileInfo", mlog.Err(err), mlog.String("post_id", post.Id)) 192 return "" 193 } 194 195 if len(teams) == 1 { 196 // The user has only one team so the post must've been sent from it 197 return teams[0].Id 198 } 199 200 for _, team := range teams { 201 path := fmt.Sprintf("teams/%s/channels/%s/users/%s/%s/%s", team.Id, post.ChannelId, post.UserId, id, name) 202 if ok, err := a.FileExists(path); ok && err == nil { 203 // Found the team that this file was posted from 204 return team.Id 205 } 206 } 207 208 return "" 209 } 210 211 var fileMigrationLock sync.Mutex 212 var oldFilenameMatchExp *regexp.Regexp = regexp.MustCompile(`^\/([a-z\d]{26})\/([a-z\d]{26})\/([a-z\d]{26})\/([^\/]+)$`) 213 214 // Parse the path from the Filename of the form /{channelId}/{userId}/{uid}/{nameWithExtension} 215 func parseOldFilenames(filenames []string, channelId, userId string) [][]string { 216 parsed := [][]string{} 217 for _, filename := range filenames { 218 matches := oldFilenameMatchExp.FindStringSubmatch(filename) 219 if len(matches) != 5 { 220 mlog.Error("Failed to parse old Filename", mlog.String("filename", filename)) 221 continue 222 } 223 if matches[1] != channelId { 224 mlog.Error("ChannelId in Filename does not match", mlog.String("channel_id", channelId), mlog.String("matched", matches[1])) 225 } else if matches[2] != userId { 226 mlog.Error("UserId in Filename does not match", mlog.String("user_id", userId), mlog.String("matched", matches[2])) 227 } else { 228 parsed = append(parsed, matches[1:]) 229 } 230 } 231 return parsed 232 } 233 234 // Creates and stores FileInfos for a post created before the FileInfos table existed. 235 func (a *App) MigrateFilenamesToFileInfos(post *model.Post) []*model.FileInfo { 236 if len(post.Filenames) == 0 { 237 mlog.Warn("Unable to migrate post to use FileInfos with an empty Filenames field", mlog.String("post_id", post.Id)) 238 return []*model.FileInfo{} 239 } 240 241 channel, errCh := a.Srv().Store.Channel().Get(post.ChannelId, true) 242 // There's a weird bug that rarely happens where a post ends up with duplicate Filenames so remove those 243 filenames := utils.RemoveDuplicatesFromStringArray(post.Filenames) 244 if errCh != nil { 245 mlog.Error( 246 "Unable to get channel when migrating post to use FileInfos", 247 mlog.String("post_id", post.Id), 248 mlog.String("channel_id", post.ChannelId), 249 mlog.Err(errCh), 250 ) 251 return []*model.FileInfo{} 252 } 253 254 // Parse and validate filenames before further processing 255 parsedFilenames := parseOldFilenames(filenames, post.ChannelId, post.UserId) 256 257 if len(parsedFilenames) == 0 { 258 mlog.Error("Unable to parse filenames") 259 return []*model.FileInfo{} 260 } 261 262 // Find the team that was used to make this post since its part of the file path that isn't saved in the Filename 263 var teamId string 264 if channel.TeamId == "" { 265 // This post was made in a cross-team DM channel, so we need to find where its files were saved 266 teamId = a.findTeamIdForFilename(post, parsedFilenames[0][2], parsedFilenames[0][3]) 267 } else { 268 teamId = channel.TeamId 269 } 270 271 // Create FileInfo objects for this post 272 infos := make([]*model.FileInfo, 0, len(filenames)) 273 if teamId == "" { 274 mlog.Error( 275 "Unable to find team id for files when migrating post to use FileInfos", 276 mlog.String("filenames", strings.Join(filenames, ",")), 277 mlog.String("post_id", post.Id), 278 ) 279 } else { 280 for _, parsed := range parsedFilenames { 281 info := a.getInfoForFilename(post, teamId, parsed[0], parsed[1], parsed[2], parsed[3]) 282 if info == nil { 283 continue 284 } 285 286 infos = append(infos, info) 287 } 288 } 289 290 // Lock to prevent only one migration thread from trying to update the post at once, preventing duplicate FileInfos from being created 291 fileMigrationLock.Lock() 292 defer fileMigrationLock.Unlock() 293 294 result, err := a.Srv().Store.Post().Get(post.Id, false) 295 if err != nil { 296 mlog.Error("Unable to get post when migrating post to use FileInfos", mlog.Err(err), mlog.String("post_id", post.Id)) 297 return []*model.FileInfo{} 298 } 299 300 if newPost := result.Posts[post.Id]; len(newPost.Filenames) != len(post.Filenames) { 301 // Another thread has already created FileInfos for this post, so just return those 302 var fileInfos []*model.FileInfo 303 fileInfos, err = a.Srv().Store.FileInfo().GetForPost(post.Id, true, false, false) 304 if err != nil { 305 mlog.Error("Unable to get FileInfos for migrated post", mlog.Err(err), mlog.String("post_id", post.Id)) 306 return []*model.FileInfo{} 307 } 308 309 mlog.Debug("Post already migrated to use FileInfos", mlog.String("post_id", post.Id)) 310 return fileInfos 311 } 312 313 mlog.Debug("Migrating post to use FileInfos", mlog.String("post_id", post.Id)) 314 315 savedInfos := make([]*model.FileInfo, 0, len(infos)) 316 fileIds := make([]string, 0, len(filenames)) 317 for _, info := range infos { 318 if _, err = a.Srv().Store.FileInfo().Save(info); err != nil { 319 mlog.Error( 320 "Unable to save file info when migrating post to use FileInfos", 321 mlog.String("post_id", post.Id), 322 mlog.String("file_info_id", info.Id), 323 mlog.String("file_info_path", info.Path), 324 mlog.Err(err), 325 ) 326 continue 327 } 328 329 savedInfos = append(savedInfos, info) 330 fileIds = append(fileIds, info.Id) 331 } 332 333 // Copy and save the updated post 334 newPost := post.Clone() 335 336 newPost.Filenames = []string{} 337 newPost.FileIds = fileIds 338 339 // Update Posts to clear Filenames and set FileIds 340 if _, err = a.Srv().Store.Post().Update(newPost, post); err != nil { 341 mlog.Error( 342 "Unable to save migrated post when migrating to use FileInfos", 343 mlog.String("new_file_ids", strings.Join(newPost.FileIds, ",")), 344 mlog.String("old_filenames", strings.Join(post.Filenames, ",")), 345 mlog.String("post_id", post.Id), 346 mlog.Err(err), 347 ) 348 return []*model.FileInfo{} 349 } 350 return savedInfos 351 } 352 353 func (a *App) GeneratePublicLink(siteURL string, info *model.FileInfo) string { 354 hash := GeneratePublicLinkHash(info.Id, *a.Config().FileSettings.PublicLinkSalt) 355 return fmt.Sprintf("%s/files/%v/public?h=%s", siteURL, info.Id, hash) 356 } 357 358 func GeneratePublicLinkHash(fileId, salt string) string { 359 hash := sha256.New() 360 hash.Write([]byte(salt)) 361 hash.Write([]byte(fileId)) 362 363 return base64.RawURLEncoding.EncodeToString(hash.Sum(nil)) 364 } 365 366 func (a *App) UploadMultipartFiles(teamId string, channelId string, userId string, fileHeaders []*multipart.FileHeader, clientIds []string, now time.Time) (*model.FileUploadResponse, *model.AppError) { 367 files := make([]io.ReadCloser, len(fileHeaders)) 368 filenames := make([]string, len(fileHeaders)) 369 370 for i, fileHeader := range fileHeaders { 371 file, fileErr := fileHeader.Open() 372 if fileErr != nil { 373 return nil, model.NewAppError("UploadFiles", "api.file.upload_file.read_request.app_error", 374 map[string]interface{}{"Filename": fileHeader.Filename}, fileErr.Error(), http.StatusBadRequest) 375 } 376 377 // Will be closed after UploadFiles returns 378 defer file.Close() 379 380 files[i] = file 381 filenames[i] = fileHeader.Filename 382 } 383 384 return a.UploadFiles(teamId, channelId, userId, files, filenames, clientIds, now) 385 } 386 387 // Uploads some files to the given team and channel as the given user. files and filenames should have 388 // the same length. clientIds should either not be provided or have the same length as files and filenames. 389 // The provided files should be closed by the caller so that they are not leaked. 390 func (a *App) UploadFiles(teamId string, channelId string, userId string, files []io.ReadCloser, filenames []string, clientIds []string, now time.Time) (*model.FileUploadResponse, *model.AppError) { 391 if len(*a.Config().FileSettings.DriverName) == 0 { 392 return nil, model.NewAppError("UploadFiles", "api.file.upload_file.storage.app_error", nil, "", http.StatusNotImplemented) 393 } 394 395 if len(filenames) != len(files) || (len(clientIds) > 0 && len(clientIds) != len(files)) { 396 return nil, model.NewAppError("UploadFiles", "api.file.upload_file.incorrect_number_of_files.app_error", nil, "", http.StatusBadRequest) 397 } 398 399 resStruct := &model.FileUploadResponse{ 400 FileInfos: []*model.FileInfo{}, 401 ClientIds: []string{}, 402 } 403 404 previewPathList := []string{} 405 thumbnailPathList := []string{} 406 imageDataList := [][]byte{} 407 408 for i, file := range files { 409 buf := bytes.NewBuffer(nil) 410 io.Copy(buf, file) 411 data := buf.Bytes() 412 413 info, data, err := a.DoUploadFileExpectModification(now, teamId, channelId, userId, filenames[i], data) 414 if err != nil { 415 return nil, err 416 } 417 418 if info.PreviewPath != "" || info.ThumbnailPath != "" { 419 previewPathList = append(previewPathList, info.PreviewPath) 420 thumbnailPathList = append(thumbnailPathList, info.ThumbnailPath) 421 imageDataList = append(imageDataList, data) 422 } 423 424 resStruct.FileInfos = append(resStruct.FileInfos, info) 425 426 if len(clientIds) > 0 { 427 resStruct.ClientIds = append(resStruct.ClientIds, clientIds[i]) 428 } 429 } 430 431 a.HandleImages(previewPathList, thumbnailPathList, imageDataList) 432 433 return resStruct, nil 434 } 435 436 // UploadFile uploads a single file in form of a completely constructed byte array for a channel. 437 func (a *App) UploadFile(data []byte, channelId string, filename string) (*model.FileInfo, *model.AppError) { 438 _, err := a.GetChannel(channelId) 439 if err != nil && channelId != "" { 440 return nil, model.NewAppError("UploadFile", "api.file.upload_file.incorrect_channelId.app_error", 441 map[string]interface{}{"channelId": channelId}, "", http.StatusBadRequest) 442 } 443 444 info, _, appError := a.DoUploadFileExpectModification(time.Now(), "noteam", channelId, "nouser", filename, data) 445 if appError != nil { 446 return nil, appError 447 } 448 449 if info.PreviewPath != "" || info.ThumbnailPath != "" { 450 previewPathList := []string{info.PreviewPath} 451 thumbnailPathList := []string{info.ThumbnailPath} 452 imageDataList := [][]byte{data} 453 454 a.HandleImages(previewPathList, thumbnailPathList, imageDataList) 455 } 456 457 return info, nil 458 } 459 460 func (a *App) DoUploadFile(now time.Time, rawTeamId string, rawChannelId string, rawUserId string, rawFilename string, data []byte) (*model.FileInfo, *model.AppError) { 461 info, _, err := a.DoUploadFileExpectModification(now, rawTeamId, rawChannelId, rawUserId, rawFilename, data) 462 return info, err 463 } 464 465 func UploadFileSetTeamId(teamId string) func(t *UploadFileTask) { 466 return func(t *UploadFileTask) { 467 t.TeamId = filepath.Base(teamId) 468 } 469 } 470 471 func UploadFileSetUserId(userId string) func(t *UploadFileTask) { 472 return func(t *UploadFileTask) { 473 t.UserId = filepath.Base(userId) 474 } 475 } 476 477 func UploadFileSetTimestamp(timestamp time.Time) func(t *UploadFileTask) { 478 return func(t *UploadFileTask) { 479 t.Timestamp = timestamp 480 } 481 } 482 483 func UploadFileSetContentLength(contentLength int64) func(t *UploadFileTask) { 484 return func(t *UploadFileTask) { 485 t.ContentLength = contentLength 486 } 487 } 488 489 func UploadFileSetClientId(clientId string) func(t *UploadFileTask) { 490 return func(t *UploadFileTask) { 491 t.ClientId = clientId 492 } 493 } 494 495 func UploadFileSetRaw() func(t *UploadFileTask) { 496 return func(t *UploadFileTask) { 497 t.Raw = true 498 } 499 } 500 501 type UploadFileTask struct { 502 // File name. 503 Name string 504 505 ChannelId string 506 TeamId string 507 UserId string 508 509 // Time stamp to use when creating the file. 510 Timestamp time.Time 511 512 // The value of the Content-Length http header, when available. 513 ContentLength int64 514 515 // The file data stream. 516 Input io.Reader 517 518 // An optional, client-assigned Id field. 519 ClientId string 520 521 // If Raw, do not execute special processing for images, just upload 522 // the file. Plugins are still invoked. 523 Raw bool 524 525 //============================================================= 526 // Internal state 527 528 buf *bytes.Buffer 529 limit int64 530 limitedInput io.Reader 531 teeInput io.Reader 532 fileinfo *model.FileInfo 533 maxFileSize int64 534 535 // Cached image data that (may) get initialized in preprocessImage and 536 // is used in postprocessImage 537 decoded image.Image 538 imageType string 539 imageOrientation int 540 541 // Testing: overrideable dependency functions 542 pluginsEnvironment *plugin.Environment 543 writeFile func(io.Reader, string) (int64, *model.AppError) 544 saveToDatabase func(*model.FileInfo) (*model.FileInfo, *model.AppError) 545 } 546 547 func (t *UploadFileTask) init(a *App) { 548 t.buf = &bytes.Buffer{} 549 if t.ContentLength > 0 { 550 t.limit = t.ContentLength 551 } else { 552 t.limit = t.maxFileSize 553 } 554 555 if t.ContentLength > 0 && t.ContentLength < maxUploadInitialBufferSize { 556 t.buf.Grow(int(t.ContentLength)) 557 } else { 558 t.buf.Grow(maxUploadInitialBufferSize) 559 } 560 561 t.fileinfo = model.NewInfo(filepath.Base(t.Name)) 562 t.fileinfo.Id = model.NewId() 563 t.fileinfo.CreatorId = t.UserId 564 t.fileinfo.CreateAt = t.Timestamp.UnixNano() / int64(time.Millisecond) 565 t.fileinfo.Path = t.pathPrefix() + t.Name 566 567 t.limitedInput = &io.LimitedReader{ 568 R: t.Input, 569 N: t.limit + 1, 570 } 571 t.teeInput = io.TeeReader(t.limitedInput, t.buf) 572 573 t.pluginsEnvironment = a.GetPluginsEnvironment() 574 t.writeFile = a.WriteFile 575 t.saveToDatabase = a.Srv().Store.FileInfo().Save 576 } 577 578 // UploadFileX uploads a single file as specified in t. It applies the upload 579 // constraints, executes plugins and image processing logic as needed. It 580 // returns a filled-out FileInfo and an optional error. A plugin may reject the 581 // upload, returning a rejection error. In this case FileInfo would have 582 // contained the last "good" FileInfo before the execution of that plugin. 583 func (a *App) UploadFileX(channelId, name string, input io.Reader, 584 opts ...func(*UploadFileTask)) (*model.FileInfo, *model.AppError) { 585 586 t := &UploadFileTask{ 587 ChannelId: filepath.Base(channelId), 588 Name: filepath.Base(name), 589 Input: input, 590 maxFileSize: *a.Config().FileSettings.MaxFileSize, 591 } 592 for _, o := range opts { 593 o(t) 594 } 595 596 if len(*a.Config().FileSettings.DriverName) == 0 { 597 return nil, t.newAppError("api.file.upload_file.storage.app_error", 598 "", http.StatusNotImplemented) 599 } 600 if t.ContentLength > t.maxFileSize { 601 return nil, t.newAppError("api.file.upload_file.too_large_detailed.app_error", 602 "", http.StatusRequestEntityTooLarge, "Length", t.ContentLength, "Limit", t.maxFileSize) 603 } 604 605 t.init(a) 606 607 var aerr *model.AppError 608 if !t.Raw && t.fileinfo.IsImage() { 609 aerr = t.preprocessImage() 610 if aerr != nil { 611 return t.fileinfo, aerr 612 } 613 } 614 615 aerr = t.readAll() 616 if aerr != nil { 617 return t.fileinfo, aerr 618 } 619 620 aerr = t.runPlugins() 621 if aerr != nil { 622 return t.fileinfo, aerr 623 } 624 625 // Concurrently upload and update DB, and post-process the image. 626 wg := sync.WaitGroup{} 627 628 if !t.Raw && t.fileinfo.IsImage() { 629 wg.Add(1) 630 go func() { 631 t.postprocessImage() 632 wg.Done() 633 }() 634 } 635 636 _, aerr = t.writeFile(t.newReader(), t.fileinfo.Path) 637 if aerr != nil { 638 return nil, aerr 639 } 640 641 if _, err := t.saveToDatabase(t.fileinfo); err != nil { 642 return nil, err 643 } 644 645 wg.Wait() 646 647 return t.fileinfo, nil 648 } 649 650 func (t *UploadFileTask) readAll() *model.AppError { 651 _, err := t.buf.ReadFrom(t.limitedInput) 652 if err != nil { 653 // Ugly hack: the error is not exported from net/http. 654 if err.Error() == "http: request body too large" { 655 return t.newAppError("api.file.upload_file.too_large_detailed.app_error", 656 "", http.StatusRequestEntityTooLarge, "Length", t.buf.Len(), "Limit", t.limit) 657 } 658 return t.newAppError("api.file.upload_file.read_request.app_error", 659 err.Error(), http.StatusBadRequest) 660 } 661 if int64(t.buf.Len()) > t.limit { 662 return t.newAppError("api.file.upload_file.too_large_detailed.app_error", 663 "", http.StatusRequestEntityTooLarge, "Length", t.buf.Len(), "Limit", t.limit) 664 } 665 t.fileinfo.Size = int64(t.buf.Len()) 666 667 t.limitedInput = nil 668 t.teeInput = nil 669 return nil 670 } 671 672 func (t *UploadFileTask) runPlugins() *model.AppError { 673 if t.pluginsEnvironment == nil { 674 return nil 675 } 676 677 pluginContext := &plugin.Context{} 678 var rejectionError *model.AppError 679 680 t.pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool { 681 buf := &bytes.Buffer{} 682 replacementInfo, rejectionReason := hooks.FileWillBeUploaded(pluginContext, 683 t.fileinfo, t.newReader(), buf) 684 if rejectionReason != "" { 685 rejectionError = t.newAppError("api.file.upload_file.rejected_by_plugin.app_error", 686 rejectionReason, http.StatusForbidden, "Reason", rejectionReason) 687 return false 688 } 689 if replacementInfo != nil { 690 t.fileinfo = replacementInfo 691 } 692 if buf.Len() != 0 { 693 t.buf = buf 694 t.teeInput = nil 695 t.limitedInput = nil 696 t.fileinfo.Size = int64(buf.Len()) 697 } 698 699 return true 700 }, plugin.FileWillBeUploadedId) 701 702 if rejectionError != nil { 703 return rejectionError 704 } 705 706 return nil 707 } 708 709 func (t *UploadFileTask) preprocessImage() *model.AppError { 710 // If SVG, attempt to extract dimensions and then return 711 if t.fileinfo.MimeType == "image/svg+xml" { 712 svgInfo, err := parseSVG(t.newReader()) 713 if err != nil { 714 mlog.Error("Failed to parse SVG", mlog.Err(err)) 715 } 716 if svgInfo.Width > 0 && svgInfo.Height > 0 { 717 t.fileinfo.Width = svgInfo.Width 718 t.fileinfo.Height = svgInfo.Height 719 } 720 t.fileinfo.HasPreviewImage = false 721 return nil 722 } 723 724 // If we fail to decode, return "as is". 725 config, _, err := image.DecodeConfig(t.newReader()) 726 if err != nil { 727 return nil 728 } 729 730 t.fileinfo.Width = config.Width 731 t.fileinfo.Height = config.Height 732 733 // Check dimensions before loading the whole thing into memory later on. 734 // This casting is done to prevent overflow on 32 bit systems (not needed 735 // in 64 bits systems because images can't have more than 32 bits height or 736 // width) 737 if int64(t.fileinfo.Width)*int64(t.fileinfo.Height) > MaxImageSize { 738 return t.newAppError("api.file.upload_file.large_image_detailed.app_error", 739 "", http.StatusBadRequest) 740 } 741 t.fileinfo.HasPreviewImage = true 742 nameWithoutExtension := t.Name[:strings.LastIndex(t.Name, ".")] 743 t.fileinfo.PreviewPath = t.pathPrefix() + nameWithoutExtension + "_preview.jpg" 744 t.fileinfo.ThumbnailPath = t.pathPrefix() + nameWithoutExtension + "_thumb.jpg" 745 746 // check the image orientation with goexif; consume the bytes we 747 // already have first, then keep Tee-ing from input. 748 // TODO: try to reuse exif's .Raw buffer rather than Tee-ing 749 if t.imageOrientation, err = getImageOrientation(t.newReader()); err == nil && 750 (t.imageOrientation == RotatedCWMirrored || 751 t.imageOrientation == RotatedCCW || 752 t.imageOrientation == RotatedCCWMirrored || 753 t.imageOrientation == RotatedCW) { 754 t.fileinfo.Width, t.fileinfo.Height = t.fileinfo.Height, t.fileinfo.Width 755 } 756 757 // For animated GIFs disable the preview; since we have to Decode gifs 758 // anyway, cache the decoded image for later. 759 if t.fileinfo.MimeType == "image/gif" { 760 gifConfig, err := gif.DecodeAll(t.newReader()) 761 if err == nil { 762 if len(gifConfig.Image) >= 1 { 763 t.fileinfo.HasPreviewImage = false 764 765 } 766 if len(gifConfig.Image) > 0 { 767 t.decoded = gifConfig.Image[0] 768 t.imageType = "gif" 769 } 770 } 771 } 772 773 return nil 774 } 775 776 func (t *UploadFileTask) postprocessImage() { 777 // don't try to process SVG files 778 if t.fileinfo.MimeType == "image/svg+xml" { 779 return 780 } 781 782 decoded, typ := t.decoded, t.imageType 783 if decoded == nil { 784 var err error 785 decoded, typ, err = image.Decode(t.newReader()) 786 if err != nil { 787 mlog.Error("Unable to decode image", mlog.Err(err)) 788 return 789 } 790 } 791 792 // Fill in the background of a potentially-transparent png file as 793 // white. 794 if typ == "png" { 795 dst := image.NewRGBA(decoded.Bounds()) 796 draw.Draw(dst, dst.Bounds(), image.NewUniform(color.White), image.Point{}, draw.Src) 797 draw.Draw(dst, dst.Bounds(), decoded, decoded.Bounds().Min, draw.Over) 798 decoded = dst 799 } 800 801 decoded = makeImageUpright(decoded, t.imageOrientation) 802 if decoded == nil { 803 return 804 } 805 806 const jpegQuality = 90 807 writeJPEG := func(img image.Image, path string) { 808 r, w := io.Pipe() 809 go func() { 810 err := jpeg.Encode(w, img, &jpeg.Options{Quality: jpegQuality}) 811 if err != nil { 812 mlog.Error("Unable to encode image as jpeg", mlog.String("path", path), mlog.Err(err)) 813 w.CloseWithError(err) 814 } else { 815 w.Close() 816 } 817 }() 818 _, aerr := t.writeFile(r, path) 819 if aerr != nil { 820 mlog.Error("Unable to upload", mlog.String("path", path), mlog.Err(aerr)) 821 return 822 } 823 } 824 825 w := decoded.Bounds().Dx() 826 h := decoded.Bounds().Dy() 827 828 var wg sync.WaitGroup 829 wg.Add(2) 830 go func() { 831 defer wg.Done() 832 thumb := decoded 833 if h > ImageThumbnailHeight || w > ImageThumbnailWidth { 834 if float64(h)/float64(w) < ImageThumbnailRatio { 835 thumb = imaging.Resize(decoded, 0, ImageThumbnailHeight, imaging.Lanczos) 836 } else { 837 thumb = imaging.Resize(decoded, ImageThumbnailWidth, 0, imaging.Lanczos) 838 } 839 } 840 writeJPEG(thumb, t.fileinfo.ThumbnailPath) 841 }() 842 843 go func() { 844 defer wg.Done() 845 preview := decoded 846 if w > ImagePreviewWidth { 847 preview = imaging.Resize(decoded, ImagePreviewWidth, 0, imaging.Lanczos) 848 } 849 writeJPEG(preview, t.fileinfo.PreviewPath) 850 }() 851 wg.Wait() 852 } 853 854 func (t UploadFileTask) newReader() io.Reader { 855 if t.teeInput != nil { 856 return io.MultiReader(bytes.NewReader(t.buf.Bytes()), t.teeInput) 857 } else { 858 return bytes.NewReader(t.buf.Bytes()) 859 } 860 } 861 862 func (t UploadFileTask) pathPrefix() string { 863 return t.Timestamp.Format("20060102") + 864 "/teams/" + t.TeamId + 865 "/channels/" + t.ChannelId + 866 "/users/" + t.UserId + 867 "/" + t.fileinfo.Id + "/" 868 } 869 870 func (t UploadFileTask) newAppError(id string, details interface{}, httpStatus int, extra ...interface{}) *model.AppError { 871 params := map[string]interface{}{ 872 "Name": t.Name, 873 "Filename": t.Name, 874 "ChannelId": t.ChannelId, 875 "TeamId": t.TeamId, 876 "UserId": t.UserId, 877 "ContentLength": t.ContentLength, 878 "ClientId": t.ClientId, 879 } 880 if t.fileinfo != nil { 881 params["Width"] = t.fileinfo.Width 882 params["Height"] = t.fileinfo.Height 883 } 884 for i := 0; i+1 < len(extra); i += 2 { 885 params[fmt.Sprintf("%v", extra[i])] = extra[i+1] 886 } 887 888 return model.NewAppError("uploadFileTask", id, params, fmt.Sprintf("%v", details), httpStatus) 889 } 890 891 func (a *App) DoUploadFileExpectModification(now time.Time, rawTeamId string, rawChannelId string, rawUserId string, rawFilename string, data []byte) (*model.FileInfo, []byte, *model.AppError) { 892 filename := filepath.Base(rawFilename) 893 teamId := filepath.Base(rawTeamId) 894 channelId := filepath.Base(rawChannelId) 895 userId := filepath.Base(rawUserId) 896 897 info, err := model.GetInfoForBytes(filename, data) 898 if err != nil { 899 err.StatusCode = http.StatusBadRequest 900 return nil, data, err 901 } 902 903 if orientation, err := getImageOrientation(bytes.NewReader(data)); err == nil && 904 (orientation == RotatedCWMirrored || 905 orientation == RotatedCCW || 906 orientation == RotatedCCWMirrored || 907 orientation == RotatedCW) { 908 info.Width, info.Height = info.Height, info.Width 909 } 910 911 info.Id = model.NewId() 912 info.CreatorId = userId 913 info.CreateAt = now.UnixNano() / int64(time.Millisecond) 914 915 pathPrefix := now.Format("20060102") + "/teams/" + teamId + "/channels/" + channelId + "/users/" + userId + "/" + info.Id + "/" 916 info.Path = pathPrefix + filename 917 918 if info.IsImage() { 919 // Check dimensions before loading the whole thing into memory later on 920 // This casting is done to prevent overflow on 32 bit systems (not needed 921 // in 64 bits systems because images can't have more than 32 bits height or 922 // width) 923 if int64(info.Width)*int64(info.Height) > MaxImageSize { 924 err := model.NewAppError("uploadFile", "api.file.upload_file.large_image.app_error", map[string]interface{}{"Filename": filename}, "", http.StatusBadRequest) 925 return nil, data, err 926 } 927 928 nameWithoutExtension := filename[:strings.LastIndex(filename, ".")] 929 info.PreviewPath = pathPrefix + nameWithoutExtension + "_preview.jpg" 930 info.ThumbnailPath = pathPrefix + nameWithoutExtension + "_thumb.jpg" 931 } 932 933 if pluginsEnvironment := a.GetPluginsEnvironment(); pluginsEnvironment != nil { 934 var rejectionError *model.AppError 935 pluginContext := a.PluginContext() 936 pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool { 937 var newBytes bytes.Buffer 938 replacementInfo, rejectionReason := hooks.FileWillBeUploaded(pluginContext, info, bytes.NewReader(data), &newBytes) 939 if rejectionReason != "" { 940 rejectionError = model.NewAppError("DoUploadFile", "File rejected by plugin. "+rejectionReason, nil, "", http.StatusBadRequest) 941 return false 942 } 943 if replacementInfo != nil { 944 info = replacementInfo 945 } 946 if newBytes.Len() != 0 { 947 data = newBytes.Bytes() 948 info.Size = int64(len(data)) 949 } 950 951 return true 952 }, plugin.FileWillBeUploadedId) 953 if rejectionError != nil { 954 return nil, data, rejectionError 955 } 956 } 957 958 if _, err := a.WriteFile(bytes.NewReader(data), info.Path); err != nil { 959 return nil, data, err 960 } 961 962 if _, err := a.Srv().Store.FileInfo().Save(info); err != nil { 963 return nil, data, err 964 } 965 966 return info, data, nil 967 } 968 969 func (a *App) HandleImages(previewPathList []string, thumbnailPathList []string, fileData [][]byte) { 970 wg := new(sync.WaitGroup) 971 972 for i := range fileData { 973 img, width, height := prepareImage(fileData[i]) 974 if img != nil { 975 wg.Add(2) 976 go func(img image.Image, path string, width int, height int) { 977 defer wg.Done() 978 a.generateThumbnailImage(img, path, width, height) 979 }(img, thumbnailPathList[i], width, height) 980 981 go func(img image.Image, path string, width int) { 982 defer wg.Done() 983 a.generatePreviewImage(img, path, width) 984 }(img, previewPathList[i], width) 985 } 986 } 987 wg.Wait() 988 } 989 990 func prepareImage(fileData []byte) (image.Image, int, int) { 991 // Decode image bytes into Image object 992 img, imgType, err := image.Decode(bytes.NewReader(fileData)) 993 if err != nil { 994 mlog.Error("Unable to decode image", mlog.Err(err)) 995 return nil, 0, 0 996 } 997 998 width := img.Bounds().Dx() 999 height := img.Bounds().Dy() 1000 1001 // Fill in the background of a potentially-transparent png file as white 1002 if imgType == "png" { 1003 dst := image.NewRGBA(img.Bounds()) 1004 draw.Draw(dst, dst.Bounds(), image.NewUniform(color.White), image.Point{}, draw.Src) 1005 draw.Draw(dst, dst.Bounds(), img, img.Bounds().Min, draw.Over) 1006 img = dst 1007 } 1008 1009 // Flip the image to be upright 1010 orientation, _ := getImageOrientation(bytes.NewReader(fileData)) 1011 img = makeImageUpright(img, orientation) 1012 1013 return img, width, height 1014 } 1015 1016 func makeImageUpright(img image.Image, orientation int) image.Image { 1017 switch orientation { 1018 case UprightMirrored: 1019 return imaging.FlipH(img) 1020 case UpsideDown: 1021 return imaging.Rotate180(img) 1022 case UpsideDownMirrored: 1023 return imaging.FlipV(img) 1024 case RotatedCWMirrored: 1025 return imaging.Transpose(img) 1026 case RotatedCCW: 1027 return imaging.Rotate270(img) 1028 case RotatedCCWMirrored: 1029 return imaging.Transverse(img) 1030 case RotatedCW: 1031 return imaging.Rotate90(img) 1032 default: 1033 return img 1034 } 1035 } 1036 1037 func getImageOrientation(input io.Reader) (int, error) { 1038 exifData, err := exif.Decode(input) 1039 if err != nil { 1040 return Upright, err 1041 } 1042 1043 tag, err := exifData.Get("Orientation") 1044 if err != nil { 1045 return Upright, err 1046 } 1047 1048 orientation, err := tag.Int(0) 1049 if err != nil { 1050 return Upright, err 1051 } 1052 1053 return orientation, nil 1054 } 1055 1056 func (a *App) generateThumbnailImage(img image.Image, thumbnailPath string, width int, height int) { 1057 thumbWidth := float64(IMAGE_THUMBNAIL_PIXEL_WIDTH) 1058 thumbHeight := float64(IMAGE_THUMBNAIL_PIXEL_HEIGHT) 1059 imgWidth := float64(width) 1060 imgHeight := float64(height) 1061 1062 var thumbnail image.Image 1063 if imgHeight < IMAGE_THUMBNAIL_PIXEL_HEIGHT && imgWidth < thumbWidth { 1064 thumbnail = img 1065 } else if imgHeight/imgWidth < thumbHeight/thumbWidth { 1066 thumbnail = imaging.Resize(img, 0, IMAGE_THUMBNAIL_PIXEL_HEIGHT, imaging.Lanczos) 1067 } else { 1068 thumbnail = imaging.Resize(img, IMAGE_THUMBNAIL_PIXEL_WIDTH, 0, imaging.Lanczos) 1069 } 1070 1071 buf := new(bytes.Buffer) 1072 if err := jpeg.Encode(buf, thumbnail, &jpeg.Options{Quality: 90}); err != nil { 1073 mlog.Error("Unable to encode image as jpeg", mlog.String("path", thumbnailPath), mlog.Err(err)) 1074 return 1075 } 1076 1077 if _, err := a.WriteFile(buf, thumbnailPath); err != nil { 1078 mlog.Error("Unable to upload thumbnail", mlog.String("path", thumbnailPath), mlog.Err(err)) 1079 return 1080 } 1081 } 1082 1083 func (a *App) generatePreviewImage(img image.Image, previewPath string, width int) { 1084 var preview image.Image 1085 1086 if width > IMAGE_PREVIEW_PIXEL_WIDTH { 1087 preview = imaging.Resize(img, IMAGE_PREVIEW_PIXEL_WIDTH, 0, imaging.Lanczos) 1088 } else { 1089 preview = img 1090 } 1091 1092 buf := new(bytes.Buffer) 1093 1094 if err := jpeg.Encode(buf, preview, &jpeg.Options{Quality: 90}); err != nil { 1095 mlog.Error("Unable to encode image as preview jpg", mlog.Err(err), mlog.String("path", previewPath)) 1096 return 1097 } 1098 1099 if _, err := a.WriteFile(buf, previewPath); err != nil { 1100 mlog.Error("Unable to upload preview", mlog.Err(err), mlog.String("path", previewPath)) 1101 return 1102 } 1103 } 1104 1105 func (a *App) GetFileInfo(fileId string) (*model.FileInfo, *model.AppError) { 1106 return a.Srv().Store.FileInfo().Get(fileId) 1107 } 1108 1109 func (a *App) GetFileInfos(page, perPage int, opt *model.GetFileInfosOptions) ([]*model.FileInfo, *model.AppError) { 1110 return a.Srv().Store.FileInfo().GetWithOptions(page, perPage, opt) 1111 } 1112 1113 func (a *App) GetFile(fileId string) ([]byte, *model.AppError) { 1114 info, err := a.GetFileInfo(fileId) 1115 if err != nil { 1116 return nil, err 1117 } 1118 1119 data, err := a.ReadFile(info.Path) 1120 if err != nil { 1121 return nil, err 1122 } 1123 1124 return data, nil 1125 } 1126 1127 func (a *App) CopyFileInfos(userId string, fileIds []string) ([]string, *model.AppError) { 1128 var newFileIds []string 1129 1130 now := model.GetMillis() 1131 1132 for _, fileId := range fileIds { 1133 fileInfo, err := a.Srv().Store.FileInfo().Get(fileId) 1134 if err != nil { 1135 return nil, err 1136 } 1137 1138 fileInfo.Id = model.NewId() 1139 fileInfo.CreatorId = userId 1140 fileInfo.CreateAt = now 1141 fileInfo.UpdateAt = now 1142 fileInfo.PostId = "" 1143 1144 if _, err := a.Srv().Store.FileInfo().Save(fileInfo); err != nil { 1145 return newFileIds, err 1146 } 1147 1148 newFileIds = append(newFileIds, fileInfo.Id) 1149 } 1150 1151 return newFileIds, nil 1152 }