2018-09-17 15:51:26 +01:00
|
|
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
2019-11-29 12:59:40 +01:00
|
|
|
// See LICENSE.txt for license information.
|
2018-09-17 15:51:26 +01:00
|
|
|
|
|
|
|
|
package app
|
|
|
|
|
|
|
|
|
|
import (
|
2021-02-09 11:58:31 +01:00
|
|
|
"archive/zip"
|
MM-30882: Fix read-after-write issue for demoting user (#16911)
* MM-30882: Fix read-after-write issue for demoting user
In (*App).DemoteUserToGuest, we would demote a user, and then immediately
read it back to do future operations from the user. This reading back
of the user had the effect of sticking the old value into the cache
after which it would never be updated.
There was another issue along with this, which was when the invalidation
message would broadcast across the cluster, it would hit the cache invalidation
problem where an unrelated store call would miss the cache because
it was invalidated, and then again read from replica and stick the old value.
To fix all these, we return the new value directly from the store method
to avoid having the app to read it again.
And we add a map in the localcache layer which tracks invalidations made,
and then switch to use master if it's true.
The core change is fairly limited, but due to changing the store method signatures,
a lot of code needed to be updated to pass "context.Background". Therefore the PR
just "appears" to be big, but the main changes are limited to app/user.go,
sqlstore/user_store.go and user_layer.go
https://mattermost.atlassian.net/browse/MM-30882
```release-note
Fix an issue where demoting a user to guest would not take effect in
an environment with read replicas.
```
* Fix concurrent map access
* Fixing mistakes
* fix tests
2021-02-12 19:04:05 +05:30
|
|
|
"context"
|
2018-09-17 15:51:26 +01:00
|
|
|
"encoding/json"
|
|
|
|
|
"io"
|
|
|
|
|
"net/http"
|
2018-11-19 20:13:31 +05:30
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
2018-09-17 15:51:26 +01:00
|
|
|
"strings"
|
|
|
|
|
|
2021-01-07 22:42:43 +05:30
|
|
|
"github.com/pkg/errors"
|
2019-08-01 15:19:38 +03:00
|
|
|
|
2019-11-28 14:39:38 +01:00
|
|
|
"github.com/mattermost/mattermost-server/v5/model"
|
2021-03-05 09:18:37 +01:00
|
|
|
"github.com/mattermost/mattermost-server/v5/shared/mlog"
|
2021-01-07 22:42:43 +05:30
|
|
|
"github.com/mattermost/mattermost-server/v5/store"
|
2018-09-17 15:51:26 +01:00
|
|
|
)
|
|
|
|
|
|
2021-02-09 11:58:31 +01:00
|
|
|
type BulkExportOpts struct {
|
|
|
|
|
IncludeAttachments bool
|
|
|
|
|
CreateArchive bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ExportDataDir is the name of the directory were to store additional data
|
|
|
|
|
// included with the export (e.g. file attachments).
|
|
|
|
|
const ExportDataDir = "data"
|
|
|
|
|
|
2019-02-01 12:43:41 +01:00
|
|
|
// We use this map to identify the exportable preferences.
|
2019-07-23 18:44:59 +02:00
|
|
|
// Here we link the preference category and name, to the name of the relevant field in the import struct.
|
2019-02-01 12:43:41 +01:00
|
|
|
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",
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-09 11:58:31 +01:00
|
|
|
func (a *App) BulkExport(writer io.Writer, outPath string, opts BulkExportOpts) *model.AppError {
|
|
|
|
|
var zipWr *zip.Writer
|
|
|
|
|
if opts.CreateArchive {
|
|
|
|
|
var err error
|
|
|
|
|
zipWr = zip.NewWriter(writer)
|
|
|
|
|
defer zipWr.Close()
|
|
|
|
|
writer, err = zipWr.Create("import.jsonl")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return model.NewAppError("BulkExport", "app.export.zip_create.error",
|
|
|
|
|
nil, "err="+err.Error(), http.StatusInternalServerError)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-25 12:20:49 -04:00
|
|
|
mlog.Info("Bulk export: exporting version")
|
2020-03-03 22:43:48 +01:00
|
|
|
if err := a.exportVersion(writer); err != nil {
|
2018-09-17 15:51:26 +01:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-25 12:20:49 -04:00
|
|
|
mlog.Info("Bulk export: exporting teams")
|
2020-03-03 22:43:48 +01:00
|
|
|
if err := a.exportAllTeams(writer); err != nil {
|
2018-09-17 15:51:26 +01:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-25 12:20:49 -04:00
|
|
|
mlog.Info("Bulk export: exporting channels")
|
2020-03-03 22:43:48 +01:00
|
|
|
if err := a.exportAllChannels(writer); err != nil {
|
2018-09-17 15:51:26 +01:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-25 12:20:49 -04:00
|
|
|
mlog.Info("Bulk export: exporting users")
|
2020-03-03 22:43:48 +01:00
|
|
|
if err := a.exportAllUsers(writer); err != nil {
|
2018-09-17 15:51:26 +01:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-25 12:20:49 -04:00
|
|
|
mlog.Info("Bulk export: exporting posts")
|
2021-02-09 11:58:31 +01:00
|
|
|
attachments, err := a.exportAllPosts(writer, opts.IncludeAttachments)
|
|
|
|
|
if err != nil {
|
2018-09-17 15:51:26 +01:00
|
|
|
return err
|
|
|
|
|
}
|
2019-03-15 12:28:43 -03:00
|
|
|
|
2019-07-25 12:20:49 -04:00
|
|
|
mlog.Info("Bulk export: exporting emoji")
|
2021-02-09 11:58:31 +01:00
|
|
|
emojiPaths, err := a.exportCustomEmoji(writer, outPath, "exported_emoji", !opts.CreateArchive)
|
|
|
|
|
if err != nil {
|
2018-11-19 20:13:31 +05:30
|
|
|
return err
|
|
|
|
|
}
|
2018-09-17 15:51:26 +01:00
|
|
|
|
2019-07-25 12:20:49 -04:00
|
|
|
mlog.Info("Bulk export: exporting direct channels")
|
2021-02-09 11:58:31 +01:00
|
|
|
if err = a.exportAllDirectChannels(writer); err != nil {
|
2019-03-15 12:28:43 -03:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-25 12:20:49 -04:00
|
|
|
mlog.Info("Bulk export: exporting direct posts")
|
2021-02-09 11:58:31 +01:00
|
|
|
directAttachments, err := a.exportAllDirectPosts(writer, opts.IncludeAttachments)
|
|
|
|
|
if err != nil {
|
2019-03-15 12:28:43 -03:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-09 11:58:31 +01:00
|
|
|
if opts.IncludeAttachments {
|
|
|
|
|
mlog.Info("Bulk export: exporting file attachments")
|
|
|
|
|
for _, attachment := range attachments {
|
|
|
|
|
if err := a.exportFile(outPath, *attachment.Path, zipWr); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for _, attachment := range directAttachments {
|
|
|
|
|
if err := a.exportFile(outPath, *attachment.Path, zipWr); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for _, emojiPath := range emojiPaths {
|
|
|
|
|
if err := a.exportFile(outPath, emojiPath, zipWr); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-17 15:51:26 +01:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-03 22:43:48 +01:00
|
|
|
func (a *App) exportWriteLine(writer io.Writer, line *LineImportData) *model.AppError {
|
2018-09-17 15:51:26 +01:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-03 22:43:48 +01:00
|
|
|
func (a *App) exportVersion(writer io.Writer) *model.AppError {
|
2018-09-17 15:51:26 +01:00
|
|
|
version := 1
|
|
|
|
|
versionLine := &LineImportData{
|
|
|
|
|
Type: "version",
|
|
|
|
|
Version: &version,
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-03 22:43:48 +01:00
|
|
|
return a.exportWriteLine(writer, versionLine)
|
2018-09-17 15:51:26 +01:00
|
|
|
}
|
|
|
|
|
|
2020-03-03 22:43:48 +01:00
|
|
|
func (a *App) exportAllTeams(writer io.Writer) *model.AppError {
|
2018-09-17 15:51:26 +01:00
|
|
|
afterId := strings.Repeat("0", 26)
|
|
|
|
|
for {
|
2020-02-13 13:26:58 +01:00
|
|
|
teams, err := a.Srv().Store.Team().GetAllForExportAfter(1000, afterId)
|
2019-07-08 11:10:09 +03:00
|
|
|
if err != nil {
|
2020-09-16 12:12:10 -03:00
|
|
|
return model.NewAppError("exportAllTeams", "app.team.get_all.app_error", nil, err.Error(), http.StatusInternalServerError)
|
2018-09-17 15:51:26 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(teams) == 0 {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, team := range teams {
|
|
|
|
|
afterId = team.Id
|
|
|
|
|
|
|
|
|
|
// Skip deleted.
|
|
|
|
|
if team.DeleteAt != 0 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
teamLine := ImportLineFromTeam(team)
|
2020-03-03 22:43:48 +01:00
|
|
|
if err := a.exportWriteLine(writer, teamLine); err != nil {
|
2018-09-17 15:51:26 +01:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-03 22:43:48 +01:00
|
|
|
func (a *App) exportAllChannels(writer io.Writer) *model.AppError {
|
2018-09-17 15:51:26 +01:00
|
|
|
afterId := strings.Repeat("0", 26)
|
|
|
|
|
for {
|
2020-02-13 13:26:58 +01:00
|
|
|
channels, err := a.Srv().Store.Channel().GetAllChannelsForExportAfter(1000, afterId)
|
2018-09-17 15:51:26 +01:00
|
|
|
|
2019-06-19 07:31:51 -04:00
|
|
|
if err != nil {
|
2020-10-04 01:42:29 -03:00
|
|
|
return model.NewAppError("exportAllChannels", "app.channel.get_all.app_error", nil, err.Error(), http.StatusInternalServerError)
|
2018-09-17 15:51:26 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(channels) == 0 {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, channel := range channels {
|
|
|
|
|
afterId = channel.Id
|
|
|
|
|
|
|
|
|
|
// Skip deleted.
|
|
|
|
|
if channel.DeleteAt != 0 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
channelLine := ImportLineFromChannel(channel)
|
2020-03-03 22:43:48 +01:00
|
|
|
if err := a.exportWriteLine(writer, channelLine); err != nil {
|
2018-09-17 15:51:26 +01:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-03 22:43:48 +01:00
|
|
|
func (a *App) exportAllUsers(writer io.Writer) *model.AppError {
|
2018-09-17 15:51:26 +01:00
|
|
|
afterId := strings.Repeat("0", 26)
|
|
|
|
|
for {
|
2020-02-13 13:26:58 +01:00
|
|
|
users, err := a.Srv().Store.User().GetAllAfter(1000, afterId)
|
2018-09-17 15:51:26 +01:00
|
|
|
|
2019-06-28 12:40:09 +02:00
|
|
|
if err != nil {
|
2020-10-26 06:41:27 -03:00
|
|
|
return model.NewAppError("exportAllUsers", "app.user.get.app_error", nil, err.Error(), http.StatusInternalServerError)
|
2018-09-17 15:51:26 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(users) == 0 {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, user := range users {
|
|
|
|
|
afterId = user.Id
|
|
|
|
|
|
2019-02-01 12:43:41 +01:00
|
|
|
// 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)
|
2018-09-17 15:51:26 +01:00
|
|
|
|
2018-10-24 17:34:43 +03:00
|
|
|
userLine.User.NotifyProps = a.buildUserNotifyProps(user.NotifyProps)
|
|
|
|
|
|
2018-09-17 15:51:26 +01:00
|
|
|
// Do the Team Memberships.
|
|
|
|
|
members, err := a.buildUserTeamAndChannelMemberships(user.Id)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
userLine.User.Teams = members
|
|
|
|
|
|
2020-03-03 22:43:48 +01:00
|
|
|
if err := a.exportWriteLine(writer, userLine); err != nil {
|
2018-09-17 15:51:26 +01:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-05 11:22:27 +01:00
|
|
|
func (a *App) buildUserTeamAndChannelMemberships(userID string) (*[]UserTeamImportData, *model.AppError) {
|
2018-09-17 15:51:26 +01:00
|
|
|
var memberships []UserTeamImportData
|
|
|
|
|
|
2021-02-05 11:22:27 +01:00
|
|
|
members, err := a.Srv().Store.Team().GetTeamMembersForExport(userID)
|
2018-09-17 15:51:26 +01:00
|
|
|
|
2019-07-07 13:13:43 +03:00
|
|
|
if err != nil {
|
2020-09-16 12:12:10 -03:00
|
|
|
return nil, model.NewAppError("buildUserTeamAndChannelMemberships", "app.team.get_members.app_error", nil, err.Error(), http.StatusInternalServerError)
|
2018-09-17 15:51:26 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, member := range members {
|
|
|
|
|
// Skip deleted.
|
|
|
|
|
if member.DeleteAt != 0 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
memberData := ImportUserTeamDataFromTeamMember(member)
|
|
|
|
|
|
|
|
|
|
// Do the Channel Memberships.
|
2021-02-05 11:22:27 +01:00
|
|
|
channelMembers, err := a.buildUserChannelMemberships(userID, member.TeamId)
|
2018-09-17 15:51:26 +01:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-23 18:44:59 +02:00
|
|
|
// Get the user theme
|
2020-07-28 10:27:24 +05:30
|
|
|
themePreference, nErr := a.Srv().Store.Preference().Get(member.UserId, model.PREFERENCE_CATEGORY_THEME, member.TeamId)
|
|
|
|
|
if nErr == nil {
|
2019-07-23 18:44:59 +02:00
|
|
|
memberData.Theme = &themePreference.Value
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-17 15:51:26 +01:00
|
|
|
memberData.Channels = channelMembers
|
|
|
|
|
|
|
|
|
|
memberships = append(memberships, *memberData)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &memberships, nil
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-05 11:22:27 +01:00
|
|
|
func (a *App) buildUserChannelMemberships(userID string, teamID string) (*[]UserChannelImportData, *model.AppError) {
|
2018-09-17 15:51:26 +01:00
|
|
|
var memberships []UserChannelImportData
|
|
|
|
|
|
2021-02-05 11:22:27 +01:00
|
|
|
members, nErr := a.Srv().Store.Channel().GetChannelMembersForExport(userID, teamID)
|
2020-10-04 01:42:29 -03:00
|
|
|
if nErr != nil {
|
|
|
|
|
return nil, model.NewAppError("buildUserChannelMemberships", "app.channel.get_members.app_error", nil, nErr.Error(), http.StatusInternalServerError)
|
2018-09-17 15:51:26 +01:00
|
|
|
}
|
|
|
|
|
|
2018-11-05 05:18:00 -08:00
|
|
|
category := model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL
|
2021-02-05 11:22:27 +01:00
|
|
|
preferences, err := a.GetPreferenceByCategoryForUser(userID, category)
|
2018-12-12 14:20:22 +01:00
|
|
|
if err != nil && err.StatusCode != http.StatusNotFound {
|
2018-11-05 05:18:00 -08:00
|
|
|
return nil, err
|
2018-09-17 15:51:26 +01:00
|
|
|
}
|
|
|
|
|
|
2018-11-05 05:18:00 -08:00
|
|
|
for _, member := range members {
|
|
|
|
|
memberships = append(memberships, *ImportUserChannelDataFromChannelMemberAndPreferences(member, &preferences))
|
|
|
|
|
}
|
2018-09-17 15:51:26 +01:00
|
|
|
return &memberships, nil
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-24 17:34:43 +03:00
|
|
|
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),
|
2018-12-12 14:20:22 +01:00
|
|
|
Mobile: getProp(model.PUSH_NOTIFY_PROP),
|
|
|
|
|
MobilePushStatus: getProp(model.PUSH_STATUS_NOTIFY_PROP),
|
2018-10-24 17:34:43 +03:00
|
|
|
ChannelTrigger: getProp(model.CHANNEL_MENTIONS_NOTIFY_PROP),
|
|
|
|
|
CommentsTrigger: getProp(model.COMMENTS_NOTIFY_PROP),
|
|
|
|
|
MentionKeys: getProp(model.MENTION_KEYS_NOTIFY_PROP),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-09 11:58:31 +01:00
|
|
|
func (a *App) exportAllPosts(writer io.Writer, withAttachments bool) ([]AttachmentImportData, *model.AppError) {
|
|
|
|
|
var attachments []AttachmentImportData
|
2018-09-17 15:51:26 +01:00
|
|
|
afterId := strings.Repeat("0", 26)
|
2019-07-25 12:20:49 -04:00
|
|
|
|
2018-09-17 15:51:26 +01:00
|
|
|
for {
|
2020-09-24 02:16:36 -03:00
|
|
|
posts, nErr := a.Srv().Store.Post().GetParentsForExportAfter(1000, afterId)
|
|
|
|
|
if nErr != nil {
|
2021-02-09 11:58:31 +01:00
|
|
|
return nil, model.NewAppError("exportAllPosts", "app.post.get_posts.app_error", nil, nErr.Error(), http.StatusInternalServerError)
|
2018-09-17 15:51:26 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(posts) == 0 {
|
2021-02-09 11:58:31 +01:00
|
|
|
return attachments, nil
|
2018-09-17 15:51:26 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, post := range posts {
|
|
|
|
|
afterId = post.Id
|
|
|
|
|
|
|
|
|
|
// Skip deleted.
|
|
|
|
|
if post.DeleteAt != 0 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
postLine := ImportLineForPost(post)
|
|
|
|
|
|
2021-02-09 11:58:31 +01:00
|
|
|
replies, replyAttachments, err := a.buildPostReplies(post.Id, withAttachments)
|
2018-09-17 15:51:26 +01:00
|
|
|
if err != nil {
|
2021-02-09 11:58:31 +01:00
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if withAttachments && len(replyAttachments) > 0 {
|
|
|
|
|
attachments = append(attachments, replyAttachments...)
|
2018-09-17 15:51:26 +01:00
|
|
|
}
|
|
|
|
|
|
2021-02-09 11:58:31 +01:00
|
|
|
postLine.Post.Replies = &replies
|
2019-07-25 12:20:49 -04:00
|
|
|
postLine.Post.Reactions = &[]ReactionImportData{}
|
|
|
|
|
if post.HasReactions {
|
|
|
|
|
postLine.Post.Reactions, err = a.BuildPostReactions(post.Id)
|
|
|
|
|
if err != nil {
|
2021-02-09 11:58:31 +01:00
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(post.FileIds) > 0 {
|
|
|
|
|
postAttachments, err := a.buildPostAttachments(post.Id)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
postLine.Post.Attachments = &postAttachments
|
|
|
|
|
|
|
|
|
|
if withAttachments && len(postAttachments) > 0 {
|
|
|
|
|
attachments = append(attachments, postAttachments...)
|
2019-07-25 12:20:49 -04:00
|
|
|
}
|
2018-10-17 18:53:10 +05:30
|
|
|
}
|
|
|
|
|
|
2020-03-03 22:43:48 +01:00
|
|
|
if err := a.exportWriteLine(writer, postLine); err != nil {
|
2021-02-09 11:58:31 +01:00
|
|
|
return nil, err
|
2018-09-17 15:51:26 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-25 20:22:27 +01:00
|
|
|
func (a *App) buildPostReplies(postID string, withAttachments bool) ([]ReplyImportData, []AttachmentImportData, *model.AppError) {
|
2018-09-17 15:51:26 +01:00
|
|
|
var replies []ReplyImportData
|
2021-02-09 11:58:31 +01:00
|
|
|
var attachments []AttachmentImportData
|
2018-09-17 15:51:26 +01:00
|
|
|
|
2021-02-25 20:22:27 +01:00
|
|
|
replyPosts, nErr := a.Srv().Store.Post().GetRepliesForExport(postID)
|
2020-09-24 02:16:36 -03:00
|
|
|
if nErr != nil {
|
2021-02-09 11:58:31 +01:00
|
|
|
return nil, nil, model.NewAppError("buildPostReplies", "app.post.get_posts.app_error", nil, nErr.Error(), http.StatusInternalServerError)
|
2018-09-17 15:51:26 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, reply := range replyPosts {
|
2018-10-17 18:53:10 +05:30
|
|
|
replyImportObject := ImportReplyFromPost(reply)
|
2019-07-25 12:20:49 -04:00
|
|
|
if reply.HasReactions {
|
2020-09-24 02:16:36 -03:00
|
|
|
var appErr *model.AppError
|
|
|
|
|
replyImportObject.Reactions, appErr = a.BuildPostReactions(reply.Id)
|
|
|
|
|
if appErr != nil {
|
2021-02-09 11:58:31 +01:00
|
|
|
return nil, nil, appErr
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if len(reply.FileIds) > 0 {
|
|
|
|
|
postAttachments, appErr := a.buildPostAttachments(reply.Id)
|
|
|
|
|
if appErr != nil {
|
|
|
|
|
return nil, nil, appErr
|
|
|
|
|
}
|
|
|
|
|
replyImportObject.Attachments = &attachments
|
|
|
|
|
if withAttachments && len(postAttachments) > 0 {
|
|
|
|
|
attachments = append(attachments, postAttachments...)
|
2018-10-17 18:53:10 +05:30
|
|
|
}
|
|
|
|
|
}
|
2021-02-09 11:58:31 +01:00
|
|
|
|
2018-10-17 18:53:10 +05:30
|
|
|
replies = append(replies, *replyImportObject)
|
2018-09-17 15:51:26 +01:00
|
|
|
}
|
|
|
|
|
|
2021-02-09 11:58:31 +01:00
|
|
|
return replies, attachments, nil
|
2018-09-17 15:51:26 +01:00
|
|
|
}
|
2018-10-17 18:53:10 +05:30
|
|
|
|
2021-02-25 20:22:27 +01:00
|
|
|
func (a *App) BuildPostReactions(postID string) (*[]ReactionImportData, *model.AppError) {
|
2018-10-17 18:53:10 +05:30
|
|
|
var reactionsOfPost []ReactionImportData
|
|
|
|
|
|
2021-02-25 20:22:27 +01:00
|
|
|
reactions, nErr := a.Srv().Store.Reaction().GetForPost(postID, true)
|
2020-07-02 09:43:28 +05:30
|
|
|
if nErr != nil {
|
|
|
|
|
return nil, model.NewAppError("BuildPostReactions", "app.reaction.get_for_post.app_error", nil, nErr.Error(), http.StatusInternalServerError)
|
2018-10-17 18:53:10 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, reaction := range reactions {
|
MM-30882: Fix read-after-write issue for demoting user (#16911)
* MM-30882: Fix read-after-write issue for demoting user
In (*App).DemoteUserToGuest, we would demote a user, and then immediately
read it back to do future operations from the user. This reading back
of the user had the effect of sticking the old value into the cache
after which it would never be updated.
There was another issue along with this, which was when the invalidation
message would broadcast across the cluster, it would hit the cache invalidation
problem where an unrelated store call would miss the cache because
it was invalidated, and then again read from replica and stick the old value.
To fix all these, we return the new value directly from the store method
to avoid having the app to read it again.
And we add a map in the localcache layer which tracks invalidations made,
and then switch to use master if it's true.
The core change is fairly limited, but due to changing the store method signatures,
a lot of code needed to be updated to pass "context.Background". Therefore the PR
just "appears" to be big, but the main changes are limited to app/user.go,
sqlstore/user_store.go and user_layer.go
https://mattermost.atlassian.net/browse/MM-30882
```release-note
Fix an issue where demoting a user to guest would not take effect in
an environment with read replicas.
```
* Fix concurrent map access
* Fixing mistakes
* fix tests
2021-02-12 19:04:05 +05:30
|
|
|
user, err := a.Srv().Store.User().Get(context.Background(), reaction.UserId)
|
2019-04-15 22:53:52 +02:00
|
|
|
if err != nil {
|
2020-10-26 06:41:27 -03:00
|
|
|
var nfErr *store.ErrNotFound
|
|
|
|
|
if errors.As(err, &nfErr) { // this is a valid case, the user that reacted might've been deleted by now
|
2019-10-16 03:01:46 +05:30
|
|
|
mlog.Info("Skipping reactions by user since the entity doesn't exist anymore", mlog.String("user_id", reaction.UserId))
|
2019-08-01 15:19:38 +03:00
|
|
|
continue
|
|
|
|
|
}
|
2020-10-26 06:41:27 -03:00
|
|
|
return nil, model.NewAppError("BuildPostReactions", "app.user.get.app_error", nil, err.Error(), http.StatusInternalServerError)
|
2018-12-12 14:20:22 +01:00
|
|
|
}
|
|
|
|
|
reactionsOfPost = append(reactionsOfPost, *ImportReactionFromPost(user, reaction))
|
2018-10-17 18:53:10 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &reactionsOfPost, nil
|
|
|
|
|
|
|
|
|
|
}
|
2018-11-19 20:13:31 +05:30
|
|
|
|
2021-02-25 20:22:27 +01:00
|
|
|
func (a *App) buildPostAttachments(postID string) ([]AttachmentImportData, *model.AppError) {
|
|
|
|
|
infos, nErr := a.Srv().Store.FileInfo().GetForPost(postID, false, false, false)
|
2021-02-09 11:58:31 +01:00
|
|
|
if nErr != nil {
|
|
|
|
|
return nil, model.NewAppError("buildPostAttachments", "app.file_info.get_for_post.app_error", nil, nErr.Error(), http.StatusInternalServerError)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
attachments := make([]AttachmentImportData, 0, len(infos))
|
|
|
|
|
for _, info := range infos {
|
|
|
|
|
attachments = append(attachments, AttachmentImportData{Path: &info.Path})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return attachments, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *App) exportCustomEmoji(writer io.Writer, outPath, exportDir string, exportFiles bool) ([]string, *model.AppError) {
|
|
|
|
|
var emojiPaths []string
|
2018-11-19 20:13:31 +05:30
|
|
|
pageNumber := 0
|
|
|
|
|
for {
|
|
|
|
|
customEmojiList, err := a.GetEmojiList(pageNumber, 100, model.EMOJI_SORT_BY_NAME)
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
2021-02-09 11:58:31 +01:00
|
|
|
return nil, err
|
2018-11-19 20:13:31 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(customEmojiList) == 0 {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pageNumber++
|
|
|
|
|
|
2021-02-09 11:58:31 +01:00
|
|
|
emojiPath := filepath.Join(*a.Config().FileSettings.Directory, "emoji")
|
|
|
|
|
pathToDir := filepath.Join(outPath, exportDir)
|
|
|
|
|
if exportFiles {
|
|
|
|
|
if _, err := os.Stat(pathToDir); os.IsNotExist(err) {
|
|
|
|
|
os.Mkdir(pathToDir, os.ModePerm)
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-11-19 20:13:31 +05:30
|
|
|
|
|
|
|
|
for _, emoji := range customEmojiList {
|
2021-02-09 11:58:31 +01:00
|
|
|
emojiImagePath := filepath.Join(emojiPath, emoji.Id, "image")
|
|
|
|
|
filePath := filepath.Join(exportDir, emoji.Id, "image")
|
|
|
|
|
if exportFiles {
|
|
|
|
|
err := a.copyEmojiImages(emoji.Id, emojiImagePath, pathToDir)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, model.NewAppError("BulkExport", "app.export.export_custom_emoji.copy_emoji_images.error", nil, "err="+err.Error(), http.StatusBadRequest)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
filePath = filepath.Join("emoji", emoji.Id, "image")
|
|
|
|
|
emojiPaths = append(emojiPaths, filePath)
|
2018-11-19 20:13:31 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
emojiImportObject := ImportLineFromEmoji(emoji, filePath)
|
2020-03-03 22:43:48 +01:00
|
|
|
if err := a.exportWriteLine(writer, emojiImportObject); err != nil {
|
2021-02-09 11:58:31 +01:00
|
|
|
return nil, err
|
2018-11-19 20:13:31 +05:30
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-09 11:58:31 +01:00
|
|
|
return emojiPaths, nil
|
2018-11-19 20:13:31 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
|
2019-01-25 17:33:21 +01:00
|
|
|
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)
|
|
|
|
|
}
|
2018-11-19 20:13:31 +05:30
|
|
|
}
|
2019-01-25 17:33:21 +01:00
|
|
|
|
2018-11-19 20:13:31 +05:30
|
|
|
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
|
|
|
|
|
}
|
2019-03-15 12:28:43 -03:00
|
|
|
|
2020-03-03 22:43:48 +01:00
|
|
|
func (a *App) exportAllDirectChannels(writer io.Writer) *model.AppError {
|
2019-03-15 12:28:43 -03:00
|
|
|
afterId := strings.Repeat("0", 26)
|
|
|
|
|
for {
|
2020-02-13 13:26:58 +01:00
|
|
|
channels, err := a.Srv().Store.Channel().GetAllDirectChannelsForExportAfter(1000, afterId)
|
2019-06-20 11:01:49 -04:00
|
|
|
if err != nil {
|
2020-10-04 01:42:29 -03:00
|
|
|
return model.NewAppError("exportAllDirectChannels", "app.channel.get_all_direct.app_error", nil, err.Error(), http.StatusInternalServerError)
|
2019-03-15 12:28:43 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(channels) == 0 {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, channel := range channels {
|
|
|
|
|
afterId = channel.Id
|
|
|
|
|
|
|
|
|
|
// Skip deleted.
|
|
|
|
|
if channel.DeleteAt != 0 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
channelLine := ImportLineFromDirectChannel(channel)
|
2020-03-03 22:43:48 +01:00
|
|
|
if err := a.exportWriteLine(writer, channelLine); err != nil {
|
2019-03-15 12:28:43 -03:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-09 11:58:31 +01:00
|
|
|
func (a *App) exportAllDirectPosts(writer io.Writer, withAttachments bool) ([]AttachmentImportData, *model.AppError) {
|
|
|
|
|
var attachments []AttachmentImportData
|
2019-03-15 12:28:43 -03:00
|
|
|
afterId := strings.Repeat("0", 26)
|
|
|
|
|
for {
|
2020-02-13 13:26:58 +01:00
|
|
|
posts, err := a.Srv().Store.Post().GetDirectPostParentsForExportAfter(1000, afterId)
|
2019-05-30 17:19:14 -04:00
|
|
|
if err != nil {
|
2021-02-09 11:58:31 +01:00
|
|
|
return nil, model.NewAppError("exportAllDirectPosts", "app.post.get_direct_posts.app_error", nil, err.Error(), http.StatusInternalServerError)
|
2019-03-15 12:28:43 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(posts) == 0 {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, post := range posts {
|
|
|
|
|
afterId = post.Id
|
|
|
|
|
|
|
|
|
|
// Skip deleted.
|
|
|
|
|
if post.DeleteAt != 0 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-09 11:58:31 +01:00
|
|
|
// Handle attachments.
|
|
|
|
|
var postAttachments []AttachmentImportData
|
|
|
|
|
var err *model.AppError
|
|
|
|
|
if len(post.FileIds) > 0 {
|
|
|
|
|
postAttachments, err = a.buildPostAttachments(post.Id)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if withAttachments && len(postAttachments) > 0 {
|
|
|
|
|
attachments = append(attachments, postAttachments...)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-03-15 12:28:43 -03:00
|
|
|
// Do the Replies.
|
2021-02-09 11:58:31 +01:00
|
|
|
replies, replyAttachments, err := a.buildPostReplies(post.Id, withAttachments)
|
2019-03-15 12:28:43 -03:00
|
|
|
if err != nil {
|
2021-02-09 11:58:31 +01:00
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if withAttachments && len(replyAttachments) > 0 {
|
|
|
|
|
attachments = append(attachments, replyAttachments...)
|
2019-03-15 12:28:43 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
postLine := ImportLineForDirectPost(post)
|
2021-02-09 11:58:31 +01:00
|
|
|
postLine.DirectPost.Replies = &replies
|
|
|
|
|
if len(postAttachments) > 0 {
|
|
|
|
|
postLine.DirectPost.Attachments = &postAttachments
|
|
|
|
|
}
|
2020-03-03 22:43:48 +01:00
|
|
|
if err := a.exportWriteLine(writer, postLine); err != nil {
|
2021-02-09 11:58:31 +01:00
|
|
|
return nil, err
|
2019-03-15 12:28:43 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-02-09 11:58:31 +01:00
|
|
|
return attachments, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *App) exportFile(outPath, filePath string, zipWr *zip.Writer) *model.AppError {
|
|
|
|
|
var wr io.Writer
|
|
|
|
|
var err error
|
|
|
|
|
rd, appErr := a.FileReader(filePath)
|
|
|
|
|
if appErr != nil {
|
|
|
|
|
return appErr
|
|
|
|
|
}
|
|
|
|
|
defer rd.Close()
|
|
|
|
|
|
|
|
|
|
if zipWr != nil {
|
|
|
|
|
wr, err = zipWr.CreateHeader(&zip.FileHeader{
|
|
|
|
|
Name: filepath.Join(ExportDataDir, filePath),
|
|
|
|
|
Method: zip.Store,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return model.NewAppError("exportFileAttachment", "app.export.export_attachment.zip_create_header.error",
|
|
|
|
|
nil, "err="+err.Error(), http.StatusInternalServerError)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
filePath = filepath.Join(outPath, ExportDataDir, filePath)
|
|
|
|
|
if err = os.MkdirAll(filepath.Dir(filePath), 0700); err != nil {
|
|
|
|
|
return model.NewAppError("exportFileAttachment", "app.export.export_attachment.mkdirall.error",
|
|
|
|
|
nil, "err="+err.Error(), http.StatusInternalServerError)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
wr, err = os.Create(filePath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return model.NewAppError("exportFileAttachment", "app.export.export_attachment.create_file.error",
|
|
|
|
|
nil, "err="+err.Error(), http.StatusInternalServerError)
|
|
|
|
|
}
|
|
|
|
|
defer wr.(*os.File).Close()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if _, err := io.Copy(wr, rd); err != nil {
|
|
|
|
|
return model.NewAppError("exportFileAttachment", "app.export.export_attachment.copy_file.error",
|
|
|
|
|
nil, "err="+err.Error(), http.StatusInternalServerError)
|
|
|
|
|
}
|
|
|
|
|
|
2019-03-15 12:28:43 -03:00
|
|
|
return nil
|
|
|
|
|
}
|
2021-02-09 11:58:31 +01:00
|
|
|
|
|
|
|
|
func (a *App) ListExports() ([]string, *model.AppError) {
|
|
|
|
|
exports, appErr := a.ListDirectory(*a.Config().ExportSettings.Directory)
|
|
|
|
|
if appErr != nil {
|
|
|
|
|
return nil, appErr
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
results := make([]string, len(exports))
|
|
|
|
|
for i := range exports {
|
|
|
|
|
results[i] = filepath.Base(exports[i])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return results, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *App) DeleteExport(name string) *model.AppError {
|
|
|
|
|
filePath := filepath.Join(*a.Config().ExportSettings.Directory, name)
|
|
|
|
|
|
|
|
|
|
if ok, err := a.FileExists(filePath); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
} else if !ok {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return a.RemoveFile(filePath)
|
|
|
|
|
}
|