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