2020-10-21 11:32:13 +02:00
package app
import (
2022-06-13 10:05:42 +02:00
"errors"
2020-10-21 11:32:13 +02:00
"fmt"
"io"
"path/filepath"
"strings"
2020-11-09 14:19:03 +02:00
2023-03-07 06:51:53 +02:00
"github.com/mattermost/focalboard/server/model"
2023-05-22 18:31:24 +02:00
mm_model "github.com/mattermost/mattermost-server/v6/model"
2021-08-25 22:08:01 +02:00
2022-06-13 10:05:42 +02:00
"github.com/mattermost/focalboard/server/utils"
2021-08-24 12:13:58 +02:00
"github.com/mattermost/mattermost-server/v6/shared/filestore"
2022-06-13 10:05:42 +02:00
"github.com/mattermost/mattermost-server/v6/shared/mlog"
2020-10-21 11:32:13 +02:00
)
2022-06-13 10:05:42 +02:00
var errEmptyFilename = errors . New ( "IsFileArchived: empty filename not allowed" )
2022-10-05 22:16:03 +02:00
var ErrFileNotFound = errors . New ( "file not found" )
2022-06-13 10:05:42 +02:00
2023-05-22 18:31:24 +02:00
func ( a * App ) SaveFile ( reader io . Reader , teamID , boardID , filename string , asTemplate bool ) ( string , error ) {
2020-10-21 11:32:13 +02:00
// NOTE: File extension includes the dot
fileExtension := strings . ToLower ( filepath . Ext ( filename ) )
if fileExtension == ".jpeg" {
fileExtension = ".jpg"
}
2022-06-13 10:05:42 +02:00
createdFilename := utils . NewID ( utils . IDTypeNone )
2023-05-22 18:31:24 +02:00
newFileName := fmt . Sprintf ( ` %s%s ` , createdFilename , fileExtension )
if asTemplate {
newFileName = filename
}
filePath := getDestinationFilePath ( asTemplate , teamID , boardID , newFileName )
2020-10-21 11:32:13 +02:00
2022-06-13 10:05:42 +02:00
fileSize , appErr := a . filesBackend . WriteFile ( reader , filePath )
2020-10-21 11:32:13 +02:00
if appErr != nil {
2021-07-09 03:09:02 +02:00
return "" , fmt . Errorf ( "unable to store the file in the files storage: %w" , appErr )
2020-10-21 11:32:13 +02:00
}
2020-10-22 15:22:36 +02:00
2023-05-22 18:31:24 +02:00
fileInfo := model . NewFileInfo ( filename )
fileInfo . Id = getFileInfoID ( createdFilename )
fileInfo . Path = filePath
fileInfo . Size = fileSize
2023-03-07 06:51:53 +02:00
2022-06-13 10:05:42 +02:00
err := a . store . SaveFileInfo ( fileInfo )
if err != nil {
return "" , err
}
2023-05-22 18:31:24 +02:00
return newFileName , nil
2022-06-13 10:05:42 +02:00
}
2023-05-22 18:31:24 +02:00
func ( a * App ) GetFileInfo ( filename string ) ( * mm_model . FileInfo , error ) {
2022-06-13 10:05:42 +02:00
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.
2023-05-22 18:31:24 +02:00
fileInfoID := getFileInfoID ( strings . Split ( filename , "." ) [ 0 ] )
2023-03-07 06:51:53 +02:00
2022-06-13 10:05:42 +02:00
fileInfo , err := a . store . GetFileInfo ( fileInfoID )
if err != nil {
return nil , err
}
return fileInfo , nil
2020-10-21 11:32:13 +02:00
}
2023-05-22 18:31:24 +02:00
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 ) )
2023-03-07 06:51:53 +02:00
return nil , nil , err
}
exists , err := a . filesBackend . FileExists ( filePath )
if err != nil {
2023-05-22 18:31:24 +02:00
a . logger . Error ( "GetFile: Failed to check if file exists as path. " , mlog . String ( "Path" , filePath ) , mlog . Err ( err ) )
2023-03-07 06:51:53 +02:00
return nil , nil , err
}
if ! exists {
return nil , nil , ErrFileNotFound
}
reader , err := a . filesBackend . Reader ( filePath )
if err != nil {
2023-05-22 18:31:24 +02:00
a . logger . Error ( "GetFile: Failed to get file reader of existing file at path" , mlog . String ( "Path" , filePath ) , mlog . Err ( err ) )
2023-03-07 06:51:53 +02:00
return nil , nil , err
}
return fileInfo , reader , nil
}
2023-05-22 18:31:24 +02:00
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 != "" {
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 : ]
}
2022-03-22 16:24:34 +02:00
func ( a * App ) GetFileReader ( teamID , rootID , filename string ) ( filestore . ReadCloseSeeker , error ) {
filePath := filepath . Join ( teamID , rootID , filename )
2021-05-24 19:06:11 +02:00
exists , err := a . filesBackend . FileExists ( filePath )
if err != nil {
return nil , err
}
2021-04-01 00:30:25 +02:00
// FIXUP: Check the deprecated old location
2022-03-22 16:24:34 +02:00
if teamID == "0" && ! exists {
2021-06-21 11:21:42 +02:00
oldExists , err2 := a . filesBackend . FileExists ( filename )
if err2 != nil {
return nil , err2
2021-05-24 19:06:11 +02:00
}
if oldExists {
2021-06-21 11:21:42 +02:00
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 ) ,
)
2021-04-01 00:30:25 +02:00
} else {
2021-06-21 11:21:42 +02:00
a . logger . Debug ( "Moved file" ,
mlog . String ( "old" , filename ) ,
mlog . String ( "new" , filePath ) ,
)
2021-04-01 00:30:25 +02:00
}
}
2022-10-05 22:16:03 +02:00
} else if ! exists {
return nil , ErrFileNotFound
2021-04-01 00:30:25 +02:00
}
2021-05-24 19:06:11 +02:00
reader , err := a . filesBackend . Reader ( filePath )
if err != nil {
return nil , err
}
return reader , nil
2021-04-01 00:30:25 +02:00
}
2022-10-05 22:16:03 +02:00
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
}
2023-05-22 18:31:24 +02:00
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
}