1
0
mirror of https://github.com/mattermost/focalboard.git synced 2024-11-24 08:22:29 +02:00

Backport fixes for import/export attachments and fixes (#4741)

* Backport fixes for import/export attachments and fixes

* fix bad merge

* lint fixes

* Update server/app/boards.go

Co-authored-by: Miguel de la Cruz <miguel@mcrx.me>

---------

Co-authored-by: Miguel de la Cruz <miguel@mcrx.me>
This commit is contained in:
Scott Bishel 2023-05-22 10:31:24 -06:00 committed by GitHub
parent b7d94a8fe2
commit 888c169910
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 591 additions and 293 deletions

View File

@ -303,7 +303,7 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
// this query param exists when creating template from board, or board from template
sourceBoardID := r.URL.Query().Get("sourceBoardID")
if sourceBoardID != "" {
if updateFileIDsErr := a.app.CopyCardFiles(sourceBoardID, blocks); updateFileIDsErr != nil {
if updateFileIDsErr := a.app.CopyAndUpdateCardFiles(sourceBoardID, userID, blocks, false); updateFileIDsErr != nil {
a.errorResponse(w, r, updateFileIDsErr)
return
}

View File

@ -393,7 +393,7 @@ func (a *API) handleUploadFile(w http.ResponseWriter, r *http.Request) {
auditRec.AddMeta("teamID", board.TeamID)
auditRec.AddMeta("filename", handle.Filename)
fileID, err := a.app.SaveFile(file, board.TeamID, boardID, handle.Filename)
fileID, err := a.app.SaveFile(file, board.TeamID, boardID, handle.Filename, board.IsTemplate)
if err != nil {
a.errorResponse(w, r, err)
return

View File

@ -4,11 +4,9 @@ import (
"errors"
"fmt"
"path/filepath"
"strings"
"github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/services/notify"
"github.com/mattermost/focalboard/server/utils"
"github.com/mattermost/mattermost-server/v6/shared/mlog"
)
@ -45,6 +43,11 @@ func (a *App) DuplicateBlock(boardID string, blockID string, userID string, asTe
return nil, err
}
err = a.CopyAndUpdateCardFiles(boardID, userID, blocks, asTemplate)
if err != nil {
return nil, err
}
a.blockChangeNotifier.Enqueue(func() error {
for _, block := range blocks {
a.wsAdapter.BroadcastBlockChange(board.TeamID, block)
@ -292,95 +295,6 @@ func (a *App) InsertBlocksAndNotify(blocks []*model.Block, modifiedByID string,
return blocks, nil
}
func (a *App) CopyCardFiles(sourceBoardID string, copiedBlocks []*model.Block) error {
// Images attached in cards have a path comprising the card's board ID.
// When we create a template from this board, we need to copy the files
// with the new board ID in path.
// Not doing so causing images in templates (and boards created from this
// template) to fail to load.
// look up ID of source sourceBoard, which may be different than the blocks.
sourceBoard, err := a.GetBoard(sourceBoardID)
if err != nil || sourceBoard == nil {
return fmt.Errorf("cannot fetch source board %s for CopyCardFiles: %w", sourceBoardID, err)
}
var destTeamID string
var destBoardID string
for i := range copiedBlocks {
block := copiedBlocks[i]
fileName := ""
isOk := false
switch block.Type {
case model.TypeImage:
fileName, isOk = block.Fields["fileId"].(string)
if !isOk || fileName == "" {
continue
}
case model.TypeAttachment:
fileName, isOk = block.Fields["attachmentId"].(string)
if !isOk || fileName == "" {
continue
}
default:
continue
}
// create unique filename in case we are copying cards within the same board.
ext := filepath.Ext(fileName)
destFilename := utils.NewID(utils.IDTypeNone) + ext
if destBoardID == "" || block.BoardID != destBoardID {
destBoardID = block.BoardID
destBoard, err := a.GetBoard(destBoardID)
if err != nil {
return fmt.Errorf("cannot fetch destination board %s for CopyCardFiles: %w", sourceBoardID, err)
}
destTeamID = destBoard.TeamID
}
sourceFilePath := filepath.Join(sourceBoard.TeamID, sourceBoard.ID, fileName)
destinationFilePath := filepath.Join(destTeamID, block.BoardID, destFilename)
a.logger.Debug(
"Copying card file",
mlog.String("sourceFilePath", sourceFilePath),
mlog.String("destinationFilePath", destinationFilePath),
)
if err := a.filesBackend.CopyFile(sourceFilePath, destinationFilePath); err != nil {
a.logger.Error(
"CopyCardFiles failed to copy file",
mlog.String("sourceFilePath", sourceFilePath),
mlog.String("destinationFilePath", destinationFilePath),
mlog.Err(err),
)
}
if block.Type == model.TypeAttachment {
block.Fields["attachmentId"] = destFilename
parts := strings.Split(fileName, ".")
fileInfoID := parts[0][1:]
fileInfo, err := a.store.GetFileInfo(fileInfoID)
if err != nil {
return fmt.Errorf("CopyCardFiles: cannot retrieve original fileinfo: %w", err)
}
newParts := strings.Split(destFilename, ".")
newFileID := newParts[0][1:]
fileInfo.Id = newFileID
err = a.store.SaveFileInfo(fileInfo)
if err != nil {
return fmt.Errorf("CopyCardFiles: cannot create fileinfo: %w", err)
}
} else {
block.Fields["fileId"] = destFilename
}
}
return nil
}
func (a *App) GetBlockByID(blockID string) (*model.Block, error) {
return a.store.GetBlock(blockID)
}

View File

@ -185,8 +185,13 @@ func (a *App) DuplicateBoard(boardID, userID, toTeam string, asTemplate bool) (*
}
// copy any file attachments from the duplicated blocks.
if err = a.CopyCardFiles(boardID, bab.Blocks); err != nil {
a.logger.Error("Could not copy files while duplicating board", mlog.String("BoardID", boardID), mlog.Err(err))
err = a.CopyAndUpdateCardFiles(boardID, userID, bab.Blocks, asTemplate)
if err != nil {
dbab := model.NewDeleteBoardsAndBlocksFromBabs(bab)
if dErr := a.store.DeleteBoardsAndBlocks(dbab, userID); dErr != nil {
a.logger.Error("Cannot delete board after duplication error when updating block's file info", mlog.String("boardID", bab.Boards[0].ID), mlog.Err(dErr))
}
return nil, nil, fmt.Errorf("could not patch file IDs while duplicating board %s: %w", boardID, err)
}
if !asTemplate {
@ -197,44 +202,6 @@ func (a *App) DuplicateBoard(boardID, userID, toTeam string, asTemplate bool) (*
}
}
// bab.Blocks now has updated file ids for any blocks containing files. We need to store them.
blockIDs := make([]string, 0)
blockPatches := make([]model.BlockPatch, 0)
for _, block := range bab.Blocks {
fieldName := ""
if block.Type == model.TypeImage {
fieldName = "fileId"
} else if block.Type == model.TypeAttachment {
fieldName = "attachmentId"
}
if fieldName != "" {
if fieldID, ok := block.Fields[fieldName]; ok {
blockIDs = append(blockIDs, block.ID)
blockPatches = append(blockPatches, model.BlockPatch{
UpdatedFields: map[string]interface{}{
fieldName: fieldID,
},
})
}
}
}
a.logger.Debug("Duplicate boards patching file IDs", mlog.Int("count", len(blockIDs)))
if len(blockIDs) != 0 {
patches := &model.BlockPatchBatch{
BlockIDs: blockIDs,
BlockPatches: blockPatches,
}
if err = a.store.PatchBlocks(patches, userID); err != nil {
dbab := model.NewDeleteBoardsAndBlocksFromBabs(bab)
if err = a.store.DeleteBoardsAndBlocks(dbab, userID); err != nil {
a.logger.Error("Cannot delete board after duplication error when updating block's file info", mlog.String("boardID", bab.Boards[0].ID), mlog.Err(err))
}
return nil, nil, fmt.Errorf("could not patch file IDs while duplicating board %s: %w", boardID, err)
}
}
a.blockChangeNotifier.Enqueue(func() error {
teamID := ""
for _, board := range bab.Boards {

View File

@ -91,10 +91,10 @@ func (a *App) writeArchiveBoard(zw *zip.Writer, board model.Board, opt model.Exp
if err = a.writeArchiveBlockLine(w, block); err != nil {
return err
}
if block.Type == model.TypeImage {
filename, err2 := extractImageFilename(block)
if block.Type == model.TypeImage || block.Type == model.TypeAttachment {
filename, err2 := extractFilename(block)
if err2 != nil {
return err
return err2
}
files = append(files, filename)
}
@ -204,7 +204,10 @@ func (a *App) writeArchiveFile(zw *zip.Writer, filename string, boardID string,
return err
}
src, err := a.GetFileReader(opt.TeamID, boardID, filename)
_, fileReader, err := a.GetFile(opt.TeamID, boardID, filename)
if err != nil && !model.IsErrNotFound(err) {
return err
}
if err != nil {
// just log this; image file is missing but we'll still export an equivalent board
a.logger.Error("image file missing for export",
@ -214,9 +217,9 @@ func (a *App) writeArchiveFile(zw *zip.Writer, filename string, boardID string,
)
return nil
}
defer src.Close()
defer fileReader.Close()
_, err = io.Copy(dest, src)
_, err = io.Copy(dest, fileReader)
return err
}
@ -235,10 +238,13 @@ func (a *App) getBoardsForArchive(boardIDs []string) ([]model.Board, error) {
return boards, nil
}
func extractImageFilename(imageBlock *model.Block) (string, error) {
f, ok := imageBlock.Fields["fileId"]
func extractFilename(block *model.Block) (string, error) {
f, ok := block.Fields["fileId"]
if !ok {
return "", model.ErrInvalidImageBlock
f, ok = block.Fields["attachmentId"]
if !ok {
return "", model.ErrInvalidImageBlock
}
}
filename, ok := f.(string)

View File

@ -8,19 +8,17 @@ import (
"strings"
"github.com/mattermost/focalboard/server/model"
mmModel "github.com/mattermost/mattermost-server/v6/model"
mm_model "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/focalboard/server/utils"
"github.com/mattermost/mattermost-server/v6/shared/filestore"
"github.com/mattermost/mattermost-server/v6/shared/mlog"
)
const emptyString = "empty"
var errEmptyFilename = errors.New("IsFileArchived: empty filename not allowed")
var ErrFileNotFound = errors.New("file not found")
func (a *App) SaveFile(reader io.Reader, teamID, rootID, filename string) (string, error) {
func (a *App) SaveFile(reader io.Reader, teamID, boardID, filename string, asTemplate bool) (string, error) {
// NOTE: File extension includes the dot
fileExtension := strings.ToLower(filepath.Ext(filename))
if fileExtension == ".jpeg" {
@ -28,48 +26,31 @@ func (a *App) SaveFile(reader io.Reader, teamID, rootID, filename string) (strin
}
createdFilename := utils.NewID(utils.IDTypeNone)
fullFilename := fmt.Sprintf(`%s%s`, createdFilename, fileExtension)
filePath := filepath.Join(utils.GetBaseFilePath(), fullFilename)
newFileName := fmt.Sprintf(`%s%s`, createdFilename, fileExtension)
if asTemplate {
newFileName = filename
}
filePath := getDestinationFilePath(asTemplate, teamID, boardID, newFileName)
fileSize, appErr := a.filesBackend.WriteFile(reader, filePath)
if appErr != nil {
return "", fmt.Errorf("unable to store the file in the files storage: %w", appErr)
}
now := utils.GetMillis()
fileInfo := &mmModel.FileInfo{
Id: createdFilename[1:],
CreatorId: "boards",
PostId: emptyString,
ChannelId: emptyString,
CreateAt: now,
UpdateAt: now,
DeleteAt: 0,
Path: filePath,
ThumbnailPath: emptyString,
PreviewPath: emptyString,
Name: filename,
Extension: fileExtension,
Size: fileSize,
MimeType: emptyString,
Width: 0,
Height: 0,
HasPreviewImage: false,
MiniPreview: nil,
Content: "",
RemoteId: nil,
}
fileInfo := model.NewFileInfo(filename)
fileInfo.Id = getFileInfoID(createdFilename)
fileInfo.Path = filePath
fileInfo.Size = fileSize
err := a.store.SaveFileInfo(fileInfo)
if err != nil {
return "", err
}
return fullFilename, nil
return newFileName, nil
}
func (a *App) GetFileInfo(filename string) (*mmModel.FileInfo, error) {
func (a *App) GetFileInfo(filename string) (*mm_model.FileInfo, error) {
if len(filename) == 0 {
return nil, errEmptyFilename
}
@ -77,8 +58,7 @@ func (a *App) GetFileInfo(filename string) (*mmModel.FileInfo, error) {
// filename is in the format 7<some-alphanumeric-string>.<extension>
// we want to extract the <some-alphanumeric-string> part of this as this
// will be the fileinfo id.
parts := strings.Split(filename, ".")
fileInfoID := parts[0][1:]
fileInfoID := getFileInfoID(strings.Split(filename, ".")[0])
fileInfo, err := a.store.GetFileInfo(fileInfoID)
if err != nil {
@ -88,11 +68,36 @@ func (a *App) GetFileInfo(filename string) (*mmModel.FileInfo, error) {
return fileInfo, nil
}
func (a *App) GetFile(teamID, rootID, fileName string) (*mmModel.FileInfo, filestore.ReadCloseSeeker, error) {
func (a *App) GetFile(teamID, rootID, fileName string) (*mm_model.FileInfo, filestore.ReadCloseSeeker, error) {
fileInfo, filePath, err := a.GetFilePath(teamID, rootID, fileName)
if err != nil {
a.logger.Error("GetFile: Failed to GetFilePath.", mlog.String("Team", teamID), mlog.String("board", rootID), mlog.String("filename", fileName), mlog.Err(err))
return nil, nil, err
}
exists, err := a.filesBackend.FileExists(filePath)
if err != nil {
a.logger.Error("GetFile: Failed to check if file exists as path. ", mlog.String("Path", filePath), mlog.Err(err))
return nil, nil, err
}
if !exists {
return nil, nil, ErrFileNotFound
}
reader, err := a.filesBackend.Reader(filePath)
if err != nil {
a.logger.Error("GetFile: Failed to get file reader of existing file at path", mlog.String("Path", filePath), mlog.Err(err))
return nil, nil, err
}
return fileInfo, reader, nil
}
func (a *App) GetFilePath(teamID, rootID, fileName string) (*mm_model.FileInfo, string, error) {
fileInfo, err := a.GetFileInfo(fileName)
if err != nil && !model.IsErrNotFound(err) {
a.logger.Error("111")
return nil, nil, err
return nil, "", err
}
var filePath string
@ -103,23 +108,23 @@ func (a *App) GetFile(teamID, rootID, fileName string) (*mmModel.FileInfo, files
filePath = filepath.Join(teamID, rootID, fileName)
}
exists, err := a.filesBackend.FileExists(filePath)
if err != nil {
a.logger.Error(fmt.Sprintf("GetFile: Failed to check if file exists as path. Path: %s, error: %e", filePath, err))
return nil, nil, err
}
return fileInfo, filePath, nil
}
if !exists {
return nil, nil, ErrFileNotFound
func getDestinationFilePath(isTemplate bool, teamID, boardID, filename string) string {
// if saving a file for a template, save using the "old method" that is /teamID/boardID/fileName
// this will prevent template files from being deleted by DataRetention,
// which deletes all files inside the "date" subdirectory
if isTemplate {
return filepath.Join(teamID, boardID, filename)
}
return filepath.Join(utils.GetBaseFilePath(), filename)
}
reader, err := a.filesBackend.Reader(filePath)
if err != nil {
a.logger.Error(fmt.Sprintf("GetFile: Failed to get file reader of existing file at path: %s, error: %e", filePath, err))
return nil, nil, err
}
return fileInfo, reader, nil
func getFileInfoID(fileName string) string {
// Boards ids are 27 characters long with a prefix character.
// removing the prefix, returns the 26 character uuid
return fileName[1:]
}
func (a *App) GetFileReader(teamID, rootID, filename string) (filestore.ReadCloseSeeker, error) {
@ -175,3 +180,120 @@ func (a *App) MoveFile(channelID, teamID, boardID, filename string) error {
}
return nil
}
func (a *App) CopyAndUpdateCardFiles(boardID, userID string, blocks []*model.Block, asTemplate bool) error {
newFileNames, err := a.CopyCardFiles(boardID, blocks, asTemplate)
if err != nil {
a.logger.Error("Could not copy files while duplicating board", mlog.String("BoardID", boardID), mlog.Err(err))
}
// blocks now has updated file ids for any blocks containing files. We need to update the database for them.
blockIDs := make([]string, 0)
blockPatches := make([]model.BlockPatch, 0)
for _, block := range blocks {
if block.Type == model.TypeImage || block.Type == model.TypeAttachment {
if fileID, ok := block.Fields["fileId"].(string); ok {
blockIDs = append(blockIDs, block.ID)
blockPatches = append(blockPatches, model.BlockPatch{
UpdatedFields: map[string]interface{}{
"fileId": newFileNames[fileID],
},
DeletedFields: []string{"attachmentId"},
})
}
}
}
a.logger.Debug("Duplicate boards patching file IDs", mlog.Int("count", len(blockIDs)))
if len(blockIDs) != 0 {
patches := &model.BlockPatchBatch{
BlockIDs: blockIDs,
BlockPatches: blockPatches,
}
if err := a.store.PatchBlocks(patches, userID); err != nil {
return fmt.Errorf("could not patch file IDs while duplicating board %s: %w", boardID, err)
}
}
return nil
}
func (a *App) CopyCardFiles(sourceBoardID string, copiedBlocks []*model.Block, asTemplate bool) (map[string]string, error) {
// Images attached in cards have a path comprising the card's board ID.
// When we create a template from this board, we need to copy the files
// with the new board ID in path.
// Not doing so causing images in templates (and boards created from this
// template) to fail to load.
// look up ID of source sourceBoard, which may be different than the blocks.
sourceBoard, err := a.GetBoard(sourceBoardID)
if err != nil || sourceBoard == nil {
return nil, fmt.Errorf("cannot fetch source board %s for CopyCardFiles: %w", sourceBoardID, err)
}
var destBoard *model.Board
newFileNames := make(map[string]string)
for _, block := range copiedBlocks {
if block.Type != model.TypeImage && block.Type != model.TypeAttachment {
continue
}
fileID, isOk := block.Fields["fileId"].(string)
if !isOk {
fileID, isOk = block.Fields["attachmentId"].(string)
if !isOk {
continue
}
}
// create unique filename
ext := filepath.Ext(fileID)
fileInfoID := utils.NewID(utils.IDTypeNone)
destFilename := fileInfoID + ext
if destBoard == nil || block.BoardID != destBoard.ID {
destBoard = sourceBoard
if block.BoardID != destBoard.ID {
destBoard, err = a.GetBoard(block.BoardID)
if err != nil {
return nil, fmt.Errorf("cannot fetch destination board %s for CopyCardFiles: %w", sourceBoardID, err)
}
}
}
// GetFilePath will retrieve the correct path
// depending on whether FileInfo table is used for the file.
fileInfo, sourceFilePath, err := a.GetFilePath(sourceBoard.TeamID, sourceBoard.ID, fileID)
if err != nil {
return nil, fmt.Errorf("cannot fetch destination board %s for CopyCardFiles: %w", sourceBoardID, err)
}
destinationFilePath := getDestinationFilePath(asTemplate, destBoard.TeamID, destBoard.ID, destFilename)
if fileInfo == nil {
fileInfo = model.NewFileInfo(destFilename)
}
fileInfo.Id = getFileInfoID(fileInfoID)
fileInfo.Path = destinationFilePath
err = a.store.SaveFileInfo(fileInfo)
if err != nil {
return nil, fmt.Errorf("CopyCardFiles: cannot create fileinfo: %w", err)
}
a.logger.Debug(
"Copying card file",
mlog.String("sourceFilePath", sourceFilePath),
mlog.String("destinationFilePath", destinationFilePath),
)
if err := a.filesBackend.CopyFile(sourceFilePath, destinationFilePath); err != nil {
a.logger.Error(
"CopyCardFiles failed to copy file",
mlog.String("sourceFilePath", sourceFilePath),
mlog.String("destinationFilePath", destinationFilePath),
mlog.Err(err),
)
}
newFileNames[fileID] = destFilename
}
return newFileNames, nil
}

View File

@ -12,7 +12,8 @@ import (
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
mmModel "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/focalboard/server/model"
mm_model "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin/plugintest/mock"
"github.com/mattermost/mattermost-server/v6/shared/filestore"
"github.com/mattermost/mattermost-server/v6/shared/filestore/mocks"
@ -21,6 +22,7 @@ import (
const (
testFileName = "temp-file-name"
testBoardID = "test-board-id"
testPath = "/path/to/file/fileName.txt"
)
var errDummy = errors.New("hello")
@ -207,7 +209,7 @@ func TestSaveFile(t *testing.T) {
}
mockedFileBackend.On("WriteFile", mockedReadCloseSeek, mock.Anything).Return(writeFileFunc, writeFileErrorFunc)
actual, err := th.App.SaveFile(mockedReadCloseSeek, "1", testBoardID, fileName)
actual, err := th.App.SaveFile(mockedReadCloseSeek, "1", testBoardID, fileName, false)
assert.Equal(t, fileName, actual)
assert.Nil(t, err)
})
@ -231,7 +233,7 @@ func TestSaveFile(t *testing.T) {
}
mockedFileBackend.On("WriteFile", mockedReadCloseSeek, mock.Anything).Return(writeFileFunc, writeFileErrorFunc)
actual, err := th.App.SaveFile(mockedReadCloseSeek, "1", "test-board-id", fileName)
actual, err := th.App.SaveFile(mockedReadCloseSeek, "1", "test-board-id", fileName, false)
assert.Nil(t, err)
assert.NotNil(t, actual)
})
@ -255,7 +257,7 @@ func TestSaveFile(t *testing.T) {
}
mockedFileBackend.On("WriteFile", mockedReadCloseSeek, mock.Anything).Return(writeFileFunc, writeFileErrorFunc)
actual, err := th.App.SaveFile(mockedReadCloseSeek, "1", "test-board-id", fileName)
actual, err := th.App.SaveFile(mockedReadCloseSeek, "1", "test-board-id", fileName, false)
assert.Equal(t, "", actual)
assert.Equal(t, "unable to store the file in the files storage: Mocked File backend error", err.Error())
})
@ -265,7 +267,7 @@ func TestGetFileInfo(t *testing.T) {
th, _ := SetupTestHelper(t)
t.Run("should return file info", func(t *testing.T) {
fileInfo := &mmModel.FileInfo{
fileInfo := &mm_model.FileInfo{
Id: "file_info_id",
Archived: false,
}
@ -284,7 +286,7 @@ func TestGetFileInfo(t *testing.T) {
})
t.Run("should return archived file info", func(t *testing.T) {
fileInfo := &mmModel.FileInfo{
fileInfo := &mm_model.FileInfo{
Id: "file_info_id",
Archived: true,
}
@ -308,11 +310,10 @@ func TestGetFileInfo(t *testing.T) {
func TestGetFile(t *testing.T) {
th, _ := SetupTestHelper(t)
t.Run("when FileInfo exists", func(t *testing.T) {
th.Store.EXPECT().GetFileInfo("fileInfoID").Return(&mmModel.FileInfo{
t.Run("happy path, no errors", func(t *testing.T) {
th.Store.EXPECT().GetFileInfo("fileInfoID").Return(&mm_model.FileInfo{
Id: "fileInfoID",
Path: "/path/to/file/fileName.txt",
Path: testPath,
}, nil)
mockedFileBackend := &mocks.FileBackend{}
@ -325,8 +326,8 @@ func TestGetFile(t *testing.T) {
readerErrorFunc := func(path string) error {
return nil
}
mockedFileBackend.On("Reader", "/path/to/file/fileName.txt").Return(readerFunc, readerErrorFunc)
mockedFileBackend.On("FileExists", "/path/to/file/fileName.txt").Return(true, nil)
mockedFileBackend.On("Reader", testPath).Return(readerFunc, readerErrorFunc)
mockedFileBackend.On("FileExists", testPath).Return(true, nil)
fileInfo, seeker, err := th.App.GetFile("teamID", "boardID", "7fileInfoID.txt")
assert.NoError(t, err)
@ -334,51 +335,222 @@ func TestGetFile(t *testing.T) {
assert.NotNil(t, seeker)
})
t.Run("when GetFilePath() throws error", func(t *testing.T) {
th.Store.EXPECT().GetFileInfo("fileInfoID").Return(nil, errDummy)
fileInfo, seeker, err := th.App.GetFile("teamID", "boardID", "7fileInfoID.txt")
assert.Error(t, err)
assert.Nil(t, fileInfo)
assert.Nil(t, seeker)
})
t.Run("when FileExists returns false", func(t *testing.T) {
th.Store.EXPECT().GetFileInfo("fileInfoID").Return(&mm_model.FileInfo{
Id: "fileInfoID",
Path: testPath,
}, nil)
mockedFileBackend := &mocks.FileBackend{}
th.App.filesBackend = mockedFileBackend
mockedFileBackend.On("FileExists", testPath).Return(false, nil)
fileInfo, seeker, err := th.App.GetFile("teamID", "boardID", "7fileInfoID.txt")
assert.Error(t, err)
assert.Nil(t, fileInfo)
assert.Nil(t, seeker)
})
t.Run("when FileReader throws error", func(t *testing.T) {
th.Store.EXPECT().GetFileInfo("fileInfoID").Return(&mm_model.FileInfo{
Id: "fileInfoID",
Path: testPath,
}, nil)
mockedFileBackend := &mocks.FileBackend{}
th.App.filesBackend = mockedFileBackend
mockedFileBackend.On("Reader", testPath).Return(nil, errDummy)
mockedFileBackend.On("FileExists", testPath).Return(true, nil)
fileInfo, seeker, err := th.App.GetFile("teamID", "boardID", "7fileInfoID.txt")
assert.Error(t, err)
assert.Nil(t, fileInfo)
assert.Nil(t, seeker)
})
}
func TestGetFilePath(t *testing.T) {
th, _ := SetupTestHelper(t)
t.Run("when FileInfo exists", func(t *testing.T) {
th.Store.EXPECT().GetFileInfo("fileInfoID").Return(&mm_model.FileInfo{
Id: "fileInfoID",
Path: testPath,
}, nil)
fileInfo, filePath, err := th.App.GetFilePath("teamID", "boardID", "7fileInfoID.txt")
assert.NoError(t, err)
assert.NotNil(t, fileInfo)
assert.Equal(t, testPath, filePath)
})
t.Run("when FileInfo doesn't exist", func(t *testing.T) {
th.Store.EXPECT().GetFileInfo("fileInfoID").Return(nil, nil)
mockedFileBackend := &mocks.FileBackend{}
th.App.filesBackend = mockedFileBackend
mockedReadCloseSeek := &mocks.ReadCloseSeeker{}
readerFunc := func(path string) filestore.ReadCloseSeeker {
return mockedReadCloseSeek
}
readerErrorFunc := func(path string) error {
return nil
}
mockedFileBackend.On("Reader", "teamID/boardID/7fileInfoID.txt").Return(readerFunc, readerErrorFunc)
mockedFileBackend.On("FileExists", "teamID/boardID/7fileInfoID.txt").Return(true, nil)
fileInfo, seeker, err := th.App.GetFile("teamID", "boardID", "7fileInfoID.txt")
fileInfo, filePath, err := th.App.GetFilePath("teamID", "boardID", "7fileInfoID.txt")
assert.NoError(t, err)
assert.Nil(t, fileInfo)
assert.NotNil(t, seeker)
assert.Equal(t, "teamID/boardID/7fileInfoID.txt", filePath)
})
t.Run("when FileInfo exists but FileInfo.Path is not set", func(t *testing.T) {
th.Store.EXPECT().GetFileInfo("fileInfoID").Return(&mmModel.FileInfo{
th.Store.EXPECT().GetFileInfo("fileInfoID").Return(&mm_model.FileInfo{
Id: "fileInfoID",
Path: "",
}, nil)
mockedFileBackend := &mocks.FileBackend{}
th.App.filesBackend = mockedFileBackend
mockedReadCloseSeek := &mocks.ReadCloseSeeker{}
readerFunc := func(path string) filestore.ReadCloseSeeker {
return mockedReadCloseSeek
}
readerErrorFunc := func(path string) error {
return nil
}
mockedFileBackend.On("Reader", "teamID/boardID/7fileInfoID.txt").Return(readerFunc, readerErrorFunc)
mockedFileBackend.On("FileExists", "teamID/boardID/7fileInfoID.txt").Return(true, nil)
fileInfo, seeker, err := th.App.GetFile("teamID", "boardID", "7fileInfoID.txt")
fileInfo, filePath, err := th.App.GetFilePath("teamID", "boardID", "7fileInfoID.txt")
assert.NoError(t, err)
assert.NotNil(t, fileInfo)
assert.NotNil(t, seeker)
assert.Equal(t, "teamID/boardID/7fileInfoID.txt", filePath)
})
}
func TestCopyCard(t *testing.T) {
th, _ := SetupTestHelper(t)
imageBlock := &model.Block{
ID: "imageBlock",
ParentID: "c3zqnh6fsu3f4mr6hzq9hizwske",
CreatedBy: "6k6ynxdp47dujjhhojw9nqhmyh",
ModifiedBy: "6k6ynxdp47dujjhhojw9nqhmyh",
Schema: 1,
Type: "image",
Title: "",
Fields: map[string]interface{}{"fileId": "7fileName.jpg"},
CreateAt: 1680725585250,
UpdateAt: 1680725585250,
DeleteAt: 0,
BoardID: "boardID",
}
t.Run("Board doesn't exist", func(t *testing.T) {
th.Store.EXPECT().GetBoard("boardID").Return(nil, errDummy)
_, err := th.App.CopyCardFiles("boardID", []*model.Block{}, false)
assert.Error(t, err)
})
t.Run("Board exists, image block, with FileInfo", func(t *testing.T) {
fileInfo := &mm_model.FileInfo{
Id: "imageBlock",
Path: testPath,
}
th.Store.EXPECT().GetBoard("boardID").Return(&model.Board{
ID: "boardID",
IsTemplate: false,
}, nil)
th.Store.EXPECT().GetFileInfo("fileName").Return(fileInfo, nil)
th.Store.EXPECT().SaveFileInfo(fileInfo).Return(nil)
mockedFileBackend := &mocks.FileBackend{}
th.App.filesBackend = mockedFileBackend
mockedFileBackend.On("CopyFile", mock.Anything, mock.Anything).Return(nil)
updatedFileNames, err := th.App.CopyCardFiles("boardID", []*model.Block{imageBlock}, false)
assert.NoError(t, err)
assert.Equal(t, "7fileName.jpg", imageBlock.Fields["fileId"])
assert.NotNil(t, updatedFileNames["7fileName.jpg"])
assert.NotNil(t, updatedFileNames[imageBlock.Fields["fileId"].(string)])
})
t.Run("Board exists, attachment block, with FileInfo", func(t *testing.T) {
attachmentBlock := &model.Block{
ID: "attachmentBlock",
ParentID: "c3zqnh6fsu3f4mr6hzq9hizwske",
CreatedBy: "6k6ynxdp47dujjhhojw9nqhmyh",
ModifiedBy: "6k6ynxdp47dujjhhojw9nqhmyh",
Schema: 1,
Type: "attachment",
Title: "",
Fields: map[string]interface{}{"fileId": "7fileName.jpg"},
CreateAt: 1680725585250,
UpdateAt: 1680725585250,
DeleteAt: 0,
BoardID: "boardID",
}
fileInfo := &mm_model.FileInfo{
Id: "attachmentBlock",
Path: testPath,
}
th.Store.EXPECT().GetBoard("boardID").Return(&model.Board{
ID: "boardID",
IsTemplate: false,
}, nil)
th.Store.EXPECT().GetFileInfo("fileName").Return(fileInfo, nil)
th.Store.EXPECT().SaveFileInfo(fileInfo).Return(nil)
mockedFileBackend := &mocks.FileBackend{}
th.App.filesBackend = mockedFileBackend
mockedFileBackend.On("CopyFile", mock.Anything, mock.Anything).Return(nil)
updatedFileNames, err := th.App.CopyCardFiles("boardID", []*model.Block{attachmentBlock}, false)
assert.NoError(t, err)
assert.NotNil(t, updatedFileNames[imageBlock.Fields["fileId"].(string)])
})
t.Run("Board exists, image block, without FileInfo", func(t *testing.T) {
th.Store.EXPECT().GetBoard("boardID").Return(&model.Board{
ID: "boardID",
IsTemplate: false,
}, nil)
th.Store.EXPECT().GetFileInfo(gomock.Any()).Return(nil, nil)
th.Store.EXPECT().SaveFileInfo(gomock.Any()).Return(nil)
mockedFileBackend := &mocks.FileBackend{}
th.App.filesBackend = mockedFileBackend
mockedFileBackend.On("CopyFile", mock.Anything, mock.Anything).Return(nil)
updatedFileNames, err := th.App.CopyCardFiles("boardID", []*model.Block{imageBlock}, false)
assert.NoError(t, err)
assert.NotNil(t, imageBlock.Fields["fileId"].(string))
assert.NotNil(t, updatedFileNames[imageBlock.Fields["fileId"].(string)])
})
}
func TestCopyAndUpdateCardFiles(t *testing.T) {
th, _ := SetupTestHelper(t)
imageBlock := &model.Block{
ID: "imageBlock",
ParentID: "c3zqnh6fsu3f4mr6hzq9hizwske",
CreatedBy: "6k6ynxdp47dujjhhojw9nqhmyh",
ModifiedBy: "6k6ynxdp47dujjhhojw9nqhmyh",
Schema: 1,
Type: "image",
Title: "",
Fields: map[string]interface{}{"fileId": "7fileName.jpg"},
CreateAt: 1680725585250,
UpdateAt: 1680725585250,
DeleteAt: 0,
BoardID: "boardID",
}
t.Run("Board exists, image block, with FileInfo", func(t *testing.T) {
fileInfo := &mm_model.FileInfo{
Id: "imageBlock",
Path: testPath,
}
th.Store.EXPECT().GetBoard("boardID").Return(&model.Board{
ID: "boardID",
IsTemplate: false,
}, nil)
th.Store.EXPECT().GetFileInfo("fileName").Return(fileInfo, nil)
th.Store.EXPECT().SaveFileInfo(fileInfo).Return(nil)
th.Store.EXPECT().PatchBlocks(gomock.Any(), "userID").Return(nil)
mockedFileBackend := &mocks.FileBackend{}
th.App.filesBackend = mockedFileBackend
mockedFileBackend.On("CopyFile", mock.Anything, mock.Anything).Return(nil)
err := th.App.CopyAndUpdateCardFiles("boardID", "userID", []*model.Block{imageBlock}, false)
assert.NoError(t, err)
assert.NotEqual(t, testPath, imageBlock.Fields["fileId"])
})
}

View File

@ -41,27 +41,19 @@ func (a *App) ImportArchive(r io.Reader, opt model.ImportArchiveOptions) error {
a.logger.Debug("importing legacy archive")
_, errImport := a.ImportBoardJSONL(br, opt)
go func() {
if err := a.UpdateCardLimitTimestamp(); err != nil {
a.logger.Error(
"UpdateCardLimitTimestamp failed after importing a legacy file",
mlog.Err(err),
)
}
}()
return errImport
}
a.logger.Debug("importing archive")
zr := zipstream.NewReader(br)
boardMap := make(map[string]string) // maps old board ids to new
boardMap := make(map[string]*model.Board) // maps old board ids to new
fileMap := make(map[string]string) // maps old fileIds to new
for {
hdr, err := zr.Next()
if err != nil {
if errors.Is(err, io.EOF) {
a.fixImagesAttachments(boardMap, fileMap, opt.TeamID, opt.ModifiedBy)
a.logger.Debug("import archive - done", mlog.Int("boards_imported", len(boardMap)))
return nil
}
@ -81,14 +73,14 @@ func (a *App) ImportArchive(r io.Reader, opt model.ImportArchiveOptions) error {
return model.NewErrUnsupportedArchiveVersion(ver, archiveVersion)
}
case "board.jsonl":
boardID, err := a.ImportBoardJSONL(zr, opt)
board, err := a.ImportBoardJSONL(zr, opt)
if err != nil {
return fmt.Errorf("cannot import board %s: %w", dir, err)
}
boardMap[dir] = boardID
boardMap[dir] = board
default:
// import file/image; dir is the old board id
boardID, ok := boardMap[dir]
board, ok := boardMap[dir]
if !ok {
a.logger.Warn("skipping orphan image in archive",
mlog.String("dir", dir),
@ -96,33 +88,65 @@ func (a *App) ImportArchive(r io.Reader, opt model.ImportArchiveOptions) error {
)
continue
}
// save file with original filename so it matches name in image block.
filePath := filepath.Join(opt.TeamID, boardID, filename)
_, err := a.filesBackend.WriteFile(zr, filePath)
newFileName, err := a.SaveFile(zr, opt.TeamID, board.ID, filename, board.IsTemplate)
if err != nil {
return fmt.Errorf("cannot import file %s for board %s: %w", filename, dir, err)
}
fileMap[filename] = newFileName
a.logger.Debug("import archive file",
mlog.String("TeamID", opt.TeamID),
mlog.String("boardID", board.ID),
mlog.String("filename", filename),
mlog.String("newFileName", newFileName),
)
}
}
}
// Update image and attachment blocks.
func (a *App) fixImagesAttachments(boardMap map[string]*model.Board, fileMap map[string]string, teamID string, userID string) {
blockIDs := make([]string, 0)
blockPatches := make([]model.BlockPatch, 0)
for _, board := range boardMap {
if board.IsTemplate {
continue
}
a.logger.Trace("import archive file",
mlog.String("dir", dir),
mlog.String("filename", filename),
)
opts := model.QueryBlocksOptions{
BoardID: board.ID,
}
newBlocks, err := a.store.GetBlocks(opts)
if err != nil {
a.logger.Info("cannot retrieve imported blocks for board", mlog.String("BoardID", board.ID), mlog.Err(err))
return
}
go func() {
if err := a.UpdateCardLimitTimestamp(); err != nil {
a.logger.Error(
"UpdateCardLimitTimestamp failed after importing an archive",
mlog.Err(err),
)
for _, block := range newBlocks {
if block.Type == "image" || block.Type == "attachment" {
fieldName := "fileId"
oldID := block.Fields[fieldName]
blockIDs = append(blockIDs, block.ID)
blockPatches = append(blockPatches, model.BlockPatch{
UpdatedFields: map[string]interface{}{
fieldName: fileMap[oldID.(string)],
},
})
}
}()
}
blockPatchBatch := model.BlockPatchBatch{BlockIDs: blockIDs, BlockPatches: blockPatches}
err = a.PatchBlocks(teamID, &blockPatchBatch, userID)
if err != nil {
a.logger.Info("Error patching blocks for image import", mlog.Err(err))
}
}
}
// ImportBoardJSONL imports a JSONL file containing blocks for one board. The resulting
// board id is returned.
func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (string, error) {
func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (*model.Board, error) {
// TODO: Stream this once `model.GenerateBlockIDs` can take a stream of blocks.
// We don't want to load the whole file in memory, even though it's a single board.
boardsAndBlocks := &model.BoardsAndBlocks{
@ -155,7 +179,7 @@ func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (str
if !skip {
var archiveLine model.ArchiveLine
if err := json.Unmarshal(line, &archiveLine); err != nil {
return "", fmt.Errorf("error parsing archive line %d: %w", lineNum, err)
return nil, fmt.Errorf("error parsing archive line %d: %w", lineNum, err)
}
// first line must be a board
@ -167,7 +191,7 @@ func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (str
case "board":
var board model.Board
if err2 := json.Unmarshal(archiveLine.Data, &board); err2 != nil {
return "", fmt.Errorf("invalid board in archive line %d: %w", lineNum, err2)
return nil, fmt.Errorf("invalid board in archive line %d: %w", lineNum, err2)
}
board.ModifiedBy = userID
board.UpdateAt = now
@ -178,20 +202,20 @@ func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (str
// legacy archives encoded boards as blocks; we need to convert them to real boards.
var block *model.Block
if err2 := json.Unmarshal(archiveLine.Data, &block); err2 != nil {
return "", fmt.Errorf("invalid board block in archive line %d: %w", lineNum, err2)
return nil, fmt.Errorf("invalid board block in archive line %d: %w", lineNum, err2)
}
block.ModifiedBy = userID
block.UpdateAt = now
board, err := a.blockToBoard(block, opt)
if err != nil {
return "", fmt.Errorf("cannot convert archive line %d to block: %w", lineNum, err)
return nil, fmt.Errorf("cannot convert archive line %d to block: %w", lineNum, err)
}
boardsAndBlocks.Boards = append(boardsAndBlocks.Boards, board)
boardID = board.ID
case "block":
var block *model.Block
if err2 := json.Unmarshal(archiveLine.Data, &block); err2 != nil {
return "", fmt.Errorf("invalid block in archive line %d: %w", lineNum, err2)
return nil, fmt.Errorf("invalid block in archive line %d: %w", lineNum, err2)
}
block.ModifiedBy = userID
block.UpdateAt = now
@ -200,11 +224,11 @@ func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (str
case "boardMember":
var boardMember *model.BoardMember
if err2 := json.Unmarshal(archiveLine.Data, &boardMember); err2 != nil {
return "", fmt.Errorf("invalid board Member in archive line %d: %w", lineNum, err2)
return nil, fmt.Errorf("invalid board Member in archive line %d: %w", lineNum, err2)
}
boardMembers = append(boardMembers, boardMember)
default:
return "", model.NewErrUnsupportedArchiveLineType(lineNum, archiveLine.Type)
return nil, model.NewErrUnsupportedArchiveLineType(lineNum, archiveLine.Type)
}
firstLine = false
}
@ -214,7 +238,7 @@ func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (str
if errors.Is(errRead, io.EOF) {
break
}
return "", fmt.Errorf("error reading archive line %d: %w", lineNum, errRead)
return nil, fmt.Errorf("error reading archive line %d: %w", lineNum, errRead)
}
lineNum++
}
@ -231,12 +255,12 @@ func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (str
var err error
boardsAndBlocks, err = model.GenerateBoardsAndBlocksIDs(boardsAndBlocks, a.logger)
if err != nil {
return "", fmt.Errorf("error generating archive block IDs: %w", err)
return nil, fmt.Errorf("error generating archive block IDs: %w", err)
}
boardsAndBlocks, err = a.CreateBoardsAndBlocks(boardsAndBlocks, opt.ModifiedBy, false)
if err != nil {
return "", fmt.Errorf("error inserting archive blocks: %w", err)
return nil, fmt.Errorf("error inserting archive blocks: %w", err)
}
// add users to all the new boards (if not the fake system user).
@ -248,7 +272,7 @@ func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (str
SchemeAdmin: true,
}
if _, err2 := a.AddMemberToBoard(adminMember); err2 != nil {
return "", fmt.Errorf("cannot add adminMember to board: %w", err2)
return nil, fmt.Errorf("cannot add adminMember to board: %w", err2)
}
for _, boardMember := range boardMembers {
bm := &model.BoardMember{
@ -263,16 +287,16 @@ func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (str
Synthetic: boardMember.Synthetic,
}
if _, err2 := a.AddMemberToBoard(bm); err2 != nil {
return "", fmt.Errorf("cannot add member to board: %w", err2)
return nil, fmt.Errorf("cannot add member to board: %w", err2)
}
}
}
// find new board id
for _, board := range boardsAndBlocks.Boards {
return board.ID, nil
return board, nil
}
return "", fmt.Errorf("missing board in archive: %w", model.ErrInvalidBoardBlock)
return nil, fmt.Errorf("missing board in archive: %w", model.ErrInvalidBoardBlock)
}
// fixBoardsandBlocks allows the caller of `ImportArchive` to modify or filters boards and blocks being

View File

@ -134,9 +134,76 @@ func TestApp_ImportArchive(t *testing.T) {
th.Store.EXPECT().GetUserByID("hxxzooc3ff8cubsgtcmpn8733e").AnyTimes().Return(user2, nil)
th.Store.EXPECT().GetUserByID("nto73edn5ir6ifimo5a53y1dwa").AnyTimes().Return(user3, nil)
boardID, err := th.App.ImportBoardJSONL(r, opts)
require.Equal(t, board.ID, boardID, "Board ID should be same")
newBoard, err := th.App.ImportBoardJSONL(r, opts)
require.NoError(t, err, "import archive should not fail")
require.Equal(t, board.ID, newBoard.ID, "Board ID should be same")
})
t.Run("fix image and attachment", func(t *testing.T) {
boardMap := map[string]*model.Board{
"test": board,
}
fileMap := map[string]string{
"oldFileName1.jpg": "newFileName1.jpg",
"oldFileName2.jpg": "newFileName2.jpg",
}
imageBlock := &model.Block{
ID: "blockID-1",
ParentID: "c3zqnh6fsu3f4mr6hzq9hizwske",
CreatedBy: "6k6ynxdp47dujjhhojw9nqhmyh",
ModifiedBy: "6k6ynxdp47dujjhhojw9nqhmyh",
Schema: 1,
Type: "image",
Title: "",
Fields: map[string]interface{}{"fileId": "oldFileName1.jpg"},
CreateAt: 1680725585250,
UpdateAt: 1680725585250,
DeleteAt: 0,
BoardID: "board-id",
}
attachmentBlock := &model.Block{
ID: "blockID-2",
ParentID: "c3zqnh6fsu3f4mr6hzq9hizwske",
CreatedBy: "6k6ynxdp47dujjhhojw9nqhmyh",
ModifiedBy: "6k6ynxdp47dujjhhojw9nqhmyh",
Schema: 1,
Type: "attachment",
Title: "",
Fields: map[string]interface{}{"fileId": "oldFileName2.jpg"},
CreateAt: 1680725585250,
UpdateAt: 1680725585250,
DeleteAt: 0,
BoardID: "board-id",
}
blockIDs := []string{"blockID-1", "blockID-2"}
blockPatch := model.BlockPatch{
UpdatedFields: map[string]interface{}{"fileId": "newFileName1.jpg"},
}
blockPatch2 := model.BlockPatch{
UpdatedFields: map[string]interface{}{"fileId": "newFileName2.jpg"},
}
blockPatches := []model.BlockPatch{blockPatch, blockPatch2}
blockPatchesBatch := model.BlockPatchBatch{BlockIDs: blockIDs, BlockPatches: blockPatches}
opts := model.QueryBlocksOptions{
BoardID: board.ID,
}
th.Store.EXPECT().GetBlocks(opts).Return([]*model.Block{imageBlock, attachmentBlock}, nil)
th.Store.EXPECT().GetBlocksByIDs(blockIDs).Return([]*model.Block{imageBlock, attachmentBlock}, nil)
th.Store.EXPECT().GetBlock(blockIDs[0]).Return(imageBlock, nil)
th.Store.EXPECT().GetBlock(blockIDs[1]).Return(attachmentBlock, nil)
th.Store.EXPECT().GetMembersForBoard("board-id").AnyTimes().Return([]*model.BoardMember{}, nil)
th.Store.EXPECT().PatchBlocks(&blockPatchesBatch, "my-userid")
th.App.fixImagesAttachments(boardMap, fileMap, "test-team", "my-userid")
})
}

View File

@ -49,6 +49,7 @@ func TestApp_initializeTemplates(t *testing.T) {
th.Store.EXPECT().GetMembersForBoard(board.ID).AnyTimes().Return([]*model.BoardMember{}, nil)
th.Store.EXPECT().GetBoard(board.ID).AnyTimes().Return(board, nil)
th.Store.EXPECT().GetMemberForBoard(gomock.Any(), gomock.Any()).AnyTimes().Return(boardMember, nil)
th.Store.EXPECT().SaveFileInfo(gomock.Any()).Return(nil).AnyTimes()
th.FilesBackend.On("WriteFile", mock.Anything, mock.Anything).Return(int64(1), nil)

View File

@ -3375,7 +3375,7 @@ func TestPermissionsGetFile(t *testing.T) {
clients := setupClients(th)
testData := setupData(t, th)
newFileID, err := th.Server.App().SaveFile(bytes.NewBuffer([]byte("test")), "test-team", testData.privateBoard.ID, "test.png")
newFileID, err := th.Server.App().SaveFile(bytes.NewBuffer([]byte("test")), "test-team", testData.privateBoard.ID, "test.png", false)
require.NoError(t, err)
ttCases := ttCasesF()
@ -3390,7 +3390,7 @@ func TestPermissionsGetFile(t *testing.T) {
clients := setupLocalClients(th)
testData := setupData(t, th)
newFileID, err := th.Server.App().SaveFile(bytes.NewBuffer([]byte("test")), "test-team", testData.privateBoard.ID, "test.png")
newFileID, err := th.Server.App().SaveFile(bytes.NewBuffer([]byte("test")), "test-team", testData.privateBoard.ID, "test.png", false)
require.NoError(t, err)
ttCases := ttCasesF()

25
server/model/file.go Normal file
View File

@ -0,0 +1,25 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"mime"
"path/filepath"
"strings"
"github.com/mattermost/focalboard/server/utils"
mm_model "github.com/mattermost/mattermost-server/v6/model"
)
func NewFileInfo(name string) *mm_model.FileInfo {
extension := strings.ToLower(filepath.Ext(name))
now := utils.GetMillis()
return &mm_model.FileInfo{
CreatorId: "boards",
CreateAt: now,
UpdateAt: now,
Name: name,
Extension: extension,
MimeType: mime.TypeByExtension(extension),
}
}

View File

@ -3,7 +3,7 @@
import {Block, createBlock} from './block'
type AttachmentBlockFields = {
attachmentId: string
fileId: string
}
type AttachmentBlock = Block & {
@ -18,7 +18,7 @@ function createAttachmentBlock(block?: Block): AttachmentBlock {
...createBlock(block),
type: 'attachment',
fields: {
attachmentId: block?.fields.attachmentId || '',
fileId: block?.fields.attachmentId || block?.fields.fileId || '',
},
isUploading: false,
uploadingPercent: 0,

View File

@ -151,7 +151,7 @@ const CardDialog = (props: Props): JSX.Element => {
Utils.selectLocalFile(async (attachment) => {
const uploadingBlock = createBlock()
uploadingBlock.title = attachment.name
uploadingBlock.fields.attachmentId = attachment.name
uploadingBlock.fields.fileId = attachment.name
uploadingBlock.boardId = boardId
if (card) {
uploadingBlock.parentId = card.id
@ -177,11 +177,11 @@ const CardDialog = (props: Props): JSX.Element => {
xhr.onload = () => {
if (xhr.status === 200 && xhr.readyState === 4) {
const json = JSON.parse(xhr.response)
const attachmentId = json.fileId
if (attachmentId) {
const fileId = json.fileId
if (fileId) {
removeUploadingAttachment(uploadingBlock)
const block = createAttachmentBlock()
block.fields.attachmentId = attachmentId || ''
block.fields.fileId = fileId || ''
block.title = attachment.name
sendFlashMessage({content: intl.formatMessage({id: 'AttachmentBlock.uploadSuccess', defaultMessage: 'Attachment uploaded successfull.'}), severity: 'normal'})
resolve(block)

View File

@ -39,7 +39,7 @@ describe('component/content/FileBlock', () => {
type: 'attachment',
title: 'test-title',
fields: {
attachmentId: 'test.txt',
fileId: 'test.txt',
},
createdBy: 'test-user-id',
createAt: 0,

View File

@ -50,7 +50,7 @@ const AttachmentElement = (props: Props): JSX.Element|null => {
})
return
}
const attachmentInfo = await octoClient.getFileInfo(block.boardId, block.fields.attachmentId)
const attachmentInfo = await octoClient.getFileInfo(block.boardId, block.fields.fileId)
setFileInfo(attachmentInfo)
}
loadFile()
@ -113,7 +113,7 @@ const AttachmentElement = (props: Props): JSX.Element|null => {
}
const attachmentDownloadHandler = async () => {
const attachment = await octoClient.getFileAsDataUrl(block.boardId, block.fields.attachmentId)
const attachment = await octoClient.getFileAsDataUrl(block.boardId, block.fields.fileId)
const anchor = document.createElement('a')
anchor.href = attachment.url || ''
anchor.download = fileInfo.name || ''