mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
update import/export for attachments and new file paths (#22915)
* update import/export for attachments and new file paths * add unit test * update templates test * temp checking * cleanup * cleanup * more cleanup * lint fixes * cleanup some functions * cleanup * fix build breaks * fix tests for changes * move function to different file * more cleanup, code movement * unit test fixes * add unit tests * fix tests * more unit test fixes * test fix * revert package-lock.json * fixes from code review * fix export image * more fixes to remove attachmentId * change from removed api * update test * lint fixes * update for review comments * fix tests * test fix * lint fix * remove sprintf from logging, use mlog * more lint fixes * Update server/boards/app/files.go Co-authored-by: Doug Lauder <wiggin77@warpmail.net> * remove code --------- Co-authored-by: Mattermost Build <build@mattermost.com> Co-authored-by: Doug Lauder <wiggin77@warpmail.net>
This commit is contained in:
parent
ac35bdff68
commit
81ef403230
@ -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
|
// this query param exists when creating template from board, or board from template
|
||||||
sourceBoardID := r.URL.Query().Get("sourceBoardID")
|
sourceBoardID := r.URL.Query().Get("sourceBoardID")
|
||||||
if 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)
|
a.errorResponse(w, r, updateFileIDsErr)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -312,7 +312,7 @@ func (a *API) handleUploadFile(w http.ResponseWriter, r *http.Request) {
|
|||||||
auditRec.AddMeta("teamID", board.TeamID)
|
auditRec.AddMeta("teamID", board.TeamID)
|
||||||
auditRec.AddMeta("filename", handle.Filename)
|
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 {
|
if err != nil {
|
||||||
a.errorResponse(w, r, err)
|
a.errorResponse(w, r, err)
|
||||||
return
|
return
|
||||||
|
@ -7,11 +7,9 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/mattermost/mattermost-server/server/v8/boards/model"
|
"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/services/notify"
|
||||||
"github.com/mattermost/mattermost-server/server/v8/boards/utils"
|
|
||||||
|
|
||||||
"github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog"
|
"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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = a.CopyAndUpdateCardFiles(boardID, userID, blocks, asTemplate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
a.blockChangeNotifier.Enqueue(func() error {
|
a.blockChangeNotifier.Enqueue(func() error {
|
||||||
for _, block := range blocks {
|
for _, block := range blocks {
|
||||||
a.wsAdapter.BroadcastBlockChange(board.TeamID, block)
|
a.wsAdapter.BroadcastBlockChange(board.TeamID, block)
|
||||||
@ -286,95 +289,6 @@ func (a *App) InsertBlocksAndNotify(blocks []*model.Block, modifiedByID string,
|
|||||||
return blocks, nil
|
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) {
|
func (a *App) GetBlockByID(blockID string) (*model.Block, error) {
|
||||||
return a.store.GetBlock(blockID)
|
return a.store.GetBlock(blockID)
|
||||||
}
|
}
|
||||||
|
@ -184,8 +184,13 @@ func (a *App) DuplicateBoard(boardID, userID, toTeam string, asTemplate bool) (*
|
|||||||
}
|
}
|
||||||
|
|
||||||
// copy any file attachments from the duplicated blocks.
|
// copy any file attachments from the duplicated blocks.
|
||||||
if err = a.CopyCardFiles(boardID, bab.Blocks); err != nil {
|
err = a.CopyAndUpdateCardFiles(boardID, userID, bab.Blocks, asTemplate)
|
||||||
a.logger.Error("Could not copy files while duplicating board", mlog.String("BoardID", boardID), mlog.Err(err))
|
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 {
|
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 {
|
a.blockChangeNotifier.Enqueue(func() error {
|
||||||
teamID := ""
|
teamID := ""
|
||||||
for _, board := range bab.Boards {
|
for _, board := range bab.Boards {
|
||||||
|
@ -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 {
|
if err = a.writeArchiveBlockLine(w, block); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if block.Type == model.TypeImage {
|
if block.Type == model.TypeImage || block.Type == model.TypeAttachment {
|
||||||
filename, err2 := extractImageFilename(block)
|
filename, err2 := extractFilename(block)
|
||||||
if err2 != nil {
|
if err2 != nil {
|
||||||
return err
|
return err2
|
||||||
}
|
}
|
||||||
files = append(files, filename)
|
files = append(files, filename)
|
||||||
}
|
}
|
||||||
@ -208,7 +208,10 @@ func (a *App) writeArchiveFile(zw *zip.Writer, filename string, boardID string,
|
|||||||
return err
|
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 {
|
if err != nil {
|
||||||
// just log this; image file is missing but we'll still export an equivalent board
|
// just log this; image file is missing but we'll still export an equivalent board
|
||||||
a.logger.Error("image file missing for export",
|
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
|
return nil
|
||||||
}
|
}
|
||||||
defer src.Close()
|
defer fileReader.Close()
|
||||||
|
|
||||||
_, err = io.Copy(dest, src)
|
_, err = io.Copy(dest, fileReader)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -239,10 +242,13 @@ func (a *App) getBoardsForArchive(boardIDs []string) ([]model.Board, error) {
|
|||||||
return boards, nil
|
return boards, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractImageFilename(imageBlock *model.Block) (string, error) {
|
func extractFilename(block *model.Block) (string, error) {
|
||||||
f, ok := imageBlock.Fields["fileId"]
|
f, ok := block.Fields["fileId"]
|
||||||
if !ok {
|
if !ok {
|
||||||
return "", model.ErrInvalidImageBlock
|
f, ok = block.Fields["attachmentId"]
|
||||||
|
if !ok {
|
||||||
|
return "", model.ErrInvalidImageBlock
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
filename, ok := f.(string)
|
filename, ok := f.(string)
|
||||||
|
@ -18,12 +18,10 @@ import (
|
|||||||
"github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog"
|
"github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
const emptyString = "empty"
|
|
||||||
|
|
||||||
var errEmptyFilename = errors.New("IsFileArchived: empty filename not allowed")
|
var errEmptyFilename = errors.New("IsFileArchived: empty filename not allowed")
|
||||||
var ErrFileNotFound = errors.New("file not found")
|
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
|
// NOTE: File extension includes the dot
|
||||||
fileExtension := strings.ToLower(filepath.Ext(filename))
|
fileExtension := strings.ToLower(filepath.Ext(filename))
|
||||||
if fileExtension == ".jpeg" {
|
if fileExtension == ".jpeg" {
|
||||||
@ -31,44 +29,26 @@ func (a *App) SaveFile(reader io.Reader, teamID, rootID, filename string) (strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
createdFilename := utils.NewID(utils.IDTypeNone)
|
createdFilename := utils.NewID(utils.IDTypeNone)
|
||||||
fullFilename := fmt.Sprintf(`%s%s`, createdFilename, fileExtension)
|
newFileName := fmt.Sprintf(`%s%s`, createdFilename, fileExtension)
|
||||||
filePath := filepath.Join(utils.GetBaseFilePath(), fullFilename)
|
if asTemplate {
|
||||||
|
newFileName = filename
|
||||||
|
}
|
||||||
|
filePath := getDestinationFilePath(asTemplate, teamID, boardID, newFileName)
|
||||||
|
|
||||||
fileSize, appErr := a.filesBackend.WriteFile(reader, filePath)
|
fileSize, appErr := a.filesBackend.WriteFile(reader, filePath)
|
||||||
if appErr != nil {
|
if appErr != nil {
|
||||||
return "", fmt.Errorf("unable to store the file in the files storage: %w", appErr)
|
return "", fmt.Errorf("unable to store the file in the files storage: %w", appErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
now := utils.GetMillis()
|
fileInfo := model.NewFileInfo(filename)
|
||||||
|
fileInfo.Id = getFileInfoID(createdFilename)
|
||||||
fileInfo := &mm_model.FileInfo{
|
fileInfo.Path = filePath
|
||||||
Id: createdFilename[1:],
|
fileInfo.Size = fileSize
|
||||||
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,
|
|
||||||
}
|
|
||||||
err := a.store.SaveFileInfo(fileInfo)
|
err := a.store.SaveFileInfo(fileInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
return newFileName, nil
|
||||||
return fullFilename, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) GetFileInfo(filename string) (*mm_model.FileInfo, error) {
|
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<some-alphanumeric-string>.<extension>
|
// filename is in the format 7<some-alphanumeric-string>.<extension>
|
||||||
// we want to extract the <some-alphanumeric-string> part of this as this
|
// we want to extract the <some-alphanumeric-string> part of this as this
|
||||||
// will be the fileinfo id.
|
// will be the fileinfo id.
|
||||||
parts := strings.Split(filename, ".")
|
fileInfoID := getFileInfoID(strings.Split(filename, ".")[0])
|
||||||
fileInfoID := parts[0][1:]
|
|
||||||
fileInfo, err := a.store.GetFileInfo(fileInfoID)
|
fileInfo, err := a.store.GetFileInfo(fileInfoID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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) {
|
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)
|
fileInfo, err := a.GetFileInfo(fileName)
|
||||||
if err != nil && !model.IsErrNotFound(err) {
|
if err != nil && !model.IsErrNotFound(err) {
|
||||||
a.logger.Error("111")
|
return nil, "", err
|
||||||
return nil, nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var filePath string
|
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)
|
filePath = filepath.Join(teamID, rootID, fileName)
|
||||||
}
|
}
|
||||||
|
|
||||||
exists, err := a.filesBackend.FileExists(filePath)
|
return fileInfo, filePath, nil
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
if !exists {
|
func getDestinationFilePath(isTemplate bool, teamID, boardID, filename string) string {
|
||||||
return nil, nil, ErrFileNotFound
|
// 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)
|
func getFileInfoID(fileName string) string {
|
||||||
if err != nil {
|
// Boards ids are 27 characters long with a prefix character.
|
||||||
a.logger.Error(fmt.Sprintf("GetFile: Failed to get file reader of existing file at path: %s, error: %e", filePath, err))
|
// removing the prefix, returns the 26 character uuid
|
||||||
return nil, nil, err
|
return fileName[1:]
|
||||||
}
|
|
||||||
return fileInfo, reader, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) GetFileReader(teamID, rootID, filename string) (filestore.ReadCloseSeeker, error) {
|
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
|
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
|
||||||
|
}
|
||||||
|
@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/mattermost/mattermost-server/server/v8/boards/model"
|
||||||
mm_model "github.com/mattermost/mattermost-server/server/v8/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"
|
||||||
"github.com/mattermost/mattermost-server/server/v8/platform/shared/filestore/mocks"
|
"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)
|
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.Equal(t, fileName, actual)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
})
|
})
|
||||||
@ -234,7 +235,7 @@ func TestSaveFile(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mockedFileBackend.On("WriteFile", mockedReadCloseSeek, mock.Anything).Return(writeFileFunc, writeFileErrorFunc)
|
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.NoError(t, err)
|
||||||
assert.NotNil(t, actual)
|
assert.NotNil(t, actual)
|
||||||
})
|
})
|
||||||
@ -258,7 +259,7 @@ func TestSaveFile(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mockedFileBackend.On("WriteFile", mockedReadCloseSeek, mock.Anything).Return(writeFileFunc, writeFileErrorFunc)
|
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, "", actual)
|
||||||
assert.Equal(t, "unable to store the file in the files storage: Mocked File backend error", err.Error())
|
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) {
|
func TestGetFile(t *testing.T) {
|
||||||
th, _ := SetupTestHelper(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{
|
th.Store.EXPECT().GetFileInfo("fileInfoID").Return(&mm_model.FileInfo{
|
||||||
Id: "fileInfoID",
|
Id: "fileInfoID",
|
||||||
Path: "/path/to/file/fileName.txt",
|
Path: "/path/to/file/fileName.txt",
|
||||||
@ -337,27 +338,72 @@ func TestGetFile(t *testing.T) {
|
|||||||
assert.NotNil(t, seeker)
|
assert.NotNil(t, seeker)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("when FileInfo doesn't exist", func(t *testing.T) {
|
t.Run("when GetFilePath() throws error", func(t *testing.T) {
|
||||||
th.Store.EXPECT().GetFileInfo("fileInfoID").Return(nil, nil)
|
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{}
|
mockedFileBackend := &mocks.FileBackend{}
|
||||||
th.App.filesBackend = mockedFileBackend
|
th.App.filesBackend = mockedFileBackend
|
||||||
mockedReadCloseSeek := &mocks.ReadCloseSeeker{}
|
mockedFileBackend.On("FileExists", "/path/to/file/fileName.txt").Return(false, nil)
|
||||||
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, 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.NoError(t, err)
|
||||||
assert.Nil(t, fileInfo)
|
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) {
|
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: "",
|
Path: "",
|
||||||
}, nil)
|
}, nil)
|
||||||
|
|
||||||
mockedFileBackend := &mocks.FileBackend{}
|
fileInfo, filePath, err := th.App.GetFilePath("teamID", "boardID", "7fileInfoID.txt")
|
||||||
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")
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.NotNil(t, fileInfo)
|
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"])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -44,27 +44,19 @@ func (a *App) ImportArchive(r io.Reader, opt model.ImportArchiveOptions) error {
|
|||||||
a.logger.Debug("importing legacy archive")
|
a.logger.Debug("importing legacy archive")
|
||||||
_, errImport := a.ImportBoardJSONL(br, opt)
|
_, 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
|
return errImport
|
||||||
}
|
}
|
||||||
|
|
||||||
a.logger.Debug("importing archive")
|
|
||||||
zr := zipstream.NewReader(br)
|
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 {
|
for {
|
||||||
hdr, err := zr.Next()
|
hdr, err := zr.Next()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, io.EOF) {
|
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)))
|
a.logger.Debug("import archive - done", mlog.Int("boards_imported", len(boardMap)))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -84,14 +76,14 @@ func (a *App) ImportArchive(r io.Reader, opt model.ImportArchiveOptions) error {
|
|||||||
return model.NewErrUnsupportedArchiveVersion(ver, archiveVersion)
|
return model.NewErrUnsupportedArchiveVersion(ver, archiveVersion)
|
||||||
}
|
}
|
||||||
case "board.jsonl":
|
case "board.jsonl":
|
||||||
boardID, err := a.ImportBoardJSONL(zr, opt)
|
board, err := a.ImportBoardJSONL(zr, opt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot import board %s: %w", dir, err)
|
return fmt.Errorf("cannot import board %s: %w", dir, err)
|
||||||
}
|
}
|
||||||
boardMap[dir] = boardID
|
boardMap[dir] = board
|
||||||
default:
|
default:
|
||||||
// import file/image; dir is the old board id
|
// import file/image; dir is the old board id
|
||||||
boardID, ok := boardMap[dir]
|
board, ok := boardMap[dir]
|
||||||
if !ok {
|
if !ok {
|
||||||
a.logger.Warn("skipping orphan image in archive",
|
a.logger.Warn("skipping orphan image in archive",
|
||||||
mlog.String("dir", dir),
|
mlog.String("dir", dir),
|
||||||
@ -99,33 +91,63 @@ func (a *App) ImportArchive(r io.Reader, opt model.ImportArchiveOptions) error {
|
|||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// save file with original filename so it matches name in image block.
|
|
||||||
filePath := filepath.Join(opt.TeamID, boardID, filename)
|
newFileName, err := a.SaveFile(zr, opt.TeamID, board.ID, filename, board.IsTemplate)
|
||||||
_, err := a.filesBackend.WriteFile(zr, filePath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot import file %s for board %s: %w", filename, dir, err)
|
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",
|
opts := model.QueryBlocksOptions{
|
||||||
mlog.String("dir", dir),
|
BoardID: board.ID,
|
||||||
mlog.String("filename", filename),
|
}
|
||||||
)
|
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() {
|
for _, block := range newBlocks {
|
||||||
if err := a.UpdateCardLimitTimestamp(); err != nil {
|
if block.Type == "image" || block.Type == "attachment" {
|
||||||
a.logger.Error(
|
fieldName := "fileId"
|
||||||
"UpdateCardLimitTimestamp failed after importing an archive",
|
oldId := block.Fields[fieldName]
|
||||||
mlog.Err(err),
|
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
|
// ImportBoardJSONL imports a JSONL file containing blocks for one board. The resulting
|
||||||
// board id is returned.
|
// 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.
|
// 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.
|
// We don't want to load the whole file in memory, even though it's a single board.
|
||||||
boardsAndBlocks := &model.BoardsAndBlocks{
|
boardsAndBlocks := &model.BoardsAndBlocks{
|
||||||
@ -158,7 +180,7 @@ func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (str
|
|||||||
if !skip {
|
if !skip {
|
||||||
var archiveLine model.ArchiveLine
|
var archiveLine model.ArchiveLine
|
||||||
if err := json.Unmarshal(line, &archiveLine); err != nil {
|
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
|
// first line must be a board
|
||||||
@ -170,7 +192,7 @@ func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (str
|
|||||||
case "board":
|
case "board":
|
||||||
var board model.Board
|
var board model.Board
|
||||||
if err2 := json.Unmarshal(archiveLine.Data, &board); err2 != nil {
|
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.ModifiedBy = userID
|
||||||
board.UpdateAt = now
|
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.
|
// legacy archives encoded boards as blocks; we need to convert them to real boards.
|
||||||
var block *model.Block
|
var block *model.Block
|
||||||
if err2 := json.Unmarshal(archiveLine.Data, &block); err2 != nil {
|
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.ModifiedBy = userID
|
||||||
block.UpdateAt = now
|
block.UpdateAt = now
|
||||||
board, err := a.blockToBoard(block, opt)
|
board, err := a.blockToBoard(block, opt)
|
||||||
if err != nil {
|
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)
|
boardsAndBlocks.Boards = append(boardsAndBlocks.Boards, board)
|
||||||
boardID = board.ID
|
boardID = board.ID
|
||||||
case "block":
|
case "block":
|
||||||
var block *model.Block
|
var block *model.Block
|
||||||
if err2 := json.Unmarshal(archiveLine.Data, &block); err2 != nil {
|
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.ModifiedBy = userID
|
||||||
block.UpdateAt = now
|
block.UpdateAt = now
|
||||||
@ -203,11 +225,11 @@ func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (str
|
|||||||
case "boardMember":
|
case "boardMember":
|
||||||
var boardMember *model.BoardMember
|
var boardMember *model.BoardMember
|
||||||
if err2 := json.Unmarshal(archiveLine.Data, &boardMember); err2 != nil {
|
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)
|
boardMembers = append(boardMembers, boardMember)
|
||||||
default:
|
default:
|
||||||
return "", model.NewErrUnsupportedArchiveLineType(lineNum, archiveLine.Type)
|
return nil, model.NewErrUnsupportedArchiveLineType(lineNum, archiveLine.Type)
|
||||||
}
|
}
|
||||||
firstLine = false
|
firstLine = false
|
||||||
}
|
}
|
||||||
@ -217,7 +239,7 @@ func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (str
|
|||||||
if errors.Is(errRead, io.EOF) {
|
if errors.Is(errRead, io.EOF) {
|
||||||
break
|
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++
|
lineNum++
|
||||||
}
|
}
|
||||||
@ -234,12 +256,12 @@ func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (str
|
|||||||
var err error
|
var err error
|
||||||
boardsAndBlocks, err = model.GenerateBoardsAndBlocksIDs(boardsAndBlocks, a.logger)
|
boardsAndBlocks, err = model.GenerateBoardsAndBlocksIDs(boardsAndBlocks, a.logger)
|
||||||
if err != nil {
|
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)
|
boardsAndBlocks, err = a.CreateBoardsAndBlocks(boardsAndBlocks, opt.ModifiedBy, false)
|
||||||
if err != nil {
|
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).
|
// 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,
|
SchemeAdmin: true,
|
||||||
}
|
}
|
||||||
if _, err2 := a.AddMemberToBoard(adminMember); err2 != nil {
|
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 {
|
for _, boardMember := range boardMembers {
|
||||||
bm := &model.BoardMember{
|
bm := &model.BoardMember{
|
||||||
@ -266,16 +288,16 @@ func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (str
|
|||||||
Synthetic: boardMember.Synthetic,
|
Synthetic: boardMember.Synthetic,
|
||||||
}
|
}
|
||||||
if _, err2 := a.AddMemberToBoard(bm); err2 != nil {
|
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
|
// find new board id
|
||||||
for _, board := range boardsAndBlocks.Boards {
|
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
|
// fixBoardsandBlocks allows the caller of `ImportArchive` to modify or filters boards and blocks being
|
||||||
|
@ -138,9 +138,76 @@ func TestApp_ImportArchive(t *testing.T) {
|
|||||||
th.Store.EXPECT().GetUserByID("hxxzooc3ff8cubsgtcmpn8733e").AnyTimes().Return(user2, nil)
|
th.Store.EXPECT().GetUserByID("hxxzooc3ff8cubsgtcmpn8733e").AnyTimes().Return(user2, nil)
|
||||||
th.Store.EXPECT().GetUserByID("nto73edn5ir6ifimo5a53y1dwa").AnyTimes().Return(user3, nil)
|
th.Store.EXPECT().GetUserByID("nto73edn5ir6ifimo5a53y1dwa").AnyTimes().Return(user3, nil)
|
||||||
|
|
||||||
boardID, err := th.App.ImportBoardJSONL(r, opts)
|
newBoard, err := th.App.ImportBoardJSONL(r, opts)
|
||||||
require.Equal(t, board.ID, boardID, "Board ID should be same")
|
|
||||||
require.NoError(t, err, "import archive should not fail")
|
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")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,6 +53,7 @@ func TestApp_initializeTemplates(t *testing.T) {
|
|||||||
th.Store.EXPECT().GetMembersForBoard(board.ID).AnyTimes().Return([]*model.BoardMember{}, nil)
|
th.Store.EXPECT().GetMembersForBoard(board.ID).AnyTimes().Return([]*model.BoardMember{}, nil)
|
||||||
th.Store.EXPECT().GetBoard(board.ID).AnyTimes().Return(board, 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().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)
|
th.FilesBackend.On("WriteFile", mock.Anything, mock.Anything).Return(int64(1), nil)
|
||||||
|
|
||||||
|
@ -3379,7 +3379,7 @@ func TestPermissionsGetFile(t *testing.T) {
|
|||||||
clients := setupClients(th)
|
clients := setupClients(th)
|
||||||
testData := setupData(t, 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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
ttCases := ttCasesF()
|
ttCases := ttCasesF()
|
||||||
@ -3394,7 +3394,7 @@ func TestPermissionsGetFile(t *testing.T) {
|
|||||||
clients := setupLocalClients(th)
|
clients := setupLocalClients(th)
|
||||||
testData := setupData(t, 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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
ttCases := ttCasesF()
|
ttCases := ttCasesF()
|
||||||
|
27
server/boards/model/file.go
Normal file
27
server/boards/model/file.go
Normal file
@ -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),
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -3,7 +3,7 @@
|
|||||||
import {Block, createBlock} from './block'
|
import {Block, createBlock} from './block'
|
||||||
|
|
||||||
type AttachmentBlockFields = {
|
type AttachmentBlockFields = {
|
||||||
attachmentId: string
|
fileId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type AttachmentBlock = Block & {
|
type AttachmentBlock = Block & {
|
||||||
@ -18,7 +18,7 @@ function createAttachmentBlock(block?: Block): AttachmentBlock {
|
|||||||
...createBlock(block),
|
...createBlock(block),
|
||||||
type: 'attachment',
|
type: 'attachment',
|
||||||
fields: {
|
fields: {
|
||||||
attachmentId: block?.fields.attachmentId || '',
|
fileId: block?.fields.attachmentId || block?.fields.fileId || '',
|
||||||
},
|
},
|
||||||
isUploading: false,
|
isUploading: false,
|
||||||
uploadingPercent: 0,
|
uploadingPercent: 0,
|
||||||
|
@ -151,7 +151,7 @@ const CardDialog = (props: Props): JSX.Element => {
|
|||||||
Utils.selectLocalFile(async (attachment) => {
|
Utils.selectLocalFile(async (attachment) => {
|
||||||
const uploadingBlock = createBlock()
|
const uploadingBlock = createBlock()
|
||||||
uploadingBlock.title = attachment.name
|
uploadingBlock.title = attachment.name
|
||||||
uploadingBlock.fields.attachmentId = attachment.name
|
uploadingBlock.fields.fileId = attachment.name
|
||||||
uploadingBlock.boardId = boardId
|
uploadingBlock.boardId = boardId
|
||||||
if (card) {
|
if (card) {
|
||||||
uploadingBlock.parentId = card.id
|
uploadingBlock.parentId = card.id
|
||||||
@ -177,11 +177,11 @@ const CardDialog = (props: Props): JSX.Element => {
|
|||||||
xhr.onload = () => {
|
xhr.onload = () => {
|
||||||
if (xhr.status === 200 && xhr.readyState === 4) {
|
if (xhr.status === 200 && xhr.readyState === 4) {
|
||||||
const json = JSON.parse(xhr.response)
|
const json = JSON.parse(xhr.response)
|
||||||
const attachmentId = json.fileId
|
const fileId = json.fileId
|
||||||
if (attachmentId) {
|
if (fileId) {
|
||||||
removeUploadingAttachment(uploadingBlock)
|
removeUploadingAttachment(uploadingBlock)
|
||||||
const block = createAttachmentBlock()
|
const block = createAttachmentBlock()
|
||||||
block.fields.attachmentId = attachmentId || ''
|
block.fields.fileId = fileId || ''
|
||||||
block.title = attachment.name
|
block.title = attachment.name
|
||||||
sendFlashMessage({content: intl.formatMessage({id: 'AttachmentBlock.uploadSuccess', defaultMessage: 'Attachment uploaded.'}), severity: 'normal'})
|
sendFlashMessage({content: intl.formatMessage({id: 'AttachmentBlock.uploadSuccess', defaultMessage: 'Attachment uploaded.'}), severity: 'normal'})
|
||||||
resolve(block)
|
resolve(block)
|
||||||
|
@ -39,7 +39,7 @@ describe('component/content/FileBlock', () => {
|
|||||||
type: 'attachment',
|
type: 'attachment',
|
||||||
title: 'test-title',
|
title: 'test-title',
|
||||||
fields: {
|
fields: {
|
||||||
attachmentId: 'test.txt',
|
fileId: 'test.txt',
|
||||||
},
|
},
|
||||||
createdBy: 'test-user-id',
|
createdBy: 'test-user-id',
|
||||||
createAt: 0,
|
createAt: 0,
|
||||||
|
@ -50,7 +50,7 @@ const AttachmentElement = (props: Props): JSX.Element|null => {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const attachmentInfo = await octoClient.getFileInfo(block.boardId, block.fields.attachmentId)
|
const attachmentInfo = await octoClient.getFileInfo(block.boardId, block.fields.fileId)
|
||||||
setFileInfo(attachmentInfo)
|
setFileInfo(attachmentInfo)
|
||||||
}
|
}
|
||||||
loadFile()
|
loadFile()
|
||||||
@ -113,7 +113,7 @@ const AttachmentElement = (props: Props): JSX.Element|null => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const attachmentDownloadHandler = async () => {
|
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')
|
const anchor = document.createElement('a')
|
||||||
anchor.href = attachment.url || ''
|
anchor.href = attachment.url || ''
|
||||||
anchor.download = fileInfo.name || ''
|
anchor.download = fileInfo.name || ''
|
||||||
|
Loading…
Reference in New Issue
Block a user