mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
580 lines
14 KiB
Go
580 lines
14 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package app
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/mattermost/mattermost-server/v5/store"
|
|
|
|
"github.com/mattermost/mattermost-server/v5/mlog"
|
|
"github.com/mattermost/mattermost-server/v5/model"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
// We use this map to identify the exportable preferences.
|
|
// Here we link the preference category and name, to the name of the relevant field in the import struct.
|
|
var exportablePreferences = map[ComparablePreference]string{{
|
|
Category: model.PREFERENCE_CATEGORY_THEME,
|
|
Name: "",
|
|
}: "Theme", {
|
|
Category: model.PREFERENCE_CATEGORY_ADVANCED_SETTINGS,
|
|
Name: "feature_enabled_markdown_preview",
|
|
}: "UseMarkdownPreview", {
|
|
Category: model.PREFERENCE_CATEGORY_ADVANCED_SETTINGS,
|
|
Name: "formatting",
|
|
}: "UseFormatting", {
|
|
Category: model.PREFERENCE_CATEGORY_SIDEBAR_SETTINGS,
|
|
Name: "show_unread_section",
|
|
}: "ShowUnreadSection", {
|
|
Category: model.PREFERENCE_CATEGORY_DISPLAY_SETTINGS,
|
|
Name: model.PREFERENCE_NAME_USE_MILITARY_TIME,
|
|
}: "UseMilitaryTime", {
|
|
Category: model.PREFERENCE_CATEGORY_DISPLAY_SETTINGS,
|
|
Name: model.PREFERENCE_NAME_COLLAPSE_SETTING,
|
|
}: "CollapsePreviews", {
|
|
Category: model.PREFERENCE_CATEGORY_DISPLAY_SETTINGS,
|
|
Name: model.PREFERENCE_NAME_MESSAGE_DISPLAY,
|
|
}: "MessageDisplay", {
|
|
Category: model.PREFERENCE_CATEGORY_DISPLAY_SETTINGS,
|
|
Name: "channel_display_mode",
|
|
}: "ChannelDisplayMode", {
|
|
Category: model.PREFERENCE_CATEGORY_TUTORIAL_STEPS,
|
|
Name: "",
|
|
}: "TutorialStep", {
|
|
Category: model.PREFERENCE_CATEGORY_NOTIFICATIONS,
|
|
Name: model.PREFERENCE_NAME_EMAIL_INTERVAL,
|
|
}: "EmailInterval",
|
|
}
|
|
|
|
func (a *App) BulkExport(writer io.Writer, file string, pathToEmojiDir string, dirNameToExportEmoji string) *model.AppError {
|
|
mlog.Info("Bulk export: exporting version")
|
|
if err := a.exportVersion(writer); err != nil {
|
|
return err
|
|
}
|
|
|
|
mlog.Info("Bulk export: exporting teams")
|
|
if err := a.exportAllTeams(writer); err != nil {
|
|
return err
|
|
}
|
|
|
|
mlog.Info("Bulk export: exporting channels")
|
|
if err := a.exportAllChannels(writer); err != nil {
|
|
return err
|
|
}
|
|
|
|
mlog.Info("Bulk export: exporting users")
|
|
if err := a.exportAllUsers(writer); err != nil {
|
|
return err
|
|
}
|
|
|
|
mlog.Info("Bulk export: exporting posts")
|
|
if err := a.exportAllPosts(writer); err != nil {
|
|
return err
|
|
}
|
|
|
|
mlog.Info("Bulk export: exporting emoji")
|
|
if err := a.exportCustomEmoji(writer, file, pathToEmojiDir, dirNameToExportEmoji); err != nil {
|
|
return err
|
|
}
|
|
|
|
mlog.Info("Bulk export: exporting direct channels")
|
|
if err := a.exportAllDirectChannels(writer); err != nil {
|
|
return err
|
|
}
|
|
|
|
mlog.Info("Bulk export: exporting direct posts")
|
|
if err := a.exportAllDirectPosts(writer); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) exportWriteLine(writer io.Writer, line *LineImportData) *model.AppError {
|
|
b, err := json.Marshal(line)
|
|
if err != nil {
|
|
return model.NewAppError("BulkExport", "app.export.export_write_line.json_marshall.error", nil, "err="+err.Error(), http.StatusBadRequest)
|
|
}
|
|
|
|
if _, err := writer.Write(append(b, '\n')); err != nil {
|
|
return model.NewAppError("BulkExport", "app.export.export_write_line.io_writer.error", nil, "err="+err.Error(), http.StatusBadRequest)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) exportVersion(writer io.Writer) *model.AppError {
|
|
version := 1
|
|
versionLine := &LineImportData{
|
|
Type: "version",
|
|
Version: &version,
|
|
}
|
|
|
|
return a.exportWriteLine(writer, versionLine)
|
|
}
|
|
|
|
func (a *App) exportAllTeams(writer io.Writer) *model.AppError {
|
|
afterId := strings.Repeat("0", 26)
|
|
for {
|
|
teams, err := a.Srv().Store.Team().GetAllForExportAfter(1000, afterId)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(teams) == 0 {
|
|
break
|
|
}
|
|
|
|
for _, team := range teams {
|
|
afterId = team.Id
|
|
|
|
// Skip deleted.
|
|
if team.DeleteAt != 0 {
|
|
continue
|
|
}
|
|
|
|
teamLine := ImportLineFromTeam(team)
|
|
if err := a.exportWriteLine(writer, teamLine); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) exportAllChannels(writer io.Writer) *model.AppError {
|
|
afterId := strings.Repeat("0", 26)
|
|
for {
|
|
channels, err := a.Srv().Store.Channel().GetAllChannelsForExportAfter(1000, afterId)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(channels) == 0 {
|
|
break
|
|
}
|
|
|
|
for _, channel := range channels {
|
|
afterId = channel.Id
|
|
|
|
// Skip deleted.
|
|
if channel.DeleteAt != 0 {
|
|
continue
|
|
}
|
|
|
|
channelLine := ImportLineFromChannel(channel)
|
|
if err := a.exportWriteLine(writer, channelLine); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) exportAllUsers(writer io.Writer) *model.AppError {
|
|
afterId := strings.Repeat("0", 26)
|
|
for {
|
|
users, err := a.Srv().Store.User().GetAllAfter(1000, afterId)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(users) == 0 {
|
|
break
|
|
}
|
|
|
|
for _, user := range users {
|
|
afterId = user.Id
|
|
|
|
// Gathering here the exportable preferences to pass them on to ImportLineFromUser
|
|
exportedPrefs := make(map[string]*string)
|
|
allPrefs, err := a.GetPreferencesForUser(user.Id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, pref := range allPrefs {
|
|
// We need to manage the special cases
|
|
// Here we manage Tutorial steps
|
|
if pref.Category == model.PREFERENCE_CATEGORY_TUTORIAL_STEPS {
|
|
pref.Name = ""
|
|
// Then the email interval
|
|
} else if pref.Category == model.PREFERENCE_CATEGORY_NOTIFICATIONS && pref.Name == model.PREFERENCE_NAME_EMAIL_INTERVAL {
|
|
switch pref.Value {
|
|
case model.PREFERENCE_EMAIL_INTERVAL_NO_BATCHING_SECONDS:
|
|
pref.Value = model.PREFERENCE_EMAIL_INTERVAL_IMMEDIATELY
|
|
case model.PREFERENCE_EMAIL_INTERVAL_FIFTEEN_AS_SECONDS:
|
|
pref.Value = model.PREFERENCE_EMAIL_INTERVAL_FIFTEEN
|
|
case model.PREFERENCE_EMAIL_INTERVAL_HOUR_AS_SECONDS:
|
|
pref.Value = model.PREFERENCE_EMAIL_INTERVAL_HOUR
|
|
case "0":
|
|
pref.Value = ""
|
|
}
|
|
}
|
|
id, ok := exportablePreferences[ComparablePreference{
|
|
Category: pref.Category,
|
|
Name: pref.Name,
|
|
}]
|
|
if ok {
|
|
prefPtr := pref.Value
|
|
if prefPtr != "" {
|
|
exportedPrefs[id] = &prefPtr
|
|
} else {
|
|
exportedPrefs[id] = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
userLine := ImportLineFromUser(user, exportedPrefs)
|
|
|
|
userLine.User.NotifyProps = a.buildUserNotifyProps(user.NotifyProps)
|
|
|
|
// Do the Team Memberships.
|
|
members, err := a.buildUserTeamAndChannelMemberships(user.Id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
userLine.User.Teams = members
|
|
|
|
if err := a.exportWriteLine(writer, userLine); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) buildUserTeamAndChannelMemberships(userId string) (*[]UserTeamImportData, *model.AppError) {
|
|
var memberships []UserTeamImportData
|
|
|
|
members, err := a.Srv().Store.Team().GetTeamMembersForExport(userId)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, member := range members {
|
|
// Skip deleted.
|
|
if member.DeleteAt != 0 {
|
|
continue
|
|
}
|
|
|
|
memberData := ImportUserTeamDataFromTeamMember(member)
|
|
|
|
// Do the Channel Memberships.
|
|
channelMembers, err := a.buildUserChannelMemberships(userId, member.TeamId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Get the user theme
|
|
themePreference, err := a.Srv().Store.Preference().Get(member.UserId, model.PREFERENCE_CATEGORY_THEME, member.TeamId)
|
|
if err == nil {
|
|
memberData.Theme = &themePreference.Value
|
|
}
|
|
|
|
memberData.Channels = channelMembers
|
|
|
|
memberships = append(memberships, *memberData)
|
|
}
|
|
|
|
return &memberships, nil
|
|
}
|
|
|
|
func (a *App) buildUserChannelMemberships(userId string, teamId string) (*[]UserChannelImportData, *model.AppError) {
|
|
var memberships []UserChannelImportData
|
|
|
|
members, err := a.Srv().Store.Channel().GetChannelMembersForExport(userId, teamId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
category := model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL
|
|
preferences, err := a.GetPreferenceByCategoryForUser(userId, category)
|
|
if err != nil && err.StatusCode != http.StatusNotFound {
|
|
return nil, err
|
|
}
|
|
|
|
for _, member := range members {
|
|
memberships = append(memberships, *ImportUserChannelDataFromChannelMemberAndPreferences(member, &preferences))
|
|
}
|
|
return &memberships, nil
|
|
}
|
|
|
|
func (a *App) buildUserNotifyProps(notifyProps model.StringMap) *UserNotifyPropsImportData {
|
|
|
|
getProp := func(key string) *string {
|
|
if v, ok := notifyProps[key]; ok {
|
|
return &v
|
|
}
|
|
return nil
|
|
}
|
|
|
|
return &UserNotifyPropsImportData{
|
|
Desktop: getProp(model.DESKTOP_NOTIFY_PROP),
|
|
DesktopSound: getProp(model.DESKTOP_SOUND_NOTIFY_PROP),
|
|
Email: getProp(model.EMAIL_NOTIFY_PROP),
|
|
Mobile: getProp(model.PUSH_NOTIFY_PROP),
|
|
MobilePushStatus: getProp(model.PUSH_STATUS_NOTIFY_PROP),
|
|
ChannelTrigger: getProp(model.CHANNEL_MENTIONS_NOTIFY_PROP),
|
|
CommentsTrigger: getProp(model.COMMENTS_NOTIFY_PROP),
|
|
MentionKeys: getProp(model.MENTION_KEYS_NOTIFY_PROP),
|
|
}
|
|
}
|
|
|
|
func (a *App) exportAllPosts(writer io.Writer) *model.AppError {
|
|
afterId := strings.Repeat("0", 26)
|
|
|
|
for {
|
|
posts, err := a.Srv().Store.Post().GetParentsForExportAfter(1000, afterId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(posts) == 0 {
|
|
return nil
|
|
}
|
|
|
|
for _, post := range posts {
|
|
afterId = post.Id
|
|
|
|
// Skip deleted.
|
|
if post.DeleteAt != 0 {
|
|
continue
|
|
}
|
|
|
|
postLine := ImportLineForPost(post)
|
|
|
|
postLine.Post.Replies, err = a.buildPostReplies(post.Id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
postLine.Post.Reactions = &[]ReactionImportData{}
|
|
if post.HasReactions {
|
|
postLine.Post.Reactions, err = a.BuildPostReactions(post.Id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := a.exportWriteLine(writer, postLine); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (a *App) buildPostReplies(postId string) (*[]ReplyImportData, *model.AppError) {
|
|
var replies []ReplyImportData
|
|
|
|
replyPosts, err := a.Srv().Store.Post().GetRepliesForExport(postId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, reply := range replyPosts {
|
|
replyImportObject := ImportReplyFromPost(reply)
|
|
if reply.HasReactions {
|
|
replyImportObject.Reactions, err = a.BuildPostReactions(reply.Id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
replies = append(replies, *replyImportObject)
|
|
}
|
|
|
|
return &replies, nil
|
|
}
|
|
|
|
func (a *App) BuildPostReactions(postId string) (*[]ReactionImportData, *model.AppError) {
|
|
var reactionsOfPost []ReactionImportData
|
|
|
|
reactions, err := a.Srv().Store.Reaction().GetForPost(postId, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, reaction := range reactions {
|
|
var user *model.User
|
|
user, err = a.Srv().Store.User().Get(reaction.UserId)
|
|
if err != nil {
|
|
if err.Id == store.MISSING_ACCOUNT_ERROR { // this is a valid case, the user that reacted might've been deleted by now
|
|
mlog.Info("Skipping reactions by user since the entity doesn't exist anymore", mlog.String("user_id", reaction.UserId))
|
|
continue
|
|
}
|
|
return nil, err
|
|
}
|
|
reactionsOfPost = append(reactionsOfPost, *ImportReactionFromPost(user, reaction))
|
|
}
|
|
|
|
return &reactionsOfPost, nil
|
|
|
|
}
|
|
|
|
func (a *App) exportCustomEmoji(writer io.Writer, file string, pathToEmojiDir string, dirNameToExportEmoji string) *model.AppError {
|
|
pageNumber := 0
|
|
for {
|
|
customEmojiList, err := a.GetEmojiList(pageNumber, 100, model.EMOJI_SORT_BY_NAME)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(customEmojiList) == 0 {
|
|
break
|
|
}
|
|
|
|
pageNumber++
|
|
|
|
pathToDir := a.createDirForEmoji(file, dirNameToExportEmoji)
|
|
|
|
for _, emoji := range customEmojiList {
|
|
emojiImagePath := pathToEmojiDir + emoji.Id + "/image"
|
|
err := a.copyEmojiImages(emoji.Id, emojiImagePath, pathToDir)
|
|
if err != nil {
|
|
return model.NewAppError("BulkExport", "app.export.export_custom_emoji.copy_emoji_images.error", nil, "err="+err.Error(), http.StatusBadRequest)
|
|
}
|
|
|
|
filePath := dirNameToExportEmoji + "/" + emoji.Id + "/image"
|
|
|
|
emojiImportObject := ImportLineFromEmoji(emoji, filePath)
|
|
|
|
if err := a.exportWriteLine(writer, emojiImportObject); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Creates directory named 'exported_emoji' to copy the emoji files
|
|
// Directory and the file specified by admin share the same path
|
|
func (a *App) createDirForEmoji(file string, dirName string) string {
|
|
pathToFile, _ := filepath.Abs(file)
|
|
pathSlice := strings.Split(pathToFile, "/")
|
|
if len(pathSlice) > 0 {
|
|
pathSlice = pathSlice[:len(pathSlice)-1]
|
|
}
|
|
pathToDir := strings.Join(pathSlice, "/") + "/" + dirName
|
|
|
|
if _, err := os.Stat(pathToDir); os.IsNotExist(err) {
|
|
os.Mkdir(pathToDir, os.ModePerm)
|
|
}
|
|
return pathToDir
|
|
}
|
|
|
|
// Copies emoji files from 'data/emoji' dir to 'exported_emoji' dir
|
|
func (a *App) copyEmojiImages(emojiId string, emojiImagePath string, pathToDir string) error {
|
|
fromPath, err := os.Open(emojiImagePath)
|
|
if fromPath == nil || err != nil {
|
|
return errors.New("Error reading " + emojiImagePath + "file")
|
|
}
|
|
defer fromPath.Close()
|
|
|
|
emojiDir := pathToDir + "/" + emojiId
|
|
|
|
if _, err = os.Stat(emojiDir); err != nil {
|
|
if !os.IsNotExist(err) {
|
|
return errors.Wrapf(err, "Error fetching file info of emoji directory %v", emojiDir)
|
|
}
|
|
|
|
if err = os.Mkdir(emojiDir, os.ModePerm); err != nil {
|
|
return errors.Wrapf(err, "Error creating emoji directory %v", emojiDir)
|
|
}
|
|
}
|
|
|
|
toPath, err := os.OpenFile(emojiDir+"/image", os.O_RDWR|os.O_CREATE, 0666)
|
|
if err != nil {
|
|
return errors.New("Error creating the image file " + err.Error())
|
|
}
|
|
defer toPath.Close()
|
|
|
|
_, err = io.Copy(toPath, fromPath)
|
|
if err != nil {
|
|
return errors.New("Error copying emojis " + err.Error())
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) exportAllDirectChannels(writer io.Writer) *model.AppError {
|
|
afterId := strings.Repeat("0", 26)
|
|
for {
|
|
channels, err := a.Srv().Store.Channel().GetAllDirectChannelsForExportAfter(1000, afterId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(channels) == 0 {
|
|
break
|
|
}
|
|
|
|
for _, channel := range channels {
|
|
afterId = channel.Id
|
|
|
|
// Skip deleted.
|
|
if channel.DeleteAt != 0 {
|
|
continue
|
|
}
|
|
|
|
channelLine := ImportLineFromDirectChannel(channel)
|
|
if err := a.exportWriteLine(writer, channelLine); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) exportAllDirectPosts(writer io.Writer) *model.AppError {
|
|
afterId := strings.Repeat("0", 26)
|
|
for {
|
|
posts, err := a.Srv().Store.Post().GetDirectPostParentsForExportAfter(1000, afterId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(posts) == 0 {
|
|
break
|
|
}
|
|
|
|
for _, post := range posts {
|
|
afterId = post.Id
|
|
|
|
// Skip deleted.
|
|
if post.DeleteAt != 0 {
|
|
continue
|
|
}
|
|
|
|
// Do the Replies.
|
|
replies, err := a.buildPostReplies(post.Id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
postLine := ImportLineForDirectPost(post)
|
|
postLine.DirectPost.Replies = replies
|
|
if err := a.exportWriteLine(writer, postLine); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|