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. // we want to extract the 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 }