diff --git a/server/boards/api/blocks.go b/server/boards/api/blocks.go index 400ff624c1..49add99fef 100644 --- a/server/boards/api/blocks.go +++ b/server/boards/api/blocks.go @@ -304,7 +304,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 } diff --git a/server/boards/api/files.go b/server/boards/api/files.go index 2ca2102863..482fa0a622 100644 --- a/server/boards/api/files.go +++ b/server/boards/api/files.go @@ -312,7 +312,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 diff --git a/server/boards/app/blocks.go b/server/boards/app/blocks.go index 41de8d5736..136bf736fe 100644 --- a/server/boards/app/blocks.go +++ b/server/boards/app/blocks.go @@ -7,11 +7,9 @@ import ( "errors" "fmt" "path/filepath" - "strings" "github.com/mattermost/mattermost-server/server/v8/boards/model" "github.com/mattermost/mattermost-server/server/v8/boards/services/notify" - "github.com/mattermost/mattermost-server/server/v8/boards/utils" "github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog" ) @@ -39,6 +37,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) @@ -286,95 +289,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) } diff --git a/server/boards/app/boards.go b/server/boards/app/boards.go index ff81e81bff..1f7fc8c026 100644 --- a/server/boards/app/boards.go +++ b/server/boards/app/boards.go @@ -184,8 +184,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 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) } if !asTemplate { @@ -196,44 +201,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 { diff --git a/server/boards/app/export.go b/server/boards/app/export.go index 88cc95ab86..d8a03d78a8 100644 --- a/server/boards/app/export.go +++ b/server/boards/app/export.go @@ -95,10 +95,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) } @@ -208,7 +208,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", @@ -218,9 +221,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 } @@ -239,10 +242,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) diff --git a/server/boards/app/files.go b/server/boards/app/files.go index 9ca07ef323..0212a5176f 100644 --- a/server/boards/app/files.go +++ b/server/boards/app/files.go @@ -18,12 +18,10 @@ import ( "github.com/mattermost/mattermost-server/server/v8/platform/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" { @@ -31,44 +29,26 @@ 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 := &mm_model.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) (*mm_model.FileInfo, error) { @@ -79,8 +59,7 @@ func (a *App) GetFileInfo(filename string) (*mm_model.FileInfo, error) { // filename is in the format 7. // we want to extract the 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 { return nil, err @@ -90,10 +69,33 @@ func (a *App) GetFileInfo(filename string) (*mm_model.FileInfo, 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 @@ -104,22 +106,23 @@ func (a *App) GetFile(teamID, rootID, fileName string) (*mm_model.FileInfo, file 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 +178,121 @@ 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 +} diff --git a/server/boards/app/files_test.go b/server/boards/app/files_test.go index 229712896b..c494b8a872 100644 --- a/server/boards/app/files_test.go +++ b/server/boards/app/files_test.go @@ -15,6 +15,7 @@ import ( "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" + "github.com/mattermost/mattermost-server/server/v8/boards/model" mm_model "github.com/mattermost/mattermost-server/server/v8/model" "github.com/mattermost/mattermost-server/server/v8/platform/shared/filestore" "github.com/mattermost/mattermost-server/server/v8/platform/shared/filestore/mocks" @@ -210,7 +211,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.NoError(t, err) }) @@ -234,7 +235,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.NoError(t, err) assert.NotNil(t, actual) }) @@ -258,7 +259,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()) }) @@ -312,7 +313,7 @@ func TestGetFileInfo(t *testing.T) { func TestGetFile(t *testing.T) { th, _ := SetupTestHelper(t) - t.Run("when FileInfo exists", func(t *testing.T) { + 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", @@ -337,27 +338,72 @@ func TestGetFile(t *testing.T) { assert.NotNil(t, seeker) }) - t.Run("when FileInfo doesn't exist", func(t *testing.T) { - th.Store.EXPECT().GetFileInfo("fileInfoID").Return(nil, nil) + 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: "/path/to/file/fileName.txt", + }, 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) + mockedFileBackend.On("FileExists", "/path/to/file/fileName.txt").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: "/path/to/file/fileName.txt", + }, nil) + + mockedFileBackend := &mocks.FileBackend{} + th.App.filesBackend = mockedFileBackend + mockedFileBackend.On("Reader", "/path/to/file/fileName.txt").Return(nil, errDummy) + mockedFileBackend.On("FileExists", "/path/to/file/fileName.txt").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) { + path := "/path/to/file/fileName.txt" + th.Store.EXPECT().GetFileInfo("fileInfoID").Return(&mm_model.FileInfo{ + Id: "fileInfoID", + Path: path, + }, nil) + + fileInfo, filePath, err := th.App.GetFilePath("teamID", "boardID", "7fileInfoID.txt") + assert.NoError(t, err) + assert.NotNil(t, fileInfo) + assert.Equal(t, path, filePath) + }) + + t.Run("when FileInfo doesn't exist", func(t *testing.T) { + th.Store.EXPECT().GetFileInfo("fileInfoID").Return(nil, nil) + + 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) { @@ -366,22 +412,158 @@ func TestGetFile(t *testing.T) { 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) { + path := "/path/to/file/fileName.txt" + fileInfo := &mm_model.FileInfo{ + Id: "imageBlock", + Path: path, + } + 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", + } + + path := "/path/to/file/fileName.txt" + fileInfo := &mm_model.FileInfo{ + Id: "attachmentBlock", + Path: path, + } + 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) { + // path := "/path/to/file/fileName.txt" + // fileInfo := &mm_model.FileInfo{ + // Id: "imageBlock", + // Path: path, + // } + 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) { + path := "/path/to/file/fileName.txt" + fileInfo := &mm_model.FileInfo{ + Id: "imageBlock", + Path: path, + } + 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, path, imageBlock.Fields["fileId"]) }) } diff --git a/server/boards/app/import.go b/server/boards/app/import.go index 7f694e2717..1b30c09a88 100644 --- a/server/boards/app/import.go +++ b/server/boards/app/import.go @@ -44,27 +44,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 } @@ -84,14 +76,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), @@ -99,33 +91,63 @@ 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.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} + a.PatchBlocks(teamID, &blockPatchBatch, userId) } } // 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{ @@ -158,7 +180,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 @@ -170,7 +192,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 @@ -181,20 +203,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 @@ -203,11 +225,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 } @@ -217,7 +239,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++ } @@ -234,12 +256,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). @@ -251,7 +273,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{ @@ -266,16 +288,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 diff --git a/server/boards/app/import_test.go b/server/boards/app/import_test.go index 89d73335bf..061549cbad 100644 --- a/server/boards/app/import_test.go +++ b/server/boards/app/import_test.go @@ -138,9 +138,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") }) } diff --git a/server/boards/app/templates_test.go b/server/boards/app/templates_test.go index bb1eb9bd3a..496929a7ef 100644 --- a/server/boards/app/templates_test.go +++ b/server/boards/app/templates_test.go @@ -53,6 +53,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) diff --git a/server/boards/integrationtests/permissions_test.go b/server/boards/integrationtests/permissions_test.go index 936e15bfea..a784ef2bf3 100644 --- a/server/boards/integrationtests/permissions_test.go +++ b/server/boards/integrationtests/permissions_test.go @@ -3379,7 +3379,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() @@ -3394,7 +3394,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() diff --git a/server/boards/model/file.go b/server/boards/model/file.go new file mode 100644 index 0000000000..3a00008cac --- /dev/null +++ b/server/boards/model/file.go @@ -0,0 +1,27 @@ +// 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/mattermost-server/server/v8/boards/utils" + mm_model "github.com/mattermost/mattermost-server/server/v8/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), + } + +} diff --git a/webapp/boards/src/blocks/attachmentBlock.tsx b/webapp/boards/src/blocks/attachmentBlock.tsx index bf0568505a..f84e6d051a 100644 --- a/webapp/boards/src/blocks/attachmentBlock.tsx +++ b/webapp/boards/src/blocks/attachmentBlock.tsx @@ -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, diff --git a/webapp/boards/src/components/cardDialog.tsx b/webapp/boards/src/components/cardDialog.tsx index 7b8be86c20..b1187db750 100644 --- a/webapp/boards/src/components/cardDialog.tsx +++ b/webapp/boards/src/components/cardDialog.tsx @@ -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.'}), severity: 'normal'}) resolve(block) diff --git a/webapp/boards/src/components/content/attachmentElement.test.tsx b/webapp/boards/src/components/content/attachmentElement.test.tsx index 80a8ac6d77..1892368e62 100644 --- a/webapp/boards/src/components/content/attachmentElement.test.tsx +++ b/webapp/boards/src/components/content/attachmentElement.test.tsx @@ -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, diff --git a/webapp/boards/src/components/content/attachmentElement.tsx b/webapp/boards/src/components/content/attachmentElement.tsx index 1d18bf442b..58efbdce9b 100644 --- a/webapp/boards/src/components/content/attachmentElement.tsx +++ b/webapp/boards/src/components/content/attachmentElement.tsx @@ -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 || ''