301 lines
9.1 KiB
Go
301 lines
9.1 KiB
Go
package app
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/mattermost/focalboard/server/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, boardID, filename string, asTemplate bool) (string, error) {
|
|
// NOTE: File extension includes the dot
|
|
fileExtension := strings.ToLower(filepath.Ext(filename))
|
|
if fileExtension == ".jpeg" {
|
|
fileExtension = ".jpg"
|
|
}
|
|
|
|
createdFilename := utils.NewID(utils.IDTypeNone)
|
|
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)
|
|
}
|
|
|
|
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 newFileName, nil
|
|
}
|
|
|
|
func (a *App) GetFileInfo(filename string) (*mm_model.FileInfo, error) {
|
|
if len(filename) == 0 {
|
|
return nil, errEmptyFilename
|
|
}
|
|
|
|
// 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.
|
|
fileInfoID := getFileInfoID(strings.Split(filename, ".")[0])
|
|
|
|
fileInfo, err := a.store.GetFileInfo(fileInfoID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return fileInfo, nil
|
|
}
|
|
|
|
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) {
|
|
return nil, "", err
|
|
}
|
|
|
|
var filePath string
|
|
|
|
if fileInfo != nil && fileInfo.Path != "" && fileInfo.Path != emptyString {
|
|
filePath = fileInfo.Path
|
|
} else {
|
|
filePath = filepath.Join(teamID, rootID, fileName)
|
|
}
|
|
|
|
return fileInfo, filePath, nil
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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) {
|
|
filePath := filepath.Join(teamID, rootID, filename)
|
|
exists, err := a.filesBackend.FileExists(filePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// FIXUP: Check the deprecated old location
|
|
if teamID == "0" && !exists {
|
|
oldExists, err2 := a.filesBackend.FileExists(filename)
|
|
if err2 != nil {
|
|
return nil, err2
|
|
}
|
|
if oldExists {
|
|
err2 := a.filesBackend.MoveFile(filename, filePath)
|
|
if err2 != nil {
|
|
a.logger.Error("ERROR moving file",
|
|
mlog.String("old", filename),
|
|
mlog.String("new", filePath),
|
|
mlog.Err(err2),
|
|
)
|
|
} else {
|
|
a.logger.Debug("Moved file",
|
|
mlog.String("old", filename),
|
|
mlog.String("new", filePath),
|
|
)
|
|
}
|
|
}
|
|
} else if !exists {
|
|
return nil, ErrFileNotFound
|
|
}
|
|
|
|
reader, err := a.filesBackend.Reader(filePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return reader, nil
|
|
}
|
|
|
|
func (a *App) MoveFile(channelID, teamID, boardID, filename string) error {
|
|
oldPath := filepath.Join(channelID, boardID, filename)
|
|
newPath := filepath.Join(teamID, boardID, filename)
|
|
err := a.filesBackend.MoveFile(oldPath, newPath)
|
|
if err != nil {
|
|
a.logger.Error("ERROR moving file",
|
|
mlog.String("old", oldPath),
|
|
mlog.String("new", newPath),
|
|
mlog.Err(err),
|
|
)
|
|
return err
|
|
}
|
|
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
|
|
}
|