You've already forked focalboard
							
							
				mirror of
				https://github.com/mattermost/focalboard.git
				synced 2025-10-31 00:17:42 +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:
		| @@ -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,11 +238,14 @@ 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 { | ||||
| 		f, ok = block.Fields["attachmentId"] | ||||
| 		if !ok { | ||||
| 			return "", model.ErrInvalidImageBlock | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	filename, ok := f.(string) | ||||
| 	if !ok { | ||||
|   | ||||
| @@ -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.Trace("import archive file", | ||||
| 			mlog.String("dir", dir), | ||||
| 			a.logger.Debug("import archive file", | ||||
| 				mlog.String("TeamID", opt.TeamID), | ||||
| 				mlog.String("boardID", board.ID), | ||||
| 				mlog.String("filename", filename), | ||||
| 		) | ||||
|  | ||||
| 		go func() { | ||||
| 			if err := a.UpdateCardLimitTimestamp(); err != nil { | ||||
| 				a.logger.Error( | ||||
| 					"UpdateCardLimitTimestamp failed after importing an archive", | ||||
| 					mlog.Err(err), | ||||
| 				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 | ||||
| 		} | ||||
|  | ||||
| 		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 | ||||
| 		} | ||||
|  | ||||
| 		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 || '' | ||||
|   | ||||
		Reference in New Issue
	
	Block a user