[MM-31247] Add support for compressed export files with attachments (#16614)

* Include filepaths for post attachments

* Cleanup

* Enable exporting file attachments

* Fix file import

* Enable zip export

* Support creating missing directories when unzipping

* Add test

* Add translations

* Export direct channel posts attachments

* Fix returned values order

Remove pointer to slice in return

* [MM-31597] Implement export process job (#16626)

* Implement export process job

* Add translations

* Remove unused value

* [MM-31249] Add /exports API endpoint (#16633)

* Implement API endpoints to list, download and delete export files

* Add endpoint for single resource

* Update i18n/en.json

Co-authored-by: Ibrahim Serdar Acikgoz <serdaracikgoz86@gmail.com>

* Update i18n/en.json

Co-authored-by: Ibrahim Serdar Acikgoz <serdaracikgoz86@gmail.com>

Co-authored-by: Ibrahim Serdar Acikgoz <serdaracikgoz86@gmail.com>

* Fix var name

Co-authored-by: Ibrahim Serdar Acikgoz <serdaracikgoz86@gmail.com>
Этот коммит содержится в:
Claudio Costa
2021-02-09 11:58:31 +01:00
коммит произвёл GitHub
родитель 9a33c3706a
Коммит 572f861675
27 изменённых файлов: 1063 добавлений и 106 удалений

Просмотреть файл

@@ -123,6 +123,10 @@ func (a *App) initJobs() {
a.srv.Jobs.ImportDelete = jobsImportDeleteInterface(a)
}
if jobsExportProcessInterface != nil {
a.srv.Jobs.ExportProcess = jobsExportProcessInterface(a)
}
if jobsActiveUsersInterface != nil {
a.srv.Jobs.ActiveUsers = jobsActiveUsersInterface(a)
}

Просмотреть файл

@@ -388,7 +388,7 @@ type AppIface interface {
BuildPostReactions(postId string) (*[]ReactionImportData, *model.AppError)
BuildPushNotificationMessage(contentsConfig string, post *model.Post, user *model.User, channel *model.Channel, channelName string, senderName string, explicitMention bool, channelWideMention bool, replyToThreadType string) (*model.PushNotification, *model.AppError)
BuildSamlMetadataObject(idpMetadata []byte) (*model.SamlMetadataResponse, *model.AppError)
BulkExport(writer io.Writer, file string, pathToEmojiDir string, dirNameToExportEmoji string) *model.AppError
BulkExport(writer io.Writer, outPath string, opts BulkExportOpts) *model.AppError
BulkImport(fileReader io.Reader, dryRun bool, workers int) (*model.AppError, int)
BulkImportWithPath(fileReader io.Reader, dryRun bool, workers int, importPath string) (*model.AppError, int)
CancelJob(jobId string) *model.AppError
@@ -466,6 +466,7 @@ type AppIface interface {
DeleteCommand(commandID string) *model.AppError
DeleteEmoji(emoji *model.Emoji) *model.AppError
DeleteEphemeralPost(userID, postId string)
DeleteExport(name string) *model.AppError
DeleteFlaggedPosts(postId string)
DeleteGroup(groupID string) (*model.Group, *model.AppError)
DeleteGroupMember(groupID string, userID string) (*model.GroupMember, *model.AppError)
@@ -782,6 +783,7 @@ type AppIface interface {
LimitedClientConfig() map[string]string
ListAllCommands(teamID string, T goi18n.TranslateFunc) ([]*model.Command, *model.AppError)
ListDirectory(path string) ([]string, *model.AppError)
ListExports() ([]string, *model.AppError)
ListImports() ([]string, *model.AppError)
ListPluginKeys(pluginId string, page, perPage int) ([]string, *model.AppError)
ListTeamCommands(teamID string) ([]*model.Command, *model.AppError)

Просмотреть файл

@@ -120,6 +120,12 @@ func RegisterJobsImportDeleteInterface(f func(*App) tjobs.ImportDeleteInterface)
jobsImportDeleteInterface = f
}
var jobsExportProcessInterface func(*App) tjobs.ExportProcessInterface
func RegisterJobsExportProcessInterface(f func(*App) tjobs.ExportProcessInterface) {
jobsExportProcessInterface = f
}
var productNoticesJobInterface func(*App) tjobs.ProductNoticesJobInterface
func RegisterProductNoticesJobInterface(f func(*App) tjobs.ProductNoticesJobInterface) {

Просмотреть файл

@@ -4,6 +4,7 @@
package app
import (
"archive/zip"
"encoding/json"
"io"
"net/http"
@@ -18,6 +19,15 @@ import (
"github.com/mattermost/mattermost-server/v5/store"
)
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"
// 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{{
@@ -53,7 +63,19 @@ var exportablePreferences = map[ComparablePreference]string{{
}: "EmailInterval",
}
func (a *App) BulkExport(writer io.Writer, file string, pathToEmojiDir string, dirNameToExportEmoji string) *model.AppError {
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)
}
}
mlog.Info("Bulk export: exporting version")
if err := a.exportVersion(writer); err != nil {
return err
@@ -75,25 +97,47 @@ func (a *App) BulkExport(writer io.Writer, file string, pathToEmojiDir string, d
}
mlog.Info("Bulk export: exporting posts")
if err := a.exportAllPosts(writer); err != nil {
attachments, err := a.exportAllPosts(writer, opts.IncludeAttachments)
if err != nil {
return err
}
mlog.Info("Bulk export: exporting emoji")
if err := a.exportCustomEmoji(writer, file, pathToEmojiDir, dirNameToExportEmoji); err != nil {
emojiPaths, err := a.exportCustomEmoji(writer, outPath, "exported_emoji", !opts.CreateArchive)
if err != nil {
return err
}
mlog.Info("Bulk export: exporting direct channels")
if err := a.exportAllDirectChannels(writer); err != nil {
if err = a.exportAllDirectChannels(writer); err != nil {
return err
}
mlog.Info("Bulk export: exporting direct posts")
if err := a.exportAllDirectPosts(writer); err != nil {
directAttachments, err := a.exportAllDirectPosts(writer, opts.IncludeAttachments)
if err != nil {
return err
}
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
}
}
}
return nil
}
@@ -334,17 +378,18 @@ func (a *App) buildUserNotifyProps(notifyProps model.StringMap) *UserNotifyProps
}
}
func (a *App) exportAllPosts(writer io.Writer) *model.AppError {
func (a *App) exportAllPosts(writer io.Writer, withAttachments bool) ([]AttachmentImportData, *model.AppError) {
var attachments []AttachmentImportData
afterId := strings.Repeat("0", 26)
for {
posts, nErr := a.Srv().Store.Post().GetParentsForExportAfter(1000, afterId)
if nErr != nil {
return model.NewAppError("exportAllPosts", "app.post.get_posts.app_error", nil, nErr.Error(), http.StatusInternalServerError)
return nil, model.NewAppError("exportAllPosts", "app.post.get_posts.app_error", nil, nErr.Error(), http.StatusInternalServerError)
}
if len(posts) == 0 {
return nil
return attachments, nil
}
for _, post := range posts {
@@ -357,33 +402,50 @@ func (a *App) exportAllPosts(writer io.Writer) *model.AppError {
postLine := ImportLineForPost(post)
var err *model.AppError
postLine.Post.Replies, err = a.buildPostReplies(post.Id)
replies, replyAttachments, err := a.buildPostReplies(post.Id, withAttachments)
if err != nil {
return err
return nil, err
}
if withAttachments && len(replyAttachments) > 0 {
attachments = append(attachments, replyAttachments...)
}
postLine.Post.Replies = &replies
postLine.Post.Reactions = &[]ReactionImportData{}
if post.HasReactions {
postLine.Post.Reactions, err = a.BuildPostReactions(post.Id)
if err != nil {
return err
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...)
}
}
if err := a.exportWriteLine(writer, postLine); err != nil {
return err
return nil, err
}
}
}
}
func (a *App) buildPostReplies(postId string) (*[]ReplyImportData, *model.AppError) {
func (a *App) buildPostReplies(postId string, withAttachments bool) ([]ReplyImportData, []AttachmentImportData, *model.AppError) {
var replies []ReplyImportData
var attachments []AttachmentImportData
replyPosts, nErr := a.Srv().Store.Post().GetRepliesForExport(postId)
if nErr != nil {
return nil, model.NewAppError("buildPostReplies", "app.post.get_posts.app_error", nil, nErr.Error(), http.StatusInternalServerError)
return nil, nil, model.NewAppError("buildPostReplies", "app.post.get_posts.app_error", nil, nErr.Error(), http.StatusInternalServerError)
}
for _, reply := range replyPosts {
@@ -392,13 +454,24 @@ func (a *App) buildPostReplies(postId string) (*[]ReplyImportData, *model.AppErr
var appErr *model.AppError
replyImportObject.Reactions, appErr = a.BuildPostReactions(reply.Id)
if appErr != nil {
return nil, appErr
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...)
}
}
replies = append(replies, *replyImportObject)
}
return &replies, nil
return replies, attachments, nil
}
func (a *App) BuildPostReactions(postId string) (*[]ReactionImportData, *model.AppError) {
@@ -426,13 +499,28 @@ func (a *App) BuildPostReactions(postId string) (*[]ReactionImportData, *model.A
}
func (a *App) exportCustomEmoji(writer io.Writer, file string, pathToEmojiDir string, dirNameToExportEmoji string) *model.AppError {
func (a *App) buildPostAttachments(postId string) ([]AttachmentImportData, *model.AppError) {
infos, nErr := a.Srv().Store.FileInfo().GetForPost(postId, false, false, false)
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
pageNumber := 0
for {
customEmojiList, err := a.GetEmojiList(pageNumber, 100, model.EMOJI_SORT_BY_NAME)
if err != nil {
return err
return nil, err
}
if len(customEmojiList) == 0 {
@@ -441,42 +529,35 @@ func (a *App) exportCustomEmoji(writer io.Writer, file string, pathToEmojiDir st
pageNumber++
pathToDir := a.createDirForEmoji(file, dirNameToExportEmoji)
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)
}
}
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)
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)
}
filePath := dirNameToExportEmoji + "/" + emoji.Id + "/image"
emojiImportObject := ImportLineFromEmoji(emoji, filePath)
if err := a.exportWriteLine(writer, emojiImportObject); err != nil {
return err
return nil, 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
return emojiPaths, nil
}
// Copies emoji files from 'data/emoji' dir to 'exported_emoji' dir
@@ -543,12 +624,13 @@ func (a *App) exportAllDirectChannels(writer io.Writer) *model.AppError {
return nil
}
func (a *App) exportAllDirectPosts(writer io.Writer) *model.AppError {
func (a *App) exportAllDirectPosts(writer io.Writer, withAttachments bool) ([]AttachmentImportData, *model.AppError) {
var attachments []AttachmentImportData
afterId := strings.Repeat("0", 26)
for {
posts, err := a.Srv().Store.Post().GetDirectPostParentsForExportAfter(1000, afterId)
if err != nil {
return model.NewAppError("exportAllDirectPosts", "app.post.get_direct_posts.app_error", nil, err.Error(), http.StatusInternalServerError)
return nil, model.NewAppError("exportAllDirectPosts", "app.post.get_direct_posts.app_error", nil, err.Error(), http.StatusInternalServerError)
}
if len(posts) == 0 {
@@ -563,18 +645,106 @@ func (a *App) exportAllDirectPosts(writer io.Writer) *model.AppError {
continue
}
// 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...)
}
}
// Do the Replies.
replies, err := a.buildPostReplies(post.Id)
replies, replyAttachments, err := a.buildPostReplies(post.Id, withAttachments)
if err != nil {
return err
return nil, err
}
if withAttachments && len(replyAttachments) > 0 {
attachments = append(attachments, replyAttachments...)
}
postLine := ImportLineForDirectPost(post)
postLine.DirectPost.Replies = replies
postLine.DirectPost.Replies = &replies
if len(postAttachments) > 0 {
postLine.DirectPost.Attachments = &postAttachments
}
if err := a.exportWriteLine(writer, postLine); err != nil {
return err
return nil, err
}
}
}
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)
}
return nil
}
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)
}

Просмотреть файл

@@ -5,7 +5,9 @@ package app
import (
"bytes"
"io/ioutil"
"os"
"path/filepath"
"sort"
"testing"
@@ -13,6 +15,8 @@ import (
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v5/model"
"github.com/mattermost/mattermost-server/v5/utils"
"github.com/mattermost/mattermost-server/v5/utils/fileutils"
)
func TestReactionsOfPost(t *testing.T) {
@@ -111,16 +115,6 @@ func TestExportUserChannels(t *testing.T) {
}
}
func TestDirCreationForEmoji(t *testing.T) {
th := SetupWithStoreMock(t)
defer th.TearDown()
pathToDir := th.App.createDirForEmoji("test.json", "exported_emoji_test")
defer os.Remove(pathToDir)
_, err := os.Stat(pathToDir)
require.False(t, os.IsNotExist(err), "Directory exported_emoji_test should exist")
}
func TestCopyEmojiImages(t *testing.T) {
th := SetupWithStoreMock(t)
defer th.TearDown()
@@ -164,11 +158,13 @@ func TestExportCustomEmoji(t *testing.T) {
require.Nil(t, err)
defer os.Remove(filePath)
pathToEmojiDir := "../data/emoji/"
dirNameToExportEmoji := "exported_emoji_test"
defer os.RemoveAll("../" + dirNameToExportEmoji)
err = th.App.exportCustomEmoji(fileWriter, filePath, pathToEmojiDir, dirNameToExportEmoji)
outPath, err := filepath.Abs(filePath)
require.Nil(t, err)
_, err = th.App.exportCustomEmoji(fileWriter, outPath, dirNameToExportEmoji, false)
require.Nil(t, err, "should not have failed")
}
@@ -182,7 +178,7 @@ func TestExportAllUsers(t *testing.T) {
require.Nil(t, err)
var b bytes.Buffer
err = th1.App.BulkExport(&b, "somefile", "somePath", "someDir")
err = th1.App.BulkExport(&b, "somePath", BulkExportOpts{})
require.Nil(t, err)
th2 := Setup(t)
@@ -228,7 +224,7 @@ func TestExportDMChannel(t *testing.T) {
th1.CreateDmChannel(th1.BasicUser2)
var b bytes.Buffer
err := th1.App.BulkExport(&b, "somefile", "somePath", "someDir")
err := th1.App.BulkExport(&b, "somePath", BulkExportOpts{})
require.Nil(t, err)
channels, nErr := th1.App.Srv().Store.Channel().GetAllDirectChannelsForExportAfter(1000, "00000000")
@@ -264,7 +260,7 @@ func TestExportDMChannelToSelf(t *testing.T) {
th1.CreateDmChannel(th1.BasicUser)
var b bytes.Buffer
err := th1.App.BulkExport(&b, "somefile", "somePath", "someDir")
err := th1.App.BulkExport(&b, "somePath", BulkExportOpts{})
require.Nil(t, err)
channels, nErr := th1.App.Srv().Store.Channel().GetAllDirectChannelsForExportAfter(1000, "00000000")
@@ -302,7 +298,7 @@ func TestExportGMChannel(t *testing.T) {
th1.CreateGroupChannel(user1, user2)
var b bytes.Buffer
err := th1.App.BulkExport(&b, "somefile", "somePath", "someDir")
err := th1.App.BulkExport(&b, "somePath", BulkExportOpts{})
require.Nil(t, err)
channels, nErr := th1.App.Srv().Store.Channel().GetAllDirectChannelsForExportAfter(1000, "00000000")
@@ -334,7 +330,7 @@ func TestExportGMandDMChannels(t *testing.T) {
th1.CreateGroupChannel(user1, user2)
var b bytes.Buffer
err := th1.App.BulkExport(&b, "somefile", "somePath", "someDir")
err := th1.App.BulkExport(&b, "somePath", BulkExportOpts{})
require.Nil(t, err)
channels, nErr := th1.App.Srv().Store.Channel().GetAllDirectChannelsForExportAfter(1000, "00000000")
@@ -417,7 +413,7 @@ func TestExportDMandGMPost(t *testing.T) {
assert.Equal(t, 4, len(posts))
var b bytes.Buffer
err = th1.App.BulkExport(&b, "somefile", "somePath", "someDir")
err = th1.App.BulkExport(&b, "somePath", BulkExportOpts{})
require.Nil(t, err)
th1.TearDown()
@@ -492,7 +488,7 @@ func TestExportPostWithProps(t *testing.T) {
require.NotEmpty(t, posts[1].Props)
var b bytes.Buffer
err = th1.App.BulkExport(&b, "somefile", "somePath", "someDir")
err = th1.App.BulkExport(&b, "somePath", BulkExportOpts{})
require.Nil(t, err)
th1.TearDown()
@@ -530,7 +526,7 @@ func TestExportDMPostWithSelf(t *testing.T) {
th1.CreatePost(dmChannel)
var b bytes.Buffer
err := th1.App.BulkExport(&b, "somefile", "somePath", "someDir")
err := th1.App.BulkExport(&b, "somePath", BulkExportOpts{})
require.Nil(t, err)
posts, nErr := th1.App.Srv().Store.Post().GetDirectPostParentsForExportAfter(1000, "0000000")
@@ -557,3 +553,57 @@ func TestExportDMPostWithSelf(t *testing.T) {
assert.Equal(t, 1, len((*posts[0].ChannelMembers)))
assert.Equal(t, th1.BasicUser.Username, (*posts[0].ChannelMembers)[0])
}
func TestBulkExport(t *testing.T) {
th := Setup(t)
testsDir, _ := fileutils.FindDir("tests")
dir, err := ioutil.TempDir("", "import_test")
require.Nil(t, err)
defer os.RemoveAll(dir)
extractImportFile := func(filePath string) *os.File {
importFile, err2 := os.Open(filePath)
require.Nil(t, err2)
defer importFile.Close()
info, err2 := importFile.Stat()
require.Nil(t, err2)
paths, err2 := utils.UnzipToPath(importFile, info.Size(), dir)
require.Nil(t, err2)
require.NotEmpty(t, paths)
jsonFile, err2 := os.Open(filepath.Join(dir, "import.jsonl"))
require.Nil(t, err2)
return jsonFile
}
jsonFile := extractImportFile(filepath.Join(testsDir, "import_test.zip"))
defer jsonFile.Close()
err, _ = th.App.BulkImportWithPath(jsonFile, false, 1, dir)
require.Nil(t, err)
exportFile, err := os.Create(filepath.Join(dir, "export.zip"))
require.Nil(t, err)
defer exportFile.Close()
opts := BulkExportOpts{
IncludeAttachments: true,
CreateArchive: true,
}
err = th.App.BulkExport(exportFile, dir, opts)
require.Nil(t, err)
th.TearDown()
th = Setup(t)
defer th.TearDown()
jsonFile = extractImportFile(filepath.Join(dir, "export.zip"))
defer jsonFile.Close()
err, _ = th.App.BulkImportWithPath(jsonFile, false, 1, filepath.Join(dir, "data"))
require.Nil(t, err)
}

Просмотреть файл

@@ -9,7 +9,7 @@ import (
"crypto/sha1"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"path"
@@ -1117,10 +1117,15 @@ func (a *App) importAttachment(data *AttachmentImportData, post *model.Post, tea
if file == nil || err != nil {
return nil, model.NewAppError("BulkImport", "app.import.attachment.bad_file.error", map[string]interface{}{"FilePath": *data.Path}, "", http.StatusBadRequest)
}
defer file.Close()
timestamp := utils.TimeFromMillis(post.CreateAt)
buf := bytes.NewBuffer(nil)
_, _ = io.Copy(buf, file)
fileData, err := ioutil.ReadAll(file)
if err != nil {
return nil, model.NewAppError("BulkImport", "app.import.attachment.read_file_data.error", map[string]interface{}{"FilePath": *data.Path}, "", http.StatusBadRequest)
}
// Go over existing files in the post and see if there already exists a file with the same name, size and hash. If so - skip it
if post.Id != "" {
oldFiles, err := a.GetFileInfosForPost(post.Id, true)
@@ -1128,11 +1133,11 @@ func (a *App) importAttachment(data *AttachmentImportData, post *model.Post, tea
return nil, model.NewAppError("BulkImport", "app.import.attachment.file_upload.error", map[string]interface{}{"FilePath": *data.Path}, "", http.StatusBadRequest)
}
for _, oldFile := range oldFiles {
if oldFile.Name != path.Base(file.Name()) || oldFile.Size != int64(buf.Len()) {
if oldFile.Name != path.Base(file.Name()) || oldFile.Size != int64(len(fileData)) {
continue
}
// check md5
newHash := sha1.Sum(buf.Bytes())
newHash := sha1.Sum(fileData)
oldFileData, err := a.GetFile(oldFile.Id)
if err != nil {
return nil, model.NewAppError("BulkImport", "app.import.attachment.file_upload.error", map[string]interface{}{"FilePath": *data.Path}, "", http.StatusBadRequest)
@@ -1145,15 +1150,19 @@ func (a *App) importAttachment(data *AttachmentImportData, post *model.Post, tea
}
}
}
fileInfo, appErr := a.DoUploadFile(timestamp, teamID, post.ChannelId, post.UserId, file.Name(), buf.Bytes())
mlog.Info("Uploading file with name", mlog.String("file_name", file.Name()))
fileInfo, appErr := a.DoUploadFile(timestamp, teamID, post.ChannelId, post.UserId, file.Name(), fileData)
if appErr != nil {
mlog.Error("Failed to upload file:", mlog.Err(appErr))
return nil, appErr
}
a.HandleImages([]string{fileInfo.PreviewPath}, []string{fileInfo.ThumbnailPath}, [][]byte{buf.Bytes()})
if fileInfo.IsImage() {
a.HandleImages([]string{fileInfo.PreviewPath}, []string{fileInfo.ThumbnailPath}, [][]byte{fileData})
}
mlog.Info("Uploading file with name", mlog.String("file_name", file.Name()))
return fileInfo, nil
}

Просмотреть файл

@@ -878,7 +878,7 @@ func (a *OpenTracingAppLayer) BuildSamlMetadataObject(idpMetadata []byte) (*mode
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) BulkExport(writer io.Writer, file string, pathToEmojiDir string, dirNameToExportEmoji string) *model.AppError {
func (a *OpenTracingAppLayer) BulkExport(writer io.Writer, outPath string, opts app.BulkExportOpts) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.BulkExport")
@@ -890,7 +890,7 @@ func (a *OpenTracingAppLayer) BulkExport(writer io.Writer, file string, pathToEm
}()
defer span.Finish()
resultVar0 := a.app.BulkExport(writer, file, pathToEmojiDir, dirNameToExportEmoji)
resultVar0 := a.app.BulkExport(writer, outPath, opts)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
@@ -2756,6 +2756,28 @@ func (a *OpenTracingAppLayer) DeleteEphemeralPost(userID string, postId string)
a.app.DeleteEphemeralPost(userID, postId)
}
func (a *OpenTracingAppLayer) DeleteExport(name string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteExport")
a.ctx = newCtx
a.app.Srv().Store.SetContext(newCtx)
defer func() {
a.app.Srv().Store.SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeleteExport(name)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DeleteFlaggedPosts(postId string) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteFlaggedPosts")
@@ -10423,6 +10445,28 @@ func (a *OpenTracingAppLayer) ListDirectory(path string) ([]string, *model.AppEr
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) ListExports() ([]string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ListExports")
a.ctx = newCtx
a.app.Srv().Store.SetContext(newCtx)
defer func() {
a.app.Srv().Store.SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.ListExports()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) ListImports() ([]string, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ListImports")