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