MM-31612 Optimize bulk import process by avoiding writing zip to disk (#17659)

Этот коммит содержится в:
Ian Whitlock
2021-06-09 11:11:46 -05:00
коммит произвёл GitHub
родитель 72c86448b9
Коммит fe94a20ce7
12 изменённых файлов: 281 добавлений и 90 удалений

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

@@ -7,6 +7,7 @@
package app package app
import ( import (
"archive/zip"
"bytes" "bytes"
"context" "context"
"crypto/ecdsa" "crypto/ecdsa"
@@ -424,8 +425,8 @@ type AppIface interface {
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) 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) BuildSamlMetadataObject(idpMetadata []byte) (*model.SamlMetadataResponse, *model.AppError)
BulkExport(writer io.Writer, outPath string, opts BulkExportOpts) *model.AppError BulkExport(writer io.Writer, outPath string, opts BulkExportOpts) *model.AppError
BulkImport(c *request.Context, fileReader io.Reader, dryRun bool, workers int) (*model.AppError, int) BulkImport(c *request.Context, jsonlReader io.Reader, attachmentsReader *zip.Reader, dryRun bool, workers int) (*model.AppError, int)
BulkImportWithPath(c *request.Context, fileReader io.Reader, dryRun bool, workers int, importPath string) (*model.AppError, int) BulkImportWithPath(c *request.Context, jsonlReader io.Reader, attachmentsReader *zip.Reader, dryRun bool, workers int, importPath string) (*model.AppError, int)
CancelJob(jobId string) *model.AppError CancelJob(jobId string) *model.AppError
ChannelMembersToRemove(teamID *string) ([]*model.ChannelMember, *model.AppError) ChannelMembersToRemove(teamID *string) ([]*model.ChannelMember, *model.AppError)
CheckAndSendUserLimitWarningEmails(c *request.Context) *model.AppError CheckAndSendUserLimitWarningEmails(c *request.Context) *model.AppError

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

@@ -183,7 +183,7 @@ func TestExportAllUsers(t *testing.T) {
th2 := Setup(t) th2 := Setup(t)
defer th2.TearDown() defer th2.TearDown()
err, i := th2.App.BulkImport(th2.Context, &b, false, 5) err, i := th2.App.BulkImport(th2.Context, &b, nil, false, 5)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 0, i) assert.Equal(t, 0, i)
@@ -241,7 +241,7 @@ func TestExportDMChannel(t *testing.T) {
assert.Equal(t, 0, len(channels)) assert.Equal(t, 0, len(channels))
// import the exported channel // import the exported channel
err, i := th2.App.BulkImport(th2.Context, &b, false, 5) err, i := th2.App.BulkImport(th2.Context, &b, nil, false, 5)
require.Nil(t, err) require.Nil(t, err)
assert.Equal(t, 0, i) assert.Equal(t, 0, i)
@@ -274,7 +274,7 @@ func TestExportDMChannel(t *testing.T) {
defer th2.TearDown() defer th2.TearDown()
// import the exported channel // import the exported channel
err, _ = th2.App.BulkImport(th2.Context, &b, true, 5) err, _ = th2.App.BulkImport(th2.Context, &b, nil, true, 5)
require.Nil(t, err) require.Nil(t, err)
channels, nErr = th2.App.Srv().Store.Channel().GetAllDirectChannelsForExportAfter(1000, "00000000") channels, nErr = th2.App.Srv().Store.Channel().GetAllDirectChannelsForExportAfter(1000, "00000000")
@@ -306,7 +306,7 @@ func TestExportDMChannelToSelf(t *testing.T) {
assert.Equal(t, 0, len(channels)) assert.Equal(t, 0, len(channels))
// import the exported channel // import the exported channel
err, i := th2.App.BulkImport(th2.Context, &b, false, 5) err, i := th2.App.BulkImport(th2.Context, &b, nil, false, 5)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 0, i) assert.Equal(t, 0, i)
@@ -378,7 +378,7 @@ func TestExportGMandDMChannels(t *testing.T) {
assert.Equal(t, 0, len(channels)) assert.Equal(t, 0, len(channels))
// import the exported channel // import the exported channel
err, i := th2.App.BulkImport(th2.Context, &b, false, 5) err, i := th2.App.BulkImport(th2.Context, &b, nil, false, 5)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 0, i) assert.Equal(t, 0, i)
@@ -457,7 +457,7 @@ func TestExportDMandGMPost(t *testing.T) {
assert.Equal(t, 0, len(posts)) assert.Equal(t, 0, len(posts))
// import the exported posts // import the exported posts
appErr, i := th2.App.BulkImport(th2.Context, &b, false, 5) appErr, i := th2.App.BulkImport(th2.Context, &b, nil, false, 5)
assert.Nil(t, appErr) assert.Nil(t, appErr)
assert.Equal(t, 0, i) assert.Equal(t, 0, i)
@@ -532,7 +532,7 @@ func TestExportPostWithProps(t *testing.T) {
assert.Len(t, posts, 0) assert.Len(t, posts, 0)
// import the exported posts // import the exported posts
appErr, i := th2.App.BulkImport(th2.Context, &b, false, 5) appErr, i := th2.App.BulkImport(th2.Context, &b, nil, false, 5)
assert.Nil(t, appErr) assert.Nil(t, appErr)
assert.Equal(t, 0, i) assert.Equal(t, 0, i)
@@ -574,7 +574,7 @@ func TestExportDMPostWithSelf(t *testing.T) {
assert.Equal(t, 0, len(posts)) assert.Equal(t, 0, len(posts))
// import the exported posts // import the exported posts
err, i := th2.App.BulkImport(th2.Context, &b, false, 5) err, i := th2.App.BulkImport(th2.Context, &b, nil, false, 5)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 0, i) assert.Equal(t, 0, i)
@@ -614,7 +614,7 @@ func TestBulkExport(t *testing.T) {
jsonFile := extractImportFile(filepath.Join(testsDir, "import_test.zip")) jsonFile := extractImportFile(filepath.Join(testsDir, "import_test.zip"))
defer jsonFile.Close() defer jsonFile.Close()
appErr, _ := th.App.BulkImportWithPath(th.Context, jsonFile, false, 1, dir) appErr, _ := th.App.BulkImportWithPath(th.Context, jsonFile, nil, false, 1, dir)
require.Nil(t, appErr) require.Nil(t, appErr)
exportFile, err := os.Create(filepath.Join(dir, "export.zip")) exportFile, err := os.Create(filepath.Join(dir, "export.zip"))
@@ -635,6 +635,6 @@ func TestBulkExport(t *testing.T) {
jsonFile = extractImportFile(filepath.Join(dir, "export.zip")) jsonFile = extractImportFile(filepath.Join(dir, "export.zip"))
defer jsonFile.Close() defer jsonFile.Close()
appErr, _ = th.App.BulkImportWithPath(th.Context, jsonFile, false, 1, filepath.Join(dir, "data")) appErr, _ = th.App.BulkImportWithPath(th.Context, jsonFile, nil, false, 1, filepath.Join(dir, "data"))
require.Nil(t, appErr) require.Nil(t, appErr)
} }

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

@@ -4,9 +4,11 @@
package app package app
import ( import (
"archive/zip"
"bufio" "bufio"
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt"
"io" "io"
"net/http" "net/http"
"path/filepath" "path/filepath"
@@ -123,16 +125,20 @@ func (a *App) bulkImportWorker(c *request.Context, dryRun bool, wg *sync.WaitGro
wg.Done() wg.Done()
} }
func (a *App) BulkImport(c *request.Context, fileReader io.Reader, dryRun bool, workers int) (*model.AppError, int) { func (a *App) BulkImport(c *request.Context, jsonlReader io.Reader, attachmentsReader *zip.Reader, dryRun bool, workers int) (*model.AppError, int) {
return a.bulkImport(c, fileReader, dryRun, workers, "") return a.bulkImport(c, jsonlReader, attachmentsReader, dryRun, workers, "")
} }
func (a *App) BulkImportWithPath(c *request.Context, fileReader io.Reader, dryRun bool, workers int, importPath string) (*model.AppError, int) { func (a *App) BulkImportWithPath(c *request.Context, jsonlReader io.Reader, attachmentsReader *zip.Reader, dryRun bool, workers int, importPath string) (*model.AppError, int) {
return a.bulkImport(c, fileReader, dryRun, workers, importPath) return a.bulkImport(c, jsonlReader, attachmentsReader, dryRun, workers, importPath)
} }
func (a *App) bulkImport(c *request.Context, fileReader io.Reader, dryRun bool, workers int, importPath string) (*model.AppError, int) { // bulkImport will extract attachments from attachmentsReader if it is
scanner := bufio.NewScanner(fileReader) // not nil. If it is nil, it will look for attachments on the
// filesystem in the locations specified by the JSONL file according
// to the older behavior
func (a *App) bulkImport(c *request.Context, jsonlReader io.Reader, attachmentsReader *zip.Reader, dryRun bool, workers int, importPath string) (*model.AppError, int) {
scanner := bufio.NewScanner(jsonlReader)
buf := make([]byte, 0, 64*1024) buf := make([]byte, 0, 64*1024)
scanner.Buffer(buf, maxScanTokenSize) scanner.Buffer(buf, maxScanTokenSize)
@@ -146,6 +152,14 @@ func (a *App) bulkImport(c *request.Context, fileReader io.Reader, dryRun bool,
var linesChan chan LineImportWorkerData var linesChan chan LineImportWorkerData
lastLineType := "" lastLineType := ""
var attachedFiles map[string]*zip.File
if attachmentsReader != nil {
attachedFiles = make(map[string]*zip.File, len(attachmentsReader.File))
for _, fi := range attachmentsReader.File {
attachedFiles[fi.Name] = fi
}
}
for scanner.Scan() { for scanner.Scan() {
decoder := json.NewDecoder(bytes.NewReader(scanner.Bytes())) decoder := json.NewDecoder(bytes.NewReader(scanner.Bytes()))
lineNumber++ lineNumber++
@@ -155,6 +169,16 @@ func (a *App) bulkImport(c *request.Context, fileReader io.Reader, dryRun bool,
return model.NewAppError("BulkImport", "app.import.bulk_import.json_decode.error", nil, err.Error(), http.StatusBadRequest), lineNumber return model.NewAppError("BulkImport", "app.import.bulk_import.json_decode.error", nil, err.Error(), http.StatusBadRequest), lineNumber
} }
if len(attachedFiles) > 0 && line.Post != nil && line.Post.Attachments != nil {
for i, attachment := range *line.Post.Attachments {
var ok bool
path := *attachment.Path
if (*line.Post.Attachments)[i].Data, ok = attachedFiles[path]; !ok {
return model.NewAppError("BulkImport", "app.import.bulk_import.json_decode.error", nil, fmt.Sprintf("attachment '%s' not found in map", path), http.StatusBadRequest), lineNumber
}
}
}
if importPath != "" { if importPath != "" {
rewriteFilePaths(&line, importPath) rewriteFilePaths(&line, importPath)
} }

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

@@ -9,6 +9,7 @@ import (
"crypto/sha1" "crypto/sha1"
"errors" "errors"
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"os" "os"
@@ -1146,11 +1147,27 @@ func (a *App) importReplies(c *request.Context, data []ReplyImportData, post *mo
} }
func (a *App) importAttachment(c *request.Context, data *AttachmentImportData, post *model.Post, teamID string) (*model.FileInfo, *model.AppError) { func (a *App) importAttachment(c *request.Context, data *AttachmentImportData, post *model.Post, teamID string) (*model.FileInfo, *model.AppError) {
file, err := os.Open(*data.Path) var (
if file == nil || err != nil { name string
return nil, model.NewAppError("BulkImport", "app.import.attachment.bad_file.error", map[string]interface{}{"FilePath": *data.Path}, "", http.StatusBadRequest) file io.Reader
)
if data.Data != nil {
zipFile, err := data.Data.Open()
if err != nil {
return nil, model.NewAppError("BulkImport", "app.import.attachment.bad_file.error", map[string]interface{}{"FilePath": *data.Path}, err.Error(), http.StatusBadRequest)
}
defer zipFile.Close()
name = data.Data.Name
file = zipFile.(io.Reader)
} else {
realFile, err := os.Open(*data.Path)
if err != nil {
return nil, model.NewAppError("BulkImport", "app.import.attachment.bad_file.error", map[string]interface{}{"FilePath": *data.Path}, err.Error(), http.StatusBadRequest)
}
defer realFile.Close()
name = realFile.Name()
file = realFile
} }
defer file.Close()
timestamp := utils.TimeFromMillis(post.CreateAt) timestamp := utils.TimeFromMillis(post.CreateAt)
@@ -1166,7 +1183,7 @@ func (a *App) importAttachment(c *request.Context, data *AttachmentImportData, p
return nil, model.NewAppError("BulkImport", "app.import.attachment.file_upload.error", map[string]interface{}{"FilePath": *data.Path}, "", http.StatusBadRequest) return nil, model.NewAppError("BulkImport", "app.import.attachment.file_upload.error", map[string]interface{}{"FilePath": *data.Path}, "", http.StatusBadRequest)
} }
for _, oldFile := range oldFiles { for _, oldFile := range oldFiles {
if oldFile.Name != path.Base(file.Name()) || oldFile.Size != int64(len(fileData)) { if oldFile.Name != path.Base(name) || oldFile.Size != int64(len(fileData)) {
continue continue
} }
// check md5 // check md5
@@ -1178,15 +1195,15 @@ func (a *App) importAttachment(c *request.Context, data *AttachmentImportData, p
oldHash := sha1.Sum(oldFileData) oldHash := sha1.Sum(oldFileData)
if bytes.Equal(oldHash[:], newHash[:]) { if bytes.Equal(oldHash[:], newHash[:]) {
mlog.Info("Skipping uploading of file because name already exists", mlog.Any("file_name", file.Name())) mlog.Info("Skipping uploading of file because name already exists", mlog.Any("file_name", name))
return oldFile, nil return oldFile, nil
} }
} }
} }
mlog.Info("Uploading file with name", mlog.String("file_name", file.Name())) mlog.Info("Uploading file with name", mlog.String("file_name", name))
fileInfo, appErr := a.DoUploadFile(c, timestamp, teamID, post.ChannelId, post.UserId, file.Name(), fileData) fileInfo, appErr := a.DoUploadFile(c, timestamp, teamID, post.ChannelId, post.UserId, name, fileData)
if appErr != nil { if appErr != nil {
mlog.Error("Failed to upload file:", mlog.Err(appErr)) mlog.Error("Failed to upload file:", mlog.Err(appErr))
return nil, appErr return nil, appErr

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

@@ -4,6 +4,7 @@
package app package app
import ( import (
"archive/zip"
"context" "context"
"io/ioutil" "io/ioutil"
"os" "os"
@@ -4025,7 +4026,7 @@ func TestImportPostAndRepliesWithAttachments(t *testing.T) {
AssertFileIdsInPost(attachments, th, t) AssertFileIdsInPost(attachments, th, t)
}) })
t.Run("Reply with Attachments in Direct Pos", func(t *testing.T) { t.Run("Reply with Attachments in Direct Post", func(t *testing.T) {
directImportData := LineImportWorkerData{ directImportData := LineImportWorkerData{
LineImportData{ LineImportData{
DirectPost: &DirectPostImportData{ DirectPost: &DirectPostImportData{
@@ -4177,3 +4178,161 @@ func TestImportDirectPostWithAttachments(t *testing.T) {
require.Len(t, attachments, 3) require.Len(t, attachments, 3)
}) })
} }
func TestZippedImportPostAndRepliesWithAttachments(t *testing.T) {
th := Setup(t)
defer th.TearDown()
// Create a Team.
teamName := model.NewRandomTeamName()
th.App.importTeam(th.Context, &TeamImportData{
Name: &teamName,
DisplayName: ptrStr("Display Name"),
Type: ptrStr("O"),
}, false)
team, appErr := th.App.GetTeamByName(teamName)
require.Nil(t, appErr, "Failed to get team from database.")
// Create a Channel.
channelName := model.NewId()
th.App.importChannel(th.Context, &ChannelImportData{
Team: &teamName,
Name: &channelName,
DisplayName: ptrStr("Display Name"),
Type: ptrStr("O"),
}, false)
_, appErr = th.App.GetChannelByName(channelName, team.Id, false)
require.Nil(t, appErr, "Failed to get channel from database.")
// Create users
username2 := model.NewId()
th.App.importUser(&UserImportData{
Username: &username2,
Email: ptrStr(model.NewId() + "@example.com"),
}, false)
user2, appErr := th.App.GetUserByUsername(username2)
require.Nil(t, appErr, "Failed to get user3 from database.")
// Create direct post users.
username3 := model.NewId()
th.App.importUser(&UserImportData{
Username: &username3,
Email: ptrStr(model.NewId() + "@example.com"),
}, false)
user3, appErr := th.App.GetUserByUsername(username3)
require.Nil(t, appErr, "Failed to get user3 from database.")
username4 := model.NewId()
th.App.importUser(&UserImportData{
Username: &username4,
Email: ptrStr(model.NewId() + "@example.com"),
}, false)
user4, appErr := th.App.GetUserByUsername(username4)
require.Nil(t, appErr, "Failed to get user3 from database.")
// Post with attachments
time := model.GetMillis()
attachmentsPostTime := time
attachmentsReplyTime := time + 1
testsDir, _ := fileutils.FindDir("tests")
testImage := filepath.Join(testsDir, "test.png")
testZipFileName := filepath.Join(testsDir, "import_test.zip")
testZip, _ := os.Open(testZipFileName)
fi, err := testZip.Stat()
require.NoError(t, err, "failed to get file info")
testZipReader, err := zip.NewReader(testZip, fi.Size())
require.NoError(t, err, "failed to read test zip")
require.NotEmpty(t, testZipReader.File)
imageData := testZipReader.File[0]
require.NoError(t, err, "failed to copy test Image file into zip")
testMarkDown := filepath.Join(testsDir, "test-attachments.md")
data := LineImportWorkerData{
LineImportData{
Post: &PostImportData{
Team: &teamName,
Channel: &channelName,
User: &username3,
Message: ptrStr("Message with reply"),
CreateAt: &attachmentsPostTime,
Attachments: &[]AttachmentImportData{{Path: &testImage}, {Path: &testMarkDown}},
Replies: &[]ReplyImportData{{
User: &user4.Username,
Message: ptrStr("Message reply"),
CreateAt: &attachmentsReplyTime,
Attachments: &[]AttachmentImportData{{Path: &testImage, Data: imageData}},
}},
},
},
19,
}
t.Run("import with attachment", func(t *testing.T) {
errLine, err := th.App.importMultiplePostLines(th.Context, []LineImportWorkerData{data}, false)
require.Nil(t, err)
require.Equal(t, 0, errLine)
attachments := GetAttachments(user3.Id, th, t)
require.Len(t, attachments, 2)
assert.Contains(t, attachments[0].Path, team.Id)
assert.Contains(t, attachments[1].Path, team.Id)
AssertFileIdsInPost(attachments, th, t)
attachments = GetAttachments(user4.Id, th, t)
require.Len(t, attachments, 1)
assert.Contains(t, attachments[0].Path, team.Id)
AssertFileIdsInPost(attachments, th, t)
})
t.Run("import existing post with new attachment", func(t *testing.T) {
data.Post.Attachments = &[]AttachmentImportData{{Path: &testImage}}
errLine, err := th.App.importMultiplePostLines(th.Context, []LineImportWorkerData{data}, false)
require.Nil(t, err)
require.Equal(t, 0, errLine)
attachments := GetAttachments(user3.Id, th, t)
require.Len(t, attachments, 1)
assert.Contains(t, attachments[0].Path, team.Id)
AssertFileIdsInPost(attachments, th, t)
attachments = GetAttachments(user4.Id, th, t)
require.Len(t, attachments, 1)
assert.Contains(t, attachments[0].Path, team.Id)
AssertFileIdsInPost(attachments, th, t)
})
t.Run("Reply with Attachments in Direct Post", func(t *testing.T) {
directImportData := LineImportWorkerData{
LineImportData{
DirectPost: &DirectPostImportData{
ChannelMembers: &[]string{
user3.Username,
user2.Username,
},
User: &user3.Username,
Message: ptrStr("Message with Replies"),
CreateAt: ptrInt64(model.GetMillis()),
Replies: &[]ReplyImportData{{
User: &user2.Username,
Message: ptrStr("Message reply with attachment"),
CreateAt: ptrInt64(model.GetMillis()),
Attachments: &[]AttachmentImportData{{Path: &testImage}},
}},
},
},
7,
}
errLine, err := th.App.importMultipleDirectPostLines(th.Context, []LineImportWorkerData{directImportData}, false)
require.Nil(t, err, "Expected success.")
require.Equal(t, 0, errLine)
attachments := GetAttachments(user2.Id, th, t)
require.Len(t, attachments, 1)
assert.Contains(t, attachments[0].Path, "noteam")
AssertFileIdsInPost(attachments, th, t)
})
}

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

@@ -185,13 +185,13 @@ func TestImportBulkImport(t *testing.T) {
{"type": "direct_post", "direct_post": {"channel_members": ["` + username + `", "` + username2 + `", "` + username3 + `"], "user": "` + username + `", "message": "Hello Group Channel", "create_at": 123456789015}} {"type": "direct_post", "direct_post": {"channel_members": ["` + username + `", "` + username2 + `", "` + username3 + `"], "user": "` + username + `", "message": "Hello Group Channel", "create_at": 123456789015}}
{"type": "emoji", "emoji": {"name": "` + emojiName + `", "image": "` + testImage + `"}}` {"type": "emoji", "emoji": {"name": "` + emojiName + `", "image": "` + testImage + `"}}`
err, line := th.App.BulkImport(th.Context, strings.NewReader(data1), false, 2) err, line := th.App.BulkImport(th.Context, strings.NewReader(data1), nil, false, 2)
require.Nil(t, err, "BulkImport should have succeeded") require.Nil(t, err, "BulkImport should have succeeded")
require.Equal(t, 0, line, "BulkImport line should be 0") require.Equal(t, 0, line, "BulkImport line should be 0")
// Run bulk import using a string that contains a line with invalid json. // Run bulk import using a string that contains a line with invalid json.
data2 := `{"type": "version", "version": 1` data2 := `{"type": "version", "version": 1`
err, line = th.App.BulkImport(th.Context, strings.NewReader(data2), false, 2) err, line = th.App.BulkImport(th.Context, strings.NewReader(data2), nil, false, 2)
require.NotNil(t, err, "Should have failed due to invalid JSON on line 1.") require.NotNil(t, err, "Should have failed due to invalid JSON on line 1.")
require.Equal(t, 1, line, "Should have failed due to invalid JSON on line 1.") require.Equal(t, 1, line, "Should have failed due to invalid JSON on line 1.")
@@ -200,7 +200,7 @@ func TestImportBulkImport(t *testing.T) {
{"type": "channel", "channel": {"type": "O", "display_name": "xr6m6udffngark2uekvr3hoeny", "team": "` + teamName + `", "name": "` + channelName + `"}} {"type": "channel", "channel": {"type": "O", "display_name": "xr6m6udffngark2uekvr3hoeny", "team": "` + teamName + `", "name": "` + channelName + `"}}
{"type": "user", "user": {"username": "kufjgnkxkrhhfgbrip6qxkfsaa", "email": "kufjgnkxkrhhfgbrip6qxkfsaa@example.com"}} {"type": "user", "user": {"username": "kufjgnkxkrhhfgbrip6qxkfsaa", "email": "kufjgnkxkrhhfgbrip6qxkfsaa@example.com"}}
{"type": "user", "user": {"username": "bwshaim6qnc2ne7oqkd5b2s2rq", "email": "bwshaim6qnc2ne7oqkd5b2s2rq@example.com", "teams": [{"name": "` + teamName + `", "channels": [{"name": "` + channelName + `"}]}]}}` {"type": "user", "user": {"username": "bwshaim6qnc2ne7oqkd5b2s2rq", "email": "bwshaim6qnc2ne7oqkd5b2s2rq@example.com", "teams": [{"name": "` + teamName + `", "channels": [{"name": "` + channelName + `"}]}]}}`
err, line = th.App.BulkImport(th.Context, strings.NewReader(data3), false, 2) err, line = th.App.BulkImport(th.Context, strings.NewReader(data3), nil, false, 2)
require.NotNil(t, err, "Should have failed due to missing version line on line 1.") require.NotNil(t, err, "Should have failed due to missing version line on line 1.")
require.Equal(t, 1, line, "Should have failed due to missing version line on line 1.") require.Equal(t, 1, line, "Should have failed due to missing version line on line 1.")
@@ -212,7 +212,7 @@ func TestImportBulkImport(t *testing.T) {
{"type": "channel", "channel": {"type": "O", "display_name": "xr6m6udffngark2uekvr3hoeny", "team": "` + teamName + `", "name": "` + channelName + `"}} {"type": "channel", "channel": {"type": "O", "display_name": "xr6m6udffngark2uekvr3hoeny", "team": "` + teamName + `", "name": "` + channelName + `"}}
{"type": "user", "user": {"username": "` + username + `", "email": "` + username + `@example.com", "teams": [{"name": "` + teamName + `","theme": "` + teamTheme1 + `", "channels": [{"name": "` + channelName + `"}]}]}} {"type": "user", "user": {"username": "` + username + `", "email": "` + username + `@example.com", "teams": [{"name": "` + teamName + `","theme": "` + teamTheme1 + `", "channels": [{"name": "` + channelName + `"}]}]}}
{"type": "post", "post": {"team": "` + teamName + `", "channel": "` + channelName + `", "user": "` + username + `", "message": "Hello World", "create_at": 123456789012}}` {"type": "post", "post": {"team": "` + teamName + `", "channel": "` + channelName + `", "user": "` + username + `", "message": "Hello World", "create_at": 123456789012}}`
err, line = th.App.BulkImport(th.Context, strings.NewReader(data4+"\r\n"+posts), false, 2) err, line = th.App.BulkImport(th.Context, strings.NewReader(data4+"\r\n"+posts), nil, false, 2)
require.Nil(t, err, "BulkImport should have succeeded") require.Nil(t, err, "BulkImport should have succeeded")
require.Equal(t, 0, line, "BulkImport line should be 0") require.Equal(t, 0, line, "BulkImport line should be 0")
}) })
@@ -220,7 +220,7 @@ func TestImportBulkImport(t *testing.T) {
t.Run("First item after version without type", func(t *testing.T) { t.Run("First item after version without type", func(t *testing.T) {
data := `{"type": "version", "version": 1} data := `{"type": "version", "version": 1}
{"name": "custom-emoji-troll", "image": "bulkdata/emoji/trollolol.png"}` {"name": "custom-emoji-troll", "image": "bulkdata/emoji/trollolol.png"}`
err, line := th.App.BulkImport(th.Context, strings.NewReader(data), false, 2) err, line := th.App.BulkImport(th.Context, strings.NewReader(data), nil, false, 2)
require.NotNil(t, err, "Should have failed due to invalid type on line 2.") require.NotNil(t, err, "Should have failed due to invalid type on line 2.")
require.Equal(t, 2, line, "Should have failed due to invalid type on line 2.") require.Equal(t, 2, line, "Should have failed due to invalid type on line 2.")
}) })
@@ -234,7 +234,7 @@ func TestImportBulkImport(t *testing.T) {
{"type": "direct_channel", "direct_channel": {"members": ["` + username + `", "` + username + `"]}} {"type": "direct_channel", "direct_channel": {"members": ["` + username + `", "` + username + `"]}}
{"type": "direct_post", "direct_post": {"channel_members": ["` + username + `", "` + username + `"], "user": "` + username + `", "message": "Hello Direct Channel to myself", "create_at": 123456789014, "props":{"attachments":[{"id":0,"fallback":"[February 4th, 2020 2:46 PM] author: fallback","color":"D0D0D0","pretext":"","author_name":"author","author_link":"","title":"","title_link":"","text":"this post has props","fields":null,"image_url":"","thumb_url":"","footer":"Posted in #general","footer_icon":"","ts":"1580823992.000100"}]}}}}` {"type": "direct_post", "direct_post": {"channel_members": ["` + username + `", "` + username + `"], "user": "` + username + `", "message": "Hello Direct Channel to myself", "create_at": 123456789014, "props":{"attachments":[{"id":0,"fallback":"[February 4th, 2020 2:46 PM] author: fallback","color":"D0D0D0","pretext":"","author_name":"author","author_link":"","title":"","title_link":"","text":"this post has props","fields":null,"image_url":"","thumb_url":"","footer":"Posted in #general","footer_icon":"","ts":"1580823992.000100"}]}}}}`
err, line := th.App.BulkImport(th.Context, strings.NewReader(data6), false, 2) err, line := th.App.BulkImport(th.Context, strings.NewReader(data6), nil, false, 2)
require.Nil(t, err, "BulkImport should have succeeded") require.Nil(t, err, "BulkImport should have succeeded")
require.Equal(t, 0, line, "BulkImport line should be 0") require.Equal(t, 0, line, "BulkImport line should be 0")
}) })
@@ -393,7 +393,7 @@ func BenchmarkBulkImport(b *testing.B) {
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
err, _ := th.App.BulkImportWithPath(th.Context, jsonFile, false, runtime.NumCPU(), dir) err, _ := th.App.BulkImportWithPath(th.Context, jsonFile, nil, false, runtime.NumCPU(), dir)
require.Nil(b, err) require.Nil(b, err)
} }
b.StopTimer() b.StopTimer()

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

@@ -4,6 +4,8 @@
package app package app
import ( import (
"archive/zip"
"github.com/mattermost/mattermost-server/v5/model" "github.com/mattermost/mattermost-server/v5/model"
) )
@@ -195,7 +197,8 @@ type LineImportWorkerError struct {
} }
type AttachmentImportData struct { type AttachmentImportData struct {
Path *string `json:"path"` Path *string `json:"path"`
Data *zip.File `json:"-"`
} }
type ComparablePreference struct { type ComparablePreference struct {

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

@@ -7,6 +7,7 @@
package opentracing package opentracing
import ( import (
"archive/zip"
"bytes" "bytes"
"context" "context"
"crypto/ecdsa" "crypto/ecdsa"
@@ -958,7 +959,7 @@ func (a *OpenTracingAppLayer) BulkExport(writer io.Writer, outPath string, opts
return resultVar0 return resultVar0
} }
func (a *OpenTracingAppLayer) BulkImport(c *request.Context, fileReader io.Reader, dryRun bool, workers int) (*model.AppError, int) { func (a *OpenTracingAppLayer) BulkImport(c *request.Context, jsonlReader io.Reader, attachmentsReader *zip.Reader, dryRun bool, workers int) (*model.AppError, int) {
origCtx := a.ctx origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.BulkImport") span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.BulkImport")
@@ -970,7 +971,7 @@ func (a *OpenTracingAppLayer) BulkImport(c *request.Context, fileReader io.Reade
}() }()
defer span.Finish() defer span.Finish()
resultVar0, resultVar1 := a.app.BulkImport(c, fileReader, dryRun, workers) resultVar0, resultVar1 := a.app.BulkImport(c, jsonlReader, attachmentsReader, dryRun, workers)
if resultVar0 != nil { if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0)) span.LogFields(spanlog.Error(resultVar0))
@@ -980,7 +981,7 @@ func (a *OpenTracingAppLayer) BulkImport(c *request.Context, fileReader io.Reade
return resultVar0, resultVar1 return resultVar0, resultVar1
} }
func (a *OpenTracingAppLayer) BulkImportWithPath(c *request.Context, fileReader io.Reader, dryRun bool, workers int, importPath string) (*model.AppError, int) { func (a *OpenTracingAppLayer) BulkImportWithPath(c *request.Context, jsonlReader io.Reader, attachmentsReader *zip.Reader, dryRun bool, workers int, importPath string) (*model.AppError, int) {
origCtx := a.ctx origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.BulkImportWithPath") span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.BulkImportWithPath")
@@ -992,7 +993,7 @@ func (a *OpenTracingAppLayer) BulkImportWithPath(c *request.Context, fileReader
}() }()
defer span.Finish() defer span.Finish()
resultVar0, resultVar1 := a.app.BulkImportWithPath(c, fileReader, dryRun, workers, importPath) resultVar0, resultVar1 := a.app.BulkImportWithPath(c, jsonlReader, attachmentsReader, dryRun, workers, importPath)
if resultVar0 != nil { if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0)) span.LogFields(spanlog.Error(resultVar0))

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

@@ -150,7 +150,7 @@ func bulkImportCmdF(command *cobra.Command, args []string) error {
CommandPrettyPrintln("") CommandPrettyPrintln("")
if err, lineNumber := a.BulkImportWithPath(&request.Context{}, fileReader, !apply, workers, importPath); err != nil { if err, lineNumber := a.BulkImportWithPath(&request.Context{}, fileReader, nil, !apply, workers, importPath); err != nil {
CommandPrintErrorln(err.Error()) CommandPrintErrorln(err.Error())
if lineNumber != 0 { if lineNumber != 0 {
CommandPrintErrorln(fmt.Sprintf("Error occurred on data file line %v", lineNumber)) CommandPrintErrorln(fmt.Sprintf("Error occurred on data file line %v", lineNumber))

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

@@ -374,7 +374,8 @@ func sampleDataCmdF(command *cobra.Command, args []string) error {
} }
var importErr *model.AppError var importErr *model.AppError
importErr, lineNumber := a.BulkImport(&request.Context{}, bulkFile, false, workers)
importErr, lineNumber := a.BulkImport(&request.Context{}, bulkFile, nil, false, workers)
if importErr != nil { if importErr != nil {
return fmt.Errorf("%s: %s, %s (line: %d)", importErr.Where, importErr.Message, importErr.DetailedError, lineNumber) return fmt.Errorf("%s: %s, %s (line: %d)", importErr.Where, importErr.Message, importErr.DetailedError, lineNumber)
} }

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

@@ -7510,14 +7510,6 @@
"id": "import_process.worker.do_job.open_file", "id": "import_process.worker.do_job.open_file",
"translation": "Unable to process import: failed to open file." "translation": "Unable to process import: failed to open file."
}, },
{
"id": "import_process.worker.do_job.tmp_dir",
"translation": "Unable to process import: failed to create temporary directory."
},
{
"id": "import_process.worker.do_job.unzip",
"translation": "Unable to process import: failed to unzip file."
},
{ {
"id": "interactive_message.decode_trigger_id.base64_decode_failed", "id": "interactive_message.decode_trigger_id.base64_decode_failed",
"translation": "Failed to decode base64 for trigger ID for interactive dialog." "translation": "Failed to decode base64 for trigger ID for interactive dialog."

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

@@ -4,13 +4,13 @@
package import_process package import_process
import ( import (
"archive/zip"
"io" "io"
"io/ioutil"
"net/http" "net/http"
"os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strconv" "strconv"
"strings"
"github.com/mattermost/mattermost-server/v5/app" "github.com/mattermost/mattermost-server/v5/app"
"github.com/mattermost/mattermost-server/v5/app/request" "github.com/mattermost/mattermost-server/v5/app/request"
@@ -18,7 +18,6 @@ import (
tjobs "github.com/mattermost/mattermost-server/v5/jobs/interfaces" tjobs "github.com/mattermost/mattermost-server/v5/jobs/interfaces"
"github.com/mattermost/mattermost-server/v5/model" "github.com/mattermost/mattermost-server/v5/model"
"github.com/mattermost/mattermost-server/v5/shared/mlog" "github.com/mattermost/mattermost-server/v5/shared/mlog"
"github.com/mattermost/mattermost-server/v5/utils"
) )
func init() { func init() {
@@ -125,51 +124,45 @@ func (w *ImportProcessWorker) doJob(job *model.Job) {
} }
defer importFile.Close() defer importFile.Close()
// TODO (MM-30187): improve this process by eliminating the need to unzip the import importZipReader, err := zip.NewReader(importFile.(io.ReaderAt), importFileSize)
// file locally and instead do the whole bulk import process in memory by
// streaming the import file.
// create a temporary dir to extract the zipped import file.
dir, err := ioutil.TempDir("", "import")
if err != nil {
appError := model.NewAppError("ImportProcessWorker", "import_process.worker.do_job.tmp_dir", nil, err.Error(), http.StatusInternalServerError)
w.setJobError(job, appError)
return
}
defer os.RemoveAll(dir)
// extract the contents of the zipped file.
paths, err := utils.UnzipToPath(importFile.(io.ReaderAt), importFileSize, dir)
if err != nil {
appError := model.NewAppError("ImportProcessWorker", "import_process.worker.do_job.unzip", nil, err.Error(), http.StatusInternalServerError)
w.setJobError(job, appError)
return
}
// find JSONL import file.
var jsonFilePath string
for _, path := range paths {
if filepath.Ext(path) == ".jsonl" {
jsonFilePath = path
break
}
}
if jsonFilePath == "" {
appError := model.NewAppError("ImportProcessWorker", "import_process.worker.do_job.missing_jsonl", nil, "", http.StatusBadRequest)
w.setJobError(job, appError)
return
}
jsonFile, err := os.Open(jsonFilePath)
if err != nil { if err != nil {
appError := model.NewAppError("ImportProcessWorker", "import_process.worker.do_job.open_file", nil, err.Error(), http.StatusInternalServerError) appError := model.NewAppError("ImportProcessWorker", "import_process.worker.do_job.open_file", nil, err.Error(), http.StatusInternalServerError)
w.setJobError(job, appError) w.setJobError(job, appError)
return return
} }
// find JSONL import file.
var jsonFile io.ReadCloser
for _, f := range importZipReader.File {
if filepath.Ext(f.Name) != ".jsonl" {
continue
}
// avoid "zip slip"
if strings.Contains(f.Name, "..") {
appError := model.NewAppError("ImportProcessWorker", "import_process.worker.do_job.open_file", nil, "jsonFilePath contains path traversal", http.StatusForbidden)
w.setJobError(job, appError)
return
}
jsonFile, err = f.Open()
if err != nil {
appError := model.NewAppError("ImportProcessWorker", "import_process.worker.do_job.open_file", nil, err.Error(), http.StatusInternalServerError)
w.setJobError(job, appError)
return
}
defer jsonFile.Close()
break
}
if jsonFile == nil {
appError := model.NewAppError("ImportProcessWorker", "import_process.worker.do_job.missing_jsonl", nil, "jsonFile was nil", http.StatusBadRequest)
w.setJobError(job, appError)
return
}
// do the actual import. // do the actual import.
appErr, lineNumber := w.app.BulkImportWithPath(w.appContext, jsonFile, false, runtime.NumCPU(), filepath.Join(dir, app.ExportDataDir)) appErr, lineNumber := w.app.BulkImport(w.appContext, jsonFile, importZipReader, false, runtime.NumCPU())
if appErr != nil { if appErr != nil {
job.Data["line_number"] = strconv.Itoa(lineNumber) job.Data["line_number"] = strconv.Itoa(lineNumber)
w.setJobError(job, appErr) w.setJobError(job, appErr)