github.com/haalcala/mattermost-server-change-repo/v5@v5.33.2/app/export.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 "context" 9 "encoding/json" 10 "io" 11 "net/http" 12 "os" 13 "path/filepath" 14 "strings" 15 16 "github.com/pkg/errors" 17 18 "github.com/mattermost/mattermost-server/v5/mlog" 19 "github.com/mattermost/mattermost-server/v5/model" 20 "github.com/mattermost/mattermost-server/v5/store" 21 ) 22 23 type BulkExportOpts struct { 24 IncludeAttachments bool 25 CreateArchive bool 26 } 27 28 // ExportDataDir is the name of the directory were to store additional data 29 // included with the export (e.g. file attachments). 30 const ExportDataDir = "data" 31 32 // We use this map to identify the exportable preferences. 33 // Here we link the preference category and name, to the name of the relevant field in the import struct. 34 var exportablePreferences = map[ComparablePreference]string{{ 35 Category: model.PREFERENCE_CATEGORY_THEME, 36 Name: "", 37 }: "Theme", { 38 Category: model.PREFERENCE_CATEGORY_ADVANCED_SETTINGS, 39 Name: "feature_enabled_markdown_preview", 40 }: "UseMarkdownPreview", { 41 Category: model.PREFERENCE_CATEGORY_ADVANCED_SETTINGS, 42 Name: "formatting", 43 }: "UseFormatting", { 44 Category: model.PREFERENCE_CATEGORY_SIDEBAR_SETTINGS, 45 Name: "show_unread_section", 46 }: "ShowUnreadSection", { 47 Category: model.PREFERENCE_CATEGORY_DISPLAY_SETTINGS, 48 Name: model.PREFERENCE_NAME_USE_MILITARY_TIME, 49 }: "UseMilitaryTime", { 50 Category: model.PREFERENCE_CATEGORY_DISPLAY_SETTINGS, 51 Name: model.PREFERENCE_NAME_COLLAPSE_SETTING, 52 }: "CollapsePreviews", { 53 Category: model.PREFERENCE_CATEGORY_DISPLAY_SETTINGS, 54 Name: model.PREFERENCE_NAME_MESSAGE_DISPLAY, 55 }: "MessageDisplay", { 56 Category: model.PREFERENCE_CATEGORY_DISPLAY_SETTINGS, 57 Name: "channel_display_mode", 58 }: "ChannelDisplayMode", { 59 Category: model.PREFERENCE_CATEGORY_TUTORIAL_STEPS, 60 Name: "", 61 }: "TutorialStep", { 62 Category: model.PREFERENCE_CATEGORY_NOTIFICATIONS, 63 Name: model.PREFERENCE_NAME_EMAIL_INTERVAL, 64 }: "EmailInterval", 65 } 66 67 func (a *App) BulkExport(writer io.Writer, outPath string, opts BulkExportOpts) *model.AppError { 68 var zipWr *zip.Writer 69 if opts.CreateArchive { 70 var err error 71 zipWr = zip.NewWriter(writer) 72 defer zipWr.Close() 73 writer, err = zipWr.Create("import.jsonl") 74 if err != nil { 75 return model.NewAppError("BulkExport", "app.export.zip_create.error", 76 nil, "err="+err.Error(), http.StatusInternalServerError) 77 } 78 } 79 80 mlog.Info("Bulk export: exporting version") 81 if err := a.exportVersion(writer); err != nil { 82 return err 83 } 84 85 mlog.Info("Bulk export: exporting teams") 86 if err := a.exportAllTeams(writer); err != nil { 87 return err 88 } 89 90 mlog.Info("Bulk export: exporting channels") 91 if err := a.exportAllChannels(writer); err != nil { 92 return err 93 } 94 95 mlog.Info("Bulk export: exporting users") 96 if err := a.exportAllUsers(writer); err != nil { 97 return err 98 } 99 100 mlog.Info("Bulk export: exporting posts") 101 attachments, err := a.exportAllPosts(writer, opts.IncludeAttachments) 102 if err != nil { 103 return err 104 } 105 106 mlog.Info("Bulk export: exporting emoji") 107 emojiPaths, err := a.exportCustomEmoji(writer, outPath, "exported_emoji", !opts.CreateArchive) 108 if err != nil { 109 return err 110 } 111 112 mlog.Info("Bulk export: exporting direct channels") 113 if err = a.exportAllDirectChannels(writer); err != nil { 114 return err 115 } 116 117 mlog.Info("Bulk export: exporting direct posts") 118 directAttachments, err := a.exportAllDirectPosts(writer, opts.IncludeAttachments) 119 if err != nil { 120 return err 121 } 122 123 if opts.IncludeAttachments { 124 mlog.Info("Bulk export: exporting file attachments") 125 for _, attachment := range attachments { 126 if err := a.exportFile(outPath, *attachment.Path, zipWr); err != nil { 127 return err 128 } 129 } 130 for _, attachment := range directAttachments { 131 if err := a.exportFile(outPath, *attachment.Path, zipWr); err != nil { 132 return err 133 } 134 } 135 for _, emojiPath := range emojiPaths { 136 if err := a.exportFile(outPath, emojiPath, zipWr); err != nil { 137 return err 138 } 139 } 140 } 141 142 return nil 143 } 144 145 func (a *App) exportWriteLine(writer io.Writer, line *LineImportData) *model.AppError { 146 b, err := json.Marshal(line) 147 if err != nil { 148 return model.NewAppError("BulkExport", "app.export.export_write_line.json_marshall.error", nil, "err="+err.Error(), http.StatusBadRequest) 149 } 150 151 if _, err := writer.Write(append(b, '\n')); err != nil { 152 return model.NewAppError("BulkExport", "app.export.export_write_line.io_writer.error", nil, "err="+err.Error(), http.StatusBadRequest) 153 } 154 155 return nil 156 } 157 158 func (a *App) exportVersion(writer io.Writer) *model.AppError { 159 version := 1 160 versionLine := &LineImportData{ 161 Type: "version", 162 Version: &version, 163 } 164 165 return a.exportWriteLine(writer, versionLine) 166 } 167 168 func (a *App) exportAllTeams(writer io.Writer) *model.AppError { 169 afterId := strings.Repeat("0", 26) 170 for { 171 teams, err := a.Srv().Store.Team().GetAllForExportAfter(1000, afterId) 172 if err != nil { 173 return model.NewAppError("exportAllTeams", "app.team.get_all.app_error", nil, err.Error(), http.StatusInternalServerError) 174 } 175 176 if len(teams) == 0 { 177 break 178 } 179 180 for _, team := range teams { 181 afterId = team.Id 182 183 // Skip deleted. 184 if team.DeleteAt != 0 { 185 continue 186 } 187 188 teamLine := ImportLineFromTeam(team) 189 if err := a.exportWriteLine(writer, teamLine); err != nil { 190 return err 191 } 192 } 193 } 194 195 return nil 196 } 197 198 func (a *App) exportAllChannels(writer io.Writer) *model.AppError { 199 afterId := strings.Repeat("0", 26) 200 for { 201 channels, err := a.Srv().Store.Channel().GetAllChannelsForExportAfter(1000, afterId) 202 203 if err != nil { 204 return model.NewAppError("exportAllChannels", "app.channel.get_all.app_error", nil, err.Error(), http.StatusInternalServerError) 205 } 206 207 if len(channels) == 0 { 208 break 209 } 210 211 for _, channel := range channels { 212 afterId = channel.Id 213 214 // Skip deleted. 215 if channel.DeleteAt != 0 { 216 continue 217 } 218 219 channelLine := ImportLineFromChannel(channel) 220 if err := a.exportWriteLine(writer, channelLine); err != nil { 221 return err 222 } 223 } 224 } 225 226 return nil 227 } 228 229 func (a *App) exportAllUsers(writer io.Writer) *model.AppError { 230 afterId := strings.Repeat("0", 26) 231 for { 232 users, err := a.Srv().Store.User().GetAllAfter(1000, afterId) 233 234 if err != nil { 235 return model.NewAppError("exportAllUsers", "app.user.get.app_error", nil, err.Error(), http.StatusInternalServerError) 236 } 237 238 if len(users) == 0 { 239 break 240 } 241 242 for _, user := range users { 243 afterId = user.Id 244 245 // Gathering here the exportable preferences to pass them on to ImportLineFromUser 246 exportedPrefs := make(map[string]*string) 247 allPrefs, err := a.GetPreferencesForUser(user.Id) 248 if err != nil { 249 return err 250 } 251 for _, pref := range allPrefs { 252 // We need to manage the special cases 253 // Here we manage Tutorial steps 254 if pref.Category == model.PREFERENCE_CATEGORY_TUTORIAL_STEPS { 255 pref.Name = "" 256 // Then the email interval 257 } else if pref.Category == model.PREFERENCE_CATEGORY_NOTIFICATIONS && pref.Name == model.PREFERENCE_NAME_EMAIL_INTERVAL { 258 switch pref.Value { 259 case model.PREFERENCE_EMAIL_INTERVAL_NO_BATCHING_SECONDS: 260 pref.Value = model.PREFERENCE_EMAIL_INTERVAL_IMMEDIATELY 261 case model.PREFERENCE_EMAIL_INTERVAL_FIFTEEN_AS_SECONDS: 262 pref.Value = model.PREFERENCE_EMAIL_INTERVAL_FIFTEEN 263 case model.PREFERENCE_EMAIL_INTERVAL_HOUR_AS_SECONDS: 264 pref.Value = model.PREFERENCE_EMAIL_INTERVAL_HOUR 265 case "0": 266 pref.Value = "" 267 } 268 } 269 id, ok := exportablePreferences[ComparablePreference{ 270 Category: pref.Category, 271 Name: pref.Name, 272 }] 273 if ok { 274 prefPtr := pref.Value 275 if prefPtr != "" { 276 exportedPrefs[id] = &prefPtr 277 } else { 278 exportedPrefs[id] = nil 279 } 280 } 281 } 282 283 userLine := ImportLineFromUser(user, exportedPrefs) 284 285 userLine.User.NotifyProps = a.buildUserNotifyProps(user.NotifyProps) 286 287 // Do the Team Memberships. 288 members, err := a.buildUserTeamAndChannelMemberships(user.Id) 289 if err != nil { 290 return err 291 } 292 293 userLine.User.Teams = members 294 295 if err := a.exportWriteLine(writer, userLine); err != nil { 296 return err 297 } 298 } 299 } 300 301 return nil 302 } 303 304 func (a *App) buildUserTeamAndChannelMemberships(userID string) (*[]UserTeamImportData, *model.AppError) { 305 var memberships []UserTeamImportData 306 307 members, err := a.Srv().Store.Team().GetTeamMembersForExport(userID) 308 309 if err != nil { 310 return nil, model.NewAppError("buildUserTeamAndChannelMemberships", "app.team.get_members.app_error", nil, err.Error(), http.StatusInternalServerError) 311 } 312 313 for _, member := range members { 314 // Skip deleted. 315 if member.DeleteAt != 0 { 316 continue 317 } 318 319 memberData := ImportUserTeamDataFromTeamMember(member) 320 321 // Do the Channel Memberships. 322 channelMembers, err := a.buildUserChannelMemberships(userID, member.TeamId) 323 if err != nil { 324 return nil, err 325 } 326 327 // Get the user theme 328 themePreference, nErr := a.Srv().Store.Preference().Get(member.UserId, model.PREFERENCE_CATEGORY_THEME, member.TeamId) 329 if nErr == nil { 330 memberData.Theme = &themePreference.Value 331 } 332 333 memberData.Channels = channelMembers 334 335 memberships = append(memberships, *memberData) 336 } 337 338 return &memberships, nil 339 } 340 341 func (a *App) buildUserChannelMemberships(userID string, teamID string) (*[]UserChannelImportData, *model.AppError) { 342 var memberships []UserChannelImportData 343 344 members, nErr := a.Srv().Store.Channel().GetChannelMembersForExport(userID, teamID) 345 if nErr != nil { 346 return nil, model.NewAppError("buildUserChannelMemberships", "app.channel.get_members.app_error", nil, nErr.Error(), http.StatusInternalServerError) 347 } 348 349 category := model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL 350 preferences, err := a.GetPreferenceByCategoryForUser(userID, category) 351 if err != nil && err.StatusCode != http.StatusNotFound { 352 return nil, err 353 } 354 355 for _, member := range members { 356 memberships = append(memberships, *ImportUserChannelDataFromChannelMemberAndPreferences(member, &preferences)) 357 } 358 return &memberships, nil 359 } 360 361 func (a *App) buildUserNotifyProps(notifyProps model.StringMap) *UserNotifyPropsImportData { 362 363 getProp := func(key string) *string { 364 if v, ok := notifyProps[key]; ok { 365 return &v 366 } 367 return nil 368 } 369 370 return &UserNotifyPropsImportData{ 371 Desktop: getProp(model.DESKTOP_NOTIFY_PROP), 372 DesktopSound: getProp(model.DESKTOP_SOUND_NOTIFY_PROP), 373 Email: getProp(model.EMAIL_NOTIFY_PROP), 374 Mobile: getProp(model.PUSH_NOTIFY_PROP), 375 MobilePushStatus: getProp(model.PUSH_STATUS_NOTIFY_PROP), 376 ChannelTrigger: getProp(model.CHANNEL_MENTIONS_NOTIFY_PROP), 377 CommentsTrigger: getProp(model.COMMENTS_NOTIFY_PROP), 378 MentionKeys: getProp(model.MENTION_KEYS_NOTIFY_PROP), 379 } 380 } 381 382 func (a *App) exportAllPosts(writer io.Writer, withAttachments bool) ([]AttachmentImportData, *model.AppError) { 383 var attachments []AttachmentImportData 384 afterId := strings.Repeat("0", 26) 385 386 for { 387 posts, nErr := a.Srv().Store.Post().GetParentsForExportAfter(1000, afterId) 388 if nErr != nil { 389 return nil, model.NewAppError("exportAllPosts", "app.post.get_posts.app_error", nil, nErr.Error(), http.StatusInternalServerError) 390 } 391 392 if len(posts) == 0 { 393 return attachments, nil 394 } 395 396 for _, post := range posts { 397 afterId = post.Id 398 399 // Skip deleted. 400 if post.DeleteAt != 0 { 401 continue 402 } 403 404 postLine := ImportLineForPost(post) 405 406 replies, replyAttachments, err := a.buildPostReplies(post.Id, withAttachments) 407 if err != nil { 408 return nil, err 409 } 410 411 if withAttachments && len(replyAttachments) > 0 { 412 attachments = append(attachments, replyAttachments...) 413 } 414 415 postLine.Post.Replies = &replies 416 postLine.Post.Reactions = &[]ReactionImportData{} 417 if post.HasReactions { 418 postLine.Post.Reactions, err = a.BuildPostReactions(post.Id) 419 if err != nil { 420 return nil, err 421 } 422 } 423 424 if len(post.FileIds) > 0 { 425 postAttachments, err := a.buildPostAttachments(post.Id) 426 if err != nil { 427 return nil, err 428 } 429 postLine.Post.Attachments = &postAttachments 430 431 if withAttachments && len(postAttachments) > 0 { 432 attachments = append(attachments, postAttachments...) 433 } 434 } 435 436 if err := a.exportWriteLine(writer, postLine); err != nil { 437 return nil, err 438 } 439 } 440 } 441 } 442 443 func (a *App) buildPostReplies(postId string, withAttachments bool) ([]ReplyImportData, []AttachmentImportData, *model.AppError) { 444 var replies []ReplyImportData 445 var attachments []AttachmentImportData 446 447 replyPosts, nErr := a.Srv().Store.Post().GetRepliesForExport(postId) 448 if nErr != nil { 449 return nil, nil, model.NewAppError("buildPostReplies", "app.post.get_posts.app_error", nil, nErr.Error(), http.StatusInternalServerError) 450 } 451 452 for _, reply := range replyPosts { 453 replyImportObject := ImportReplyFromPost(reply) 454 if reply.HasReactions { 455 var appErr *model.AppError 456 replyImportObject.Reactions, appErr = a.BuildPostReactions(reply.Id) 457 if appErr != nil { 458 return nil, nil, appErr 459 } 460 } 461 if len(reply.FileIds) > 0 { 462 postAttachments, appErr := a.buildPostAttachments(reply.Id) 463 if appErr != nil { 464 return nil, nil, appErr 465 } 466 replyImportObject.Attachments = &attachments 467 if withAttachments && len(postAttachments) > 0 { 468 attachments = append(attachments, postAttachments...) 469 } 470 } 471 472 replies = append(replies, *replyImportObject) 473 } 474 475 return replies, attachments, nil 476 } 477 478 func (a *App) BuildPostReactions(postId string) (*[]ReactionImportData, *model.AppError) { 479 var reactionsOfPost []ReactionImportData 480 481 reactions, nErr := a.Srv().Store.Reaction().GetForPost(postId, true) 482 if nErr != nil { 483 return nil, model.NewAppError("BuildPostReactions", "app.reaction.get_for_post.app_error", nil, nErr.Error(), http.StatusInternalServerError) 484 } 485 486 for _, reaction := range reactions { 487 user, err := a.Srv().Store.User().Get(context.Background(), reaction.UserId) 488 if err != nil { 489 var nfErr *store.ErrNotFound 490 if errors.As(err, &nfErr) { // this is a valid case, the user that reacted might've been deleted by now 491 mlog.Info("Skipping reactions by user since the entity doesn't exist anymore", mlog.String("user_id", reaction.UserId)) 492 continue 493 } 494 return nil, model.NewAppError("BuildPostReactions", "app.user.get.app_error", nil, err.Error(), http.StatusInternalServerError) 495 } 496 reactionsOfPost = append(reactionsOfPost, *ImportReactionFromPost(user, reaction)) 497 } 498 499 return &reactionsOfPost, nil 500 501 } 502 503 func (a *App) buildPostAttachments(postId string) ([]AttachmentImportData, *model.AppError) { 504 infos, nErr := a.Srv().Store.FileInfo().GetForPost(postId, false, false, false) 505 if nErr != nil { 506 return nil, model.NewAppError("buildPostAttachments", "app.file_info.get_for_post.app_error", nil, nErr.Error(), http.StatusInternalServerError) 507 } 508 509 attachments := make([]AttachmentImportData, 0, len(infos)) 510 for _, info := range infos { 511 attachments = append(attachments, AttachmentImportData{Path: &info.Path}) 512 } 513 514 return attachments, nil 515 } 516 517 func (a *App) exportCustomEmoji(writer io.Writer, outPath, exportDir string, exportFiles bool) ([]string, *model.AppError) { 518 var emojiPaths []string 519 pageNumber := 0 520 for { 521 customEmojiList, err := a.GetEmojiList(pageNumber, 100, model.EMOJI_SORT_BY_NAME) 522 523 if err != nil { 524 return nil, err 525 } 526 527 if len(customEmojiList) == 0 { 528 break 529 } 530 531 pageNumber++ 532 533 emojiPath := filepath.Join(*a.Config().FileSettings.Directory, "emoji") 534 pathToDir := filepath.Join(outPath, exportDir) 535 if exportFiles { 536 if _, err := os.Stat(pathToDir); os.IsNotExist(err) { 537 os.Mkdir(pathToDir, os.ModePerm) 538 } 539 } 540 541 for _, emoji := range customEmojiList { 542 emojiImagePath := filepath.Join(emojiPath, emoji.Id, "image") 543 filePath := filepath.Join(exportDir, emoji.Id, "image") 544 if exportFiles { 545 err := a.copyEmojiImages(emoji.Id, emojiImagePath, pathToDir) 546 if err != nil { 547 return nil, model.NewAppError("BulkExport", "app.export.export_custom_emoji.copy_emoji_images.error", nil, "err="+err.Error(), http.StatusBadRequest) 548 } 549 } else { 550 filePath = filepath.Join("emoji", emoji.Id, "image") 551 emojiPaths = append(emojiPaths, filePath) 552 } 553 554 emojiImportObject := ImportLineFromEmoji(emoji, filePath) 555 if err := a.exportWriteLine(writer, emojiImportObject); err != nil { 556 return nil, err 557 } 558 } 559 } 560 561 return emojiPaths, nil 562 } 563 564 // Copies emoji files from 'data/emoji' dir to 'exported_emoji' dir 565 func (a *App) copyEmojiImages(emojiId string, emojiImagePath string, pathToDir string) error { 566 fromPath, err := os.Open(emojiImagePath) 567 if fromPath == nil || err != nil { 568 return errors.New("Error reading " + emojiImagePath + "file") 569 } 570 defer fromPath.Close() 571 572 emojiDir := pathToDir + "/" + emojiId 573 574 if _, err = os.Stat(emojiDir); err != nil { 575 if !os.IsNotExist(err) { 576 return errors.Wrapf(err, "Error fetching file info of emoji directory %v", emojiDir) 577 } 578 579 if err = os.Mkdir(emojiDir, os.ModePerm); err != nil { 580 return errors.Wrapf(err, "Error creating emoji directory %v", emojiDir) 581 } 582 } 583 584 toPath, err := os.OpenFile(emojiDir+"/image", os.O_RDWR|os.O_CREATE, 0666) 585 if err != nil { 586 return errors.New("Error creating the image file " + err.Error()) 587 } 588 defer toPath.Close() 589 590 _, err = io.Copy(toPath, fromPath) 591 if err != nil { 592 return errors.New("Error copying emojis " + err.Error()) 593 } 594 595 return nil 596 } 597 598 func (a *App) exportAllDirectChannels(writer io.Writer) *model.AppError { 599 afterId := strings.Repeat("0", 26) 600 for { 601 channels, err := a.Srv().Store.Channel().GetAllDirectChannelsForExportAfter(1000, afterId) 602 if err != nil { 603 return model.NewAppError("exportAllDirectChannels", "app.channel.get_all_direct.app_error", nil, err.Error(), http.StatusInternalServerError) 604 } 605 606 if len(channels) == 0 { 607 break 608 } 609 610 for _, channel := range channels { 611 afterId = channel.Id 612 613 // Skip deleted. 614 if channel.DeleteAt != 0 { 615 continue 616 } 617 618 channelLine := ImportLineFromDirectChannel(channel) 619 if err := a.exportWriteLine(writer, channelLine); err != nil { 620 return err 621 } 622 } 623 } 624 625 return nil 626 } 627 628 func (a *App) exportAllDirectPosts(writer io.Writer, withAttachments bool) ([]AttachmentImportData, *model.AppError) { 629 var attachments []AttachmentImportData 630 afterId := strings.Repeat("0", 26) 631 for { 632 posts, err := a.Srv().Store.Post().GetDirectPostParentsForExportAfter(1000, afterId) 633 if err != nil { 634 return nil, model.NewAppError("exportAllDirectPosts", "app.post.get_direct_posts.app_error", nil, err.Error(), http.StatusInternalServerError) 635 } 636 637 if len(posts) == 0 { 638 break 639 } 640 641 for _, post := range posts { 642 afterId = post.Id 643 644 // Skip deleted. 645 if post.DeleteAt != 0 { 646 continue 647 } 648 649 // Handle attachments. 650 var postAttachments []AttachmentImportData 651 var err *model.AppError 652 if len(post.FileIds) > 0 { 653 postAttachments, err = a.buildPostAttachments(post.Id) 654 if err != nil { 655 return nil, err 656 } 657 658 if withAttachments && len(postAttachments) > 0 { 659 attachments = append(attachments, postAttachments...) 660 } 661 } 662 663 // Do the Replies. 664 replies, replyAttachments, err := a.buildPostReplies(post.Id, withAttachments) 665 if err != nil { 666 return nil, err 667 } 668 669 if withAttachments && len(replyAttachments) > 0 { 670 attachments = append(attachments, replyAttachments...) 671 } 672 673 postLine := ImportLineForDirectPost(post) 674 postLine.DirectPost.Replies = &replies 675 if len(postAttachments) > 0 { 676 postLine.DirectPost.Attachments = &postAttachments 677 } 678 if err := a.exportWriteLine(writer, postLine); err != nil { 679 return nil, err 680 } 681 } 682 } 683 return attachments, nil 684 } 685 686 func (a *App) exportFile(outPath, filePath string, zipWr *zip.Writer) *model.AppError { 687 var wr io.Writer 688 var err error 689 rd, appErr := a.FileReader(filePath) 690 if appErr != nil { 691 return appErr 692 } 693 defer rd.Close() 694 695 if zipWr != nil { 696 wr, err = zipWr.CreateHeader(&zip.FileHeader{ 697 Name: filepath.Join(ExportDataDir, filePath), 698 Method: zip.Store, 699 }) 700 if err != nil { 701 return model.NewAppError("exportFileAttachment", "app.export.export_attachment.zip_create_header.error", 702 nil, "err="+err.Error(), http.StatusInternalServerError) 703 } 704 } else { 705 filePath = filepath.Join(outPath, ExportDataDir, filePath) 706 if err = os.MkdirAll(filepath.Dir(filePath), 0700); err != nil { 707 return model.NewAppError("exportFileAttachment", "app.export.export_attachment.mkdirall.error", 708 nil, "err="+err.Error(), http.StatusInternalServerError) 709 } 710 711 wr, err = os.Create(filePath) 712 if err != nil { 713 return model.NewAppError("exportFileAttachment", "app.export.export_attachment.create_file.error", 714 nil, "err="+err.Error(), http.StatusInternalServerError) 715 } 716 defer wr.(*os.File).Close() 717 } 718 719 if _, err := io.Copy(wr, rd); err != nil { 720 return model.NewAppError("exportFileAttachment", "app.export.export_attachment.copy_file.error", 721 nil, "err="+err.Error(), http.StatusInternalServerError) 722 } 723 724 return nil 725 } 726 727 func (a *App) ListExports() ([]string, *model.AppError) { 728 exports, appErr := a.ListDirectory(*a.Config().ExportSettings.Directory) 729 if appErr != nil { 730 return nil, appErr 731 } 732 733 results := make([]string, len(exports)) 734 for i := range exports { 735 results[i] = filepath.Base(exports[i]) 736 } 737 738 return results, nil 739 } 740 741 func (a *App) DeleteExport(name string) *model.AppError { 742 filePath := filepath.Join(*a.Config().ExportSettings.Directory, name) 743 744 if ok, err := a.FileExists(filePath); err != nil { 745 return err 746 } else if !ok { 747 return nil 748 } 749 750 return a.RemoveFile(filePath) 751 }