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:
parent
b7d94a8fe2
commit
888c169910
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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"])
|
||||
})
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
25
server/model/file.go
Normal 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),
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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 || ''
|
||||
|
Loading…
Reference in New Issue
Block a user