You've already forked focalboard
							
							
				mirror of
				https://github.com/mattermost/focalboard.git
				synced 2025-10-31 00:17:42 +02:00 
			
		
		
		
	Forward-porting fileinfo limits (#3164)
* Updated go version * Generated mocks * lint fix * lint fix * lint fix * backported fileinfo limits * backported fileinfo limits * added tests * synced with main * Server lint fix * used a better name
This commit is contained in:
		| @@ -1913,6 +1913,31 @@ func (a *API) handleServeFile(w http.ResponseWriter, r *http.Request) { | |||||||
|  |  | ||||||
| 	w.Header().Set("Content-Type", contentType) | 	w.Header().Set("Content-Type", contentType) | ||||||
|  |  | ||||||
|  | 	fileInfo, err := a.app.GetFileInfo(filename) | ||||||
|  | 	if err != nil { | ||||||
|  | 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if fileInfo != nil && fileInfo.Archived { | ||||||
|  | 		fileMetadata := map[string]interface{}{ | ||||||
|  | 			"archived":  true, | ||||||
|  | 			"name":      fileInfo.Name, | ||||||
|  | 			"size":      fileInfo.Size, | ||||||
|  | 			"extension": fileInfo.Extension, | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		data, jsonErr := json.Marshal(fileMetadata) | ||||||
|  | 		if jsonErr != nil { | ||||||
|  | 			a.logger.Error("failed to marshal archived file metadata", mlog.String("filename", filename), mlog.Err(jsonErr)) | ||||||
|  | 			a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", jsonErr) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		jsonBytesResponse(w, http.StatusBadRequest, data) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	fileReader, err := a.app.GetFileReader(board.TeamID, boardID, filename) | 	fileReader, err := a.app.GetFileReader(board.TeamID, boardID, filename) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||||
|   | |||||||
| @@ -1,18 +1,23 @@ | |||||||
| package app | package app | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io" | 	"io" | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
|  | 	mmModel "github.com/mattermost/mattermost-server/v6/model" | ||||||
|  |  | ||||||
| 	"github.com/mattermost/focalboard/server/utils" | 	"github.com/mattermost/focalboard/server/utils" | ||||||
|  |  | ||||||
| 	"github.com/mattermost/mattermost-server/v6/shared/mlog" |  | ||||||
|  |  | ||||||
| 	"github.com/mattermost/mattermost-server/v6/shared/filestore" | 	"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") | ||||||
|  |  | ||||||
| func (a *App) SaveFile(reader io.Reader, teamID, rootID, filename string) (string, error) { | func (a *App) SaveFile(reader io.Reader, teamID, rootID, filename string) (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)) | ||||||
| @@ -20,15 +25,63 @@ func (a *App) SaveFile(reader io.Reader, teamID, rootID, filename string) (strin | |||||||
| 		fileExtension = ".jpg" | 		fileExtension = ".jpg" | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	createdFilename := fmt.Sprintf(`%s%s`, utils.NewID(utils.IDTypeNone), fileExtension) | 	createdFilename := utils.NewID(utils.IDTypeNone) | ||||||
| 	filePath := filepath.Join(teamID, rootID, createdFilename) | 	fullFilename := fmt.Sprintf(`%s%s`, createdFilename, fileExtension) | ||||||
|  | 	filePath := filepath.Join(teamID, rootID, fullFilename) | ||||||
|  |  | ||||||
| 	_, 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) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return createdFilename, nil | 	now := utils.GetMillis() | ||||||
|  |  | ||||||
|  | 	fileInfo := &mmModel.FileInfo{ | ||||||
|  | 		Id:              createdFilename[1:], | ||||||
|  | 		CreatorId:       "boards", | ||||||
|  | 		PostId:          emptyString, | ||||||
|  | 		ChannelId:       emptyString, | ||||||
|  | 		CreateAt:        now, | ||||||
|  | 		UpdateAt:        now, | ||||||
|  | 		DeleteAt:        0, | ||||||
|  | 		Path:            emptyString, | ||||||
|  | 		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) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return fullFilename, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *App) GetFileInfo(filename string) (*mmModel.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. | ||||||
|  | 	parts := strings.Split(filename, ".") | ||||||
|  | 	fileInfoID := parts[0][1:] | ||||||
|  | 	fileInfo, err := a.store.GetFileInfo(fileInfoID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return fileInfo, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (a *App) GetFileReader(teamID, rootID, filename string) (filestore.ReadCloseSeeker, error) { | func (a *App) GetFileReader(teamID, rootID, filename string) (filestore.ReadCloseSeeker, error) { | ||||||
|   | |||||||
| @@ -1,14 +1,17 @@ | |||||||
| package app | package app | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"errors" | ||||||
| 	"io" | 	"io" | ||||||
| 	"os" | 	"os" | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
|  | 	"github.com/golang/mock/gomock" | ||||||
| 	"github.com/stretchr/testify/assert" | 	"github.com/stretchr/testify/assert" | ||||||
|  |  | ||||||
|  | 	mmModel "github.com/mattermost/mattermost-server/v6/model" | ||||||
| 	"github.com/mattermost/mattermost-server/v6/plugin/plugintest/mock" | 	"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" | ||||||
| 	"github.com/mattermost/mattermost-server/v6/shared/filestore/mocks" | 	"github.com/mattermost/mattermost-server/v6/shared/filestore/mocks" | ||||||
| @@ -19,6 +22,8 @@ const ( | |||||||
| 	testBoardID  = "test-board-id" | 	testBoardID  = "test-board-id" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | var errDummy = errors.New("hello") | ||||||
|  |  | ||||||
| type TestError struct{} | type TestError struct{} | ||||||
|  |  | ||||||
| func (err *TestError) Error() string { return "Mocked File backend error" } | func (err *TestError) Error() string { return "Mocked File backend error" } | ||||||
| @@ -186,6 +191,7 @@ func TestSaveFile(t *testing.T) { | |||||||
| 		fileName := "temp-file-name.txt" | 		fileName := "temp-file-name.txt" | ||||||
| 		mockedFileBackend := &mocks.FileBackend{} | 		mockedFileBackend := &mocks.FileBackend{} | ||||||
| 		th.App.filesBackend = mockedFileBackend | 		th.App.filesBackend = mockedFileBackend | ||||||
|  | 		th.Store.EXPECT().SaveFileInfo(gomock.Any()).Return(nil) | ||||||
|  |  | ||||||
| 		writeFileFunc := func(reader io.Reader, path string) int64 { | 		writeFileFunc := func(reader io.Reader, path string) int64 { | ||||||
| 			paths := strings.Split(path, string(os.PathSeparator)) | 			paths := strings.Split(path, string(os.PathSeparator)) | ||||||
| @@ -209,6 +215,7 @@ func TestSaveFile(t *testing.T) { | |||||||
| 		fileName := "temp-file-name.jpeg" | 		fileName := "temp-file-name.jpeg" | ||||||
| 		mockedFileBackend := &mocks.FileBackend{} | 		mockedFileBackend := &mocks.FileBackend{} | ||||||
| 		th.App.filesBackend = mockedFileBackend | 		th.App.filesBackend = mockedFileBackend | ||||||
|  | 		th.Store.EXPECT().SaveFileInfo(gomock.Any()).Return(nil) | ||||||
|  |  | ||||||
| 		writeFileFunc := func(reader io.Reader, path string) int64 { | 		writeFileFunc := func(reader io.Reader, path string) int64 { | ||||||
| 			paths := strings.Split(path, string(os.PathSeparator)) | 			paths := strings.Split(path, string(os.PathSeparator)) | ||||||
| @@ -233,6 +240,7 @@ func TestSaveFile(t *testing.T) { | |||||||
| 		mockedFileBackend := &mocks.FileBackend{} | 		mockedFileBackend := &mocks.FileBackend{} | ||||||
| 		th.App.filesBackend = mockedFileBackend | 		th.App.filesBackend = mockedFileBackend | ||||||
| 		mockedError := &TestError{} | 		mockedError := &TestError{} | ||||||
|  | 		th.Store.EXPECT().SaveFileInfo(gomock.Any()).Return(nil) | ||||||
|  |  | ||||||
| 		writeFileFunc := func(reader io.Reader, path string) int64 { | 		writeFileFunc := func(reader io.Reader, path string) int64 { | ||||||
| 			paths := strings.Split(path, string(os.PathSeparator)) | 			paths := strings.Split(path, string(os.PathSeparator)) | ||||||
| @@ -252,3 +260,48 @@ func TestSaveFile(t *testing.T) { | |||||||
| 		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()) | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func TestGetFileInfo(t *testing.T) { | ||||||
|  | 	th, _ := SetupTestHelper(t) | ||||||
|  |  | ||||||
|  | 	t.Run("should return file info", func(t *testing.T) { | ||||||
|  | 		fileInfo := &mmModel.FileInfo{ | ||||||
|  | 			Id:       "file_info_id", | ||||||
|  | 			Archived: false, | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		th.Store.EXPECT().GetFileInfo("filename").Return(fileInfo, nil).Times(2) | ||||||
|  |  | ||||||
|  | 		fetchedFileInfo, err := th.App.GetFileInfo("Afilename") | ||||||
|  | 		assert.NoError(t, err) | ||||||
|  | 		assert.Equal(t, "file_info_id", fetchedFileInfo.Id) | ||||||
|  | 		assert.False(t, fetchedFileInfo.Archived) | ||||||
|  |  | ||||||
|  | 		fetchedFileInfo, err = th.App.GetFileInfo("Afilename.txt") | ||||||
|  | 		assert.NoError(t, err) | ||||||
|  | 		assert.Equal(t, "file_info_id", fetchedFileInfo.Id) | ||||||
|  | 		assert.False(t, fetchedFileInfo.Archived) | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	t.Run("should return archived file info", func(t *testing.T) { | ||||||
|  | 		fileInfo := &mmModel.FileInfo{ | ||||||
|  | 			Id:       "file_info_id", | ||||||
|  | 			Archived: true, | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		th.Store.EXPECT().GetFileInfo("filename").Return(fileInfo, nil) | ||||||
|  |  | ||||||
|  | 		fetchedFileInfo, err := th.App.GetFileInfo("Afilename") | ||||||
|  | 		assert.NoError(t, err) | ||||||
|  | 		assert.Equal(t, "file_info_id", fetchedFileInfo.Id) | ||||||
|  | 		assert.True(t, fetchedFileInfo.Archived) | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	t.Run("should return archived file infoerror", func(t *testing.T) { | ||||||
|  | 		th.Store.EXPECT().GetFileInfo("filename").Return(nil, errDummy) | ||||||
|  |  | ||||||
|  | 		fetchedFileInfo, err := th.App.GetFileInfo("Afilename") | ||||||
|  | 		assert.Error(t, err) | ||||||
|  | 		assert.Nil(t, fetchedFileInfo) | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ package mattermostauthlayer | |||||||
| import ( | import ( | ||||||
| 	"database/sql" | 	"database/sql" | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
|  | 	"net/http" | ||||||
|  |  | ||||||
| 	mmModel "github.com/mattermost/mattermost-server/v6/model" | 	mmModel "github.com/mattermost/mattermost-server/v6/model" | ||||||
| 	"github.com/mattermost/mattermost-server/v6/plugin" | 	"github.com/mattermost/mattermost-server/v6/plugin" | ||||||
| @@ -383,6 +384,84 @@ func mmUserToFbUser(mmUser *mmModel.User) model.User { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (s *MattermostAuthLayer) GetFileInfo(id string) (*mmModel.FileInfo, error) { | ||||||
|  | 	fileInfo, appErr := s.pluginAPI.GetFileInfo(id) | ||||||
|  | 	if appErr != nil { | ||||||
|  | 		// Not finding fileinfo is fine because we don't have data for | ||||||
|  | 		// any existing files already uploaded in Boards before this code | ||||||
|  | 		// was deployed. | ||||||
|  | 		if appErr.StatusCode == http.StatusNotFound { | ||||||
|  | 			return nil, nil | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		s.logger.Error("error fetching fileinfo", mlog.String("id", id), mlog.Err(appErr)) | ||||||
|  | 		return nil, appErr | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return fileInfo, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (s *MattermostAuthLayer) SaveFileInfo(fileInfo *mmModel.FileInfo) error { | ||||||
|  | 	query := s.getQueryBuilder(). | ||||||
|  | 		Insert("FileInfo"). | ||||||
|  | 		Columns( | ||||||
|  | 			"Id", | ||||||
|  | 			"CreatorId", | ||||||
|  | 			"PostId", | ||||||
|  | 			"CreateAt", | ||||||
|  | 			"UpdateAt", | ||||||
|  | 			"DeleteAt", | ||||||
|  | 			"Path", | ||||||
|  | 			"ThumbnailPath", | ||||||
|  | 			"PreviewPath", | ||||||
|  | 			"Name", | ||||||
|  | 			"Extension", | ||||||
|  | 			"Size", | ||||||
|  | 			"MimeType", | ||||||
|  | 			"Width", | ||||||
|  | 			"Height", | ||||||
|  | 			"HasPreviewImage", | ||||||
|  | 			"MiniPreview", | ||||||
|  | 			"Content", | ||||||
|  | 			"RemoteId", | ||||||
|  | 			"Archived", | ||||||
|  | 		). | ||||||
|  | 		Values( | ||||||
|  | 			fileInfo.Id, | ||||||
|  | 			fileInfo.CreatorId, | ||||||
|  | 			fileInfo.PostId, | ||||||
|  | 			fileInfo.CreateAt, | ||||||
|  | 			fileInfo.UpdateAt, | ||||||
|  | 			fileInfo.DeleteAt, | ||||||
|  | 			fileInfo.Path, | ||||||
|  | 			fileInfo.ThumbnailPath, | ||||||
|  | 			fileInfo.PreviewPath, | ||||||
|  | 			fileInfo.Name, | ||||||
|  | 			fileInfo.Extension, | ||||||
|  | 			fileInfo.Size, | ||||||
|  | 			fileInfo.MimeType, | ||||||
|  | 			fileInfo.Width, | ||||||
|  | 			fileInfo.Height, | ||||||
|  | 			fileInfo.HasPreviewImage, | ||||||
|  | 			fileInfo.MiniPreview, | ||||||
|  | 			fileInfo.Content, | ||||||
|  | 			fileInfo.RemoteId, | ||||||
|  | 			false, | ||||||
|  | 		) | ||||||
|  |  | ||||||
|  | 	if _, err := query.Exec(); err != nil { | ||||||
|  | 		s.logger.Error( | ||||||
|  | 			"failed to save fileinfo", | ||||||
|  | 			mlog.String("file_name", fileInfo.Name), | ||||||
|  | 			mlog.Int64("size", fileInfo.Size), | ||||||
|  | 			mlog.Err(err), | ||||||
|  | 		) | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
| func (s *MattermostAuthLayer) GetLicense() *mmModel.License { | func (s *MattermostAuthLayer) GetLicense() *mmModel.License { | ||||||
| 	return s.pluginAPI.GetLicense() | 	return s.pluginAPI.GetLicense() | ||||||
| } | } | ||||||
|   | |||||||
| @@ -581,6 +581,21 @@ func (mr *MockStoreMockRecorder) GetCategory(arg0 interface{}) *gomock.Call { | |||||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCategory", reflect.TypeOf((*MockStore)(nil).GetCategory), arg0) | 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCategory", reflect.TypeOf((*MockStore)(nil).GetCategory), arg0) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // GetFileInfo mocks base method. | ||||||
|  | func (m *MockStore) GetFileInfo(arg0 string) (*model0.FileInfo, error) { | ||||||
|  | 	m.ctrl.T.Helper() | ||||||
|  | 	ret := m.ctrl.Call(m, "GetFileInfo", arg0) | ||||||
|  | 	ret0, _ := ret[0].(*model0.FileInfo) | ||||||
|  | 	ret1, _ := ret[1].(error) | ||||||
|  | 	return ret0, ret1 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GetFileInfo indicates an expected call of GetFileInfo. | ||||||
|  | func (mr *MockStoreMockRecorder) GetFileInfo(arg0 interface{}) *gomock.Call { | ||||||
|  | 	mr.mock.ctrl.T.Helper() | ||||||
|  | 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFileInfo", reflect.TypeOf((*MockStore)(nil).GetFileInfo), arg0) | ||||||
|  | } | ||||||
|  |  | ||||||
| // GetLicense mocks base method. | // GetLicense mocks base method. | ||||||
| func (m *MockStore) GetLicense() *model0.License { | func (m *MockStore) GetLicense() *model0.License { | ||||||
| 	m.ctrl.T.Helper() | 	m.ctrl.T.Helper() | ||||||
| @@ -1129,6 +1144,20 @@ func (mr *MockStoreMockRecorder) RunDataRetention(arg0, arg1 interface{}) *gomoc | |||||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RunDataRetention", reflect.TypeOf((*MockStore)(nil).RunDataRetention), arg0, arg1) | 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RunDataRetention", reflect.TypeOf((*MockStore)(nil).RunDataRetention), arg0, arg1) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // SaveFileInfo mocks base method. | ||||||
|  | func (m *MockStore) SaveFileInfo(arg0 *model0.FileInfo) error { | ||||||
|  | 	m.ctrl.T.Helper() | ||||||
|  | 	ret := m.ctrl.Call(m, "SaveFileInfo", arg0) | ||||||
|  | 	ret0, _ := ret[0].(error) | ||||||
|  | 	return ret0 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // SaveFileInfo indicates an expected call of SaveFileInfo. | ||||||
|  | func (mr *MockStoreMockRecorder) SaveFileInfo(arg0 interface{}) *gomock.Call { | ||||||
|  | 	mr.mock.ctrl.T.Helper() | ||||||
|  | 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveFileInfo", reflect.TypeOf((*MockStore)(nil).SaveFileInfo), arg0) | ||||||
|  | } | ||||||
|  |  | ||||||
| // SaveMember mocks base method. | // SaveMember mocks base method. | ||||||
| func (m *MockStore) SaveMember(arg0 *model.BoardMember) (*model.BoardMember, error) { | func (m *MockStore) SaveMember(arg0 *model.BoardMember) (*model.BoardMember, error) { | ||||||
| 	m.ctrl.T.Helper() | 	m.ctrl.T.Helper() | ||||||
|   | |||||||
							
								
								
									
										85
									
								
								server/services/store/sqlstore/file.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								server/services/store/sqlstore/file.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,85 @@ | |||||||
|  | package sqlstore | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"database/sql" | ||||||
|  | 	"errors" | ||||||
|  |  | ||||||
|  | 	sq "github.com/Masterminds/squirrel" | ||||||
|  | 	"github.com/mattermost/mattermost-server/v6/model" | ||||||
|  | 	"github.com/mattermost/mattermost-server/v6/shared/mlog" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func (s *SQLStore) saveFileInfo(db sq.BaseRunner, fileInfo *model.FileInfo) error { | ||||||
|  | 	query := s.getQueryBuilder(db). | ||||||
|  | 		Insert(s.tablePrefix+"file_info"). | ||||||
|  | 		Columns( | ||||||
|  | 			"id", | ||||||
|  | 			"create_at", | ||||||
|  | 			"name", | ||||||
|  | 			"extension", | ||||||
|  | 			"size", | ||||||
|  | 			"delete_at", | ||||||
|  | 			"archived", | ||||||
|  | 		). | ||||||
|  | 		Values( | ||||||
|  | 			fileInfo.Id, | ||||||
|  | 			fileInfo.CreateAt, | ||||||
|  | 			fileInfo.Name, | ||||||
|  | 			fileInfo.Extension, | ||||||
|  | 			fileInfo.Size, | ||||||
|  | 			fileInfo.DeleteAt, | ||||||
|  | 			false, | ||||||
|  | 		) | ||||||
|  |  | ||||||
|  | 	if _, err := query.Exec(); err != nil { | ||||||
|  | 		s.logger.Error( | ||||||
|  | 			"failed to save fileinfo", | ||||||
|  | 			mlog.String("file_name", fileInfo.Name), | ||||||
|  | 			mlog.Int64("size", fileInfo.Size), | ||||||
|  | 			mlog.Err(err), | ||||||
|  | 		) | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (s *SQLStore) getFileInfo(db sq.BaseRunner, id string) (*model.FileInfo, error) { | ||||||
|  | 	query := s.getQueryBuilder(db). | ||||||
|  | 		Select( | ||||||
|  | 			"id", | ||||||
|  | 			"create_at", | ||||||
|  | 			"delete_at", | ||||||
|  | 			"name", | ||||||
|  | 			"extension", | ||||||
|  | 			"size", | ||||||
|  | 			"archived", | ||||||
|  | 		). | ||||||
|  | 		From(s.tablePrefix + "file_info"). | ||||||
|  | 		Where(sq.Eq{"Id": id}) | ||||||
|  |  | ||||||
|  | 	row := query.QueryRow() | ||||||
|  |  | ||||||
|  | 	fileInfo := model.FileInfo{} | ||||||
|  |  | ||||||
|  | 	err := row.Scan( | ||||||
|  | 		&fileInfo.Id, | ||||||
|  | 		&fileInfo.CreateAt, | ||||||
|  | 		&fileInfo.DeleteAt, | ||||||
|  | 		&fileInfo.Name, | ||||||
|  | 		&fileInfo.Extension, | ||||||
|  | 		&fileInfo.Size, | ||||||
|  | 		&fileInfo.Archived, | ||||||
|  | 	) | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		if errors.Is(err, sql.ErrNoRows) { | ||||||
|  | 			return nil, nil | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		s.logger.Error("error scanning fileinfo row", mlog.String("id", id), mlog.Err(err)) | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return &fileInfo, nil | ||||||
|  | } | ||||||
| @@ -0,0 +1 @@ | |||||||
|  | DROP TABLE IF EXISTS {{.prefix}}file_info; | ||||||
| @@ -0,0 +1,9 @@ | |||||||
|  | CREATE TABLE {{.prefix}}file_info ( | ||||||
|  |     id varchar(26) NOT NULL, | ||||||
|  |     create_at BIGINT NOT NULL, | ||||||
|  |     delete_at BIGINT, | ||||||
|  |     name TEXT NOT NULL, | ||||||
|  |     extension VARCHAR(50) NOT NULL, | ||||||
|  |     size BIGINT NOT NULL, | ||||||
|  |     archived BOOLEAN | ||||||
|  | ) {{if .mysql}}DEFAULT CHARACTER SET utf8mb4{{end}}; | ||||||
| @@ -354,6 +354,11 @@ func (s *SQLStore) GetCategory(id string) (*model.Category, error) { | |||||||
|  |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (s *SQLStore) GetFileInfo(id string) (*mmModel.FileInfo, error) { | ||||||
|  | 	return s.getFileInfo(s.db, id) | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
| func (s *SQLStore) GetLicense() *mmModel.License { | func (s *SQLStore) GetLicense() *mmModel.License { | ||||||
| 	return s.getLicense(s.db) | 	return s.getLicense(s.db) | ||||||
|  |  | ||||||
| @@ -691,6 +696,11 @@ func (s *SQLStore) RunDataRetention(globalRetentionDate int64, batchSize int64) | |||||||
|  |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (s *SQLStore) SaveFileInfo(fileInfo *mmModel.FileInfo) error { | ||||||
|  | 	return s.saveFileInfo(s.db, fileInfo) | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
| func (s *SQLStore) SaveMember(bm *model.BoardMember) (*model.BoardMember, error) { | func (s *SQLStore) SaveMember(bm *model.BoardMember) (*model.BoardMember, error) { | ||||||
| 	return s.saveMember(s.db, bm) | 	return s.saveMember(s.db, bm) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -21,4 +21,5 @@ func TestSQLStore(t *testing.T) { | |||||||
| 	t.Run("SubscriptionStore", func(t *testing.T) { storetests.StoreTestSubscriptionsStore(t, SetupTests) }) | 	t.Run("SubscriptionStore", func(t *testing.T) { storetests.StoreTestSubscriptionsStore(t, SetupTests) }) | ||||||
| 	t.Run("NotificationHintStore", func(t *testing.T) { storetests.StoreTestNotificationHintsStore(t, SetupTests) }) | 	t.Run("NotificationHintStore", func(t *testing.T) { storetests.StoreTestNotificationHintsStore(t, SetupTests) }) | ||||||
| 	t.Run("DataRetention", func(t *testing.T) { storetests.StoreTestDataRetention(t, SetupTests) }) | 	t.Run("DataRetention", func(t *testing.T) { storetests.StoreTestDataRetention(t, SetupTests) }) | ||||||
|  | 	t.Run("StoreTestFileStore", func(t *testing.T) { storetests.StoreTestFileStore(t, SetupTests) }) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -114,6 +114,9 @@ type Store interface { | |||||||
|  |  | ||||||
| 	GetUserCategoryBoards(userID, teamID string) ([]model.CategoryBoards, error) | 	GetUserCategoryBoards(userID, teamID string) ([]model.CategoryBoards, error) | ||||||
|  |  | ||||||
|  | 	GetFileInfo(id string) (*mmModel.FileInfo, error) | ||||||
|  | 	SaveFileInfo(fileInfo *mmModel.FileInfo) error | ||||||
|  |  | ||||||
| 	// @withTransaction | 	// @withTransaction | ||||||
| 	AddUpdateCategoryBoard(userID, categoryID, blockID string) error | 	AddUpdateCategoryBoard(userID, categoryID, blockID string) error | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										38
									
								
								server/services/store/storetests/files.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								server/services/store/storetests/files.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | |||||||
|  | package storetests | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	"github.com/mattermost/focalboard/server/services/store" | ||||||
|  | 	"github.com/mattermost/focalboard/server/utils" | ||||||
|  | 	mmModel "github.com/mattermost/mattermost-server/v6/model" | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func StoreTestFileStore(t *testing.T, setup func(t *testing.T) (store.Store, func())) { | ||||||
|  | 	sqlStore, tearDown := setup(t) | ||||||
|  | 	defer tearDown() | ||||||
|  |  | ||||||
|  | 	t.Run("should save and retrieve fileinfo", func(t *testing.T) { | ||||||
|  | 		fileInfo := &mmModel.FileInfo{ | ||||||
|  | 			Id:        "file_info_1", | ||||||
|  | 			CreateAt:  utils.GetMillis(), | ||||||
|  | 			Name:      "Dunder Mifflin Sales Report 2022", | ||||||
|  | 			Extension: ".sales", | ||||||
|  | 			Size:      112233, | ||||||
|  | 			DeleteAt:  0, | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		err := sqlStore.SaveFileInfo(fileInfo) | ||||||
|  | 		assert.NoError(t, err) | ||||||
|  |  | ||||||
|  | 		retrievedFileInfo, err := sqlStore.GetFileInfo("file_info_1") | ||||||
|  | 		assert.NoError(t, err) | ||||||
|  | 		assert.Equal(t, "file_info_1", retrievedFileInfo.Id) | ||||||
|  | 		assert.Equal(t, "Dunder Mifflin Sales Report 2022", retrievedFileInfo.Name) | ||||||
|  | 		assert.Equal(t, ".sales", retrievedFileInfo.Extension) | ||||||
|  | 		assert.Equal(t, int64(112233), retrievedFileInfo.Size) | ||||||
|  | 		assert.Equal(t, int64(0), retrievedFileInfo.DeleteAt) | ||||||
|  | 		assert.False(t, retrievedFileInfo.Archived) | ||||||
|  | 	}) | ||||||
|  | } | ||||||
							
								
								
									
										3914
									
								
								webapp/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3914
									
								
								webapp/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -42,6 +42,14 @@ interface Block { | |||||||
|     deleteAt: number |     deleteAt: number | ||||||
| } | } | ||||||
|  |  | ||||||
|  | interface FileInfo { | ||||||
|  |     url?: string | ||||||
|  |     archived?: boolean | ||||||
|  |     extension?: string | ||||||
|  |     name?: string | ||||||
|  |     size?: number | ||||||
|  | } | ||||||
|  |  | ||||||
| function createBlock(block?: Block): Block { | function createBlock(block?: Block): Block { | ||||||
|     const now = Date.now() |     const now = Date.now() | ||||||
|     return { |     return { | ||||||
| @@ -106,5 +114,5 @@ function createPatchesFromBlocks(newBlock: Block, oldBlock: Block): BlockPatch[] | |||||||
|     ] |     ] | ||||||
| } | } | ||||||
|  |  | ||||||
| export type {ContentBlockTypes, BlockTypes} | export type {ContentBlockTypes, BlockTypes, FileInfo} | ||||||
| export {blockTypes, contentBlockTypes, Block, BlockPatch, createBlock, createPatchesFromBlocks} | export {blockTypes, contentBlockTypes, Block, BlockPatch, createBlock, createPatchesFromBlocks} | ||||||
|   | |||||||
| @@ -116,10 +116,8 @@ exports[`components/calculations/Calculation should match snapshot - option chan | |||||||
|             </div> |             </div> | ||||||
|             <input |             <input | ||||||
|               aria-autocomplete="list" |               aria-autocomplete="list" | ||||||
|               aria-controls="react-select-2-listbox" |  | ||||||
|               aria-expanded="false" |               aria-expanded="false" | ||||||
|               aria-haspopup="true" |               aria-haspopup="true" | ||||||
|               aria-owns="react-select-2-listbox" |  | ||||||
|               aria-readonly="true" |               aria-readonly="true" | ||||||
|               class="css-mohuvp-dummyInput-DummyInput" |               class="css-mohuvp-dummyInput-DummyInput" | ||||||
|               id="react-select-2-input" |               id="react-select-2-input" | ||||||
|   | |||||||
| @@ -28,10 +28,8 @@ exports[`components/calculations/Options should match snapshot 1`] = ` | |||||||
|         </div> |         </div> | ||||||
|         <input |         <input | ||||||
|           aria-autocomplete="list" |           aria-autocomplete="list" | ||||||
|           aria-controls="react-select-2-listbox" |  | ||||||
|           aria-expanded="false" |           aria-expanded="false" | ||||||
|           aria-haspopup="true" |           aria-haspopup="true" | ||||||
|           aria-owns="react-select-2-listbox" |  | ||||||
|           aria-readonly="true" |           aria-readonly="true" | ||||||
|           class="css-mohuvp-dummyInput-DummyInput" |           class="css-mohuvp-dummyInput-DummyInput" | ||||||
|           id="react-select-2-input" |           id="react-select-2-input" | ||||||
|   | |||||||
| @@ -1,5 +1,31 @@ | |||||||
| // Jest Snapshot v1, https://goo.gl/fbAQLP | // Jest Snapshot v1, https://goo.gl/fbAQLP | ||||||
|  |  | ||||||
|  | exports[`components/content/ImageElement archived file 1`] = ` | ||||||
|  | <div> | ||||||
|  |   <div | ||||||
|  |     class="ArchivedFile" | ||||||
|  |   > | ||||||
|  |     <i | ||||||
|  |       class="CompassIcon icon-file-image-broken-outline BrokenFile" | ||||||
|  |     /> | ||||||
|  |     <div | ||||||
|  |       class="fileMetadata" | ||||||
|  |     > | ||||||
|  |       <p | ||||||
|  |         class="filename" | ||||||
|  |       > | ||||||
|  |         Filename | ||||||
|  |       </p> | ||||||
|  |       <p> | ||||||
|  |         TXT | ||||||
|  |           | ||||||
|  |         161.1 KiB | ||||||
|  |       </p> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </div> | ||||||
|  | `; | ||||||
|  |  | ||||||
| exports[`components/content/ImageElement should match snapshot 1`] = ` | exports[`components/content/ImageElement should match snapshot 1`] = ` | ||||||
| <div> | <div> | ||||||
|   <img |   <img | ||||||
|   | |||||||
| @@ -0,0 +1,27 @@ | |||||||
|  | // Jest Snapshot v1, https://goo.gl/fbAQLP | ||||||
|  |  | ||||||
|  | exports[`components/content/archivedFile should match snapshot 1`] = ` | ||||||
|  | <div> | ||||||
|  |   <div | ||||||
|  |     class="ArchivedFile" | ||||||
|  |   > | ||||||
|  |     <i | ||||||
|  |       class="CompassIcon icon-file-image-broken-outline BrokenFile" | ||||||
|  |     /> | ||||||
|  |     <div | ||||||
|  |       class="fileMetadata" | ||||||
|  |     > | ||||||
|  |       <p | ||||||
|  |         class="filename" | ||||||
|  |       > | ||||||
|  |         stuff to put in jell-o | ||||||
|  |       </p> | ||||||
|  |       <p> | ||||||
|  |         TXT | ||||||
|  |           | ||||||
|  |         2.0 KiB | ||||||
|  |       </p> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </div> | ||||||
|  | `; | ||||||
							
								
								
									
										27
									
								
								webapp/src/components/content/archivedFile/archivedFile.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								webapp/src/components/content/archivedFile/archivedFile.scss
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | .ArchivedFile { | ||||||
|  |     border: 2px solid rgba(var(--center-channel-color-rgb), 0.1); | ||||||
|  |     border-radius: 4px; | ||||||
|  |     box-shadow: 0 2px 3px rgba(var(--center-channel-color-rgb), 0.1); | ||||||
|  |     padding: 12px; | ||||||
|  |     display: flex; | ||||||
|  |     flex-direction: row; | ||||||
|  |     color: rgba(var(--center-channel-color-rgb), 0.4); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     .CompassIcon.icon-file-image-broken-outline.BrokenFile { | ||||||
|  |         font-size: 48px; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .fileMetadata { | ||||||
|  |         display: flex; | ||||||
|  |         flex-direction: column; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .fileMetadata > p { | ||||||
|  |         margin: 0; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .filename { | ||||||
|  |         font-weight: bold; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,25 @@ | |||||||
|  | // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. | ||||||
|  | // See LICENSE.txt for license information. | ||||||
|  |  | ||||||
|  | import React from 'react' | ||||||
|  |  | ||||||
|  | import {render} from '@testing-library/react' | ||||||
|  |  | ||||||
|  | import {FileInfo} from "../../../blocks/block" | ||||||
|  |  | ||||||
|  | import ArchivedFile from "./archivedFile" | ||||||
|  |  | ||||||
|  | describe('components/content/archivedFile', () => { | ||||||
|  |     it('should match snapshot', () => { | ||||||
|  |         const fileInfo: FileInfo = { | ||||||
|  |             archived: true, | ||||||
|  |             extension: '.txt', | ||||||
|  |             name: 'stuff to put in jell-o', | ||||||
|  |             size: 2056, | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         const component = (<ArchivedFile fileInfo={fileInfo}/>) | ||||||
|  |         const {container} = render(component) | ||||||
|  |         expect(container).toMatchSnapshot() | ||||||
|  |     }) | ||||||
|  | }) | ||||||
							
								
								
									
										37
									
								
								webapp/src/components/content/archivedFile/archivedFile.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								webapp/src/components/content/archivedFile/archivedFile.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | |||||||
|  | // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. | ||||||
|  | // See LICENSE.txt for license information. | ||||||
|  | import React, {useCallback} from 'react' | ||||||
|  |  | ||||||
|  | import {FileInfo} from '../../../blocks/block' | ||||||
|  | import BrokenFile from '../../../widgets/icons/brokenFile' | ||||||
|  | import {Utils} from '../../../utils' | ||||||
|  |  | ||||||
|  | import './archivedFile.scss' | ||||||
|  |  | ||||||
|  | type Props = { | ||||||
|  |     fileInfo: FileInfo | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const ArchivedFile = (props: Props): JSX.Element => { | ||||||
|  |     const fileName = useCallback(() => props.fileInfo.name || 'untitled file', [props.fileInfo.name]) | ||||||
|  |  | ||||||
|  |     const fileExtension = useCallback(() => { | ||||||
|  |         let extension = props.fileInfo.extension | ||||||
|  |         extension = extension?.startsWith('.') ? extension?.substring(1) : extension | ||||||
|  |         return extension?.toUpperCase() | ||||||
|  |     }, [props.fileInfo.extension]) | ||||||
|  |  | ||||||
|  |     const fileSize = useCallback(() => Utils.humanFileSize(props.fileInfo.size || 0), [props.fileInfo.size]) | ||||||
|  |  | ||||||
|  |     return ( | ||||||
|  |         <div className='ArchivedFile'> | ||||||
|  |             <BrokenFile/> | ||||||
|  |             <div className='fileMetadata'> | ||||||
|  |                 <p className='filename'>{fileName()}</p> | ||||||
|  |                 <p>{fileExtension()} {fileSize()}</p> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |     ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default ArchivedFile | ||||||
| @@ -18,7 +18,7 @@ import ImageElement from './imageElement' | |||||||
|  |  | ||||||
| jest.mock('../../octoClient') | jest.mock('../../octoClient') | ||||||
| const mockedOcto = mocked(octoClient, true) | const mockedOcto = mocked(octoClient, true) | ||||||
| mockedOcto.getFileAsDataUrl.mockResolvedValue('test.jpg') | mockedOcto.getFileAsDataUrl.mockResolvedValue({url: 'test.jpg'}) | ||||||
|  |  | ||||||
| describe('components/content/ImageElement', () => { | describe('components/content/ImageElement', () => { | ||||||
|     const defaultBlock: ImageBlock = { |     const defaultBlock: ImageBlock = { | ||||||
| @@ -51,4 +51,25 @@ describe('components/content/ImageElement', () => { | |||||||
|         }) |         }) | ||||||
|         expect(imageContainer).toMatchSnapshot() |         expect(imageContainer).toMatchSnapshot() | ||||||
|     }) |     }) | ||||||
|  |  | ||||||
|  |     test('archived file', async () => { | ||||||
|  |         mockedOcto.getFileAsDataUrl.mockResolvedValue({ | ||||||
|  |             archived: true, | ||||||
|  |             name: 'Filename', | ||||||
|  |             extension: '.txt', | ||||||
|  |             size: 165002, | ||||||
|  |         }) | ||||||
|  |  | ||||||
|  |         const component = wrapIntl( | ||||||
|  |             <ImageElement | ||||||
|  |                 block={defaultBlock} | ||||||
|  |             />, | ||||||
|  |         ) | ||||||
|  |         let imageContainer: Element | undefined | ||||||
|  |         await act(async () => { | ||||||
|  |             const {container} = render(component) | ||||||
|  |             imageContainer = container | ||||||
|  |         }) | ||||||
|  |         expect(imageContainer).toMatchSnapshot() | ||||||
|  |     }) | ||||||
| }) | }) | ||||||
|   | |||||||
| @@ -10,7 +10,10 @@ import {Utils} from '../../utils' | |||||||
| import ImageIcon from '../../widgets/icons/image' | import ImageIcon from '../../widgets/icons/image' | ||||||
| import {sendFlashMessage} from '../../components/flashMessages' | import {sendFlashMessage} from '../../components/flashMessages' | ||||||
|  |  | ||||||
|  | import {FileInfo} from '../../blocks/block' | ||||||
|  |  | ||||||
| import {contentRegistry} from './contentRegistry' | import {contentRegistry} from './contentRegistry' | ||||||
|  | import ArchivedFile from './archivedFile/archivedFile' | ||||||
|  |  | ||||||
| type Props = { | type Props = { | ||||||
|     block: ContentBlock |     block: ContentBlock | ||||||
| @@ -18,18 +21,26 @@ type Props = { | |||||||
|  |  | ||||||
| const ImageElement = (props: Props): JSX.Element|null => { | const ImageElement = (props: Props): JSX.Element|null => { | ||||||
|     const [imageDataUrl, setImageDataUrl] = useState<string|null>(null) |     const [imageDataUrl, setImageDataUrl] = useState<string|null>(null) | ||||||
|  |     const [fileInfo, setFileInfo] = useState<FileInfo>({}) | ||||||
|  |  | ||||||
|     const {block} = props |     const {block} = props | ||||||
|  |  | ||||||
|     useEffect(() => { |     useEffect(() => { | ||||||
|         if (!imageDataUrl) { |         if (!imageDataUrl) { | ||||||
|             const loadImage = async () => { |             const loadImage = async () => { | ||||||
|                 const url = await octoClient.getFileAsDataUrl(block.boardId, props.block.fields.fileId) |                 const fileURL = await octoClient.getFileAsDataUrl(block.boardId, props.block.fields.fileId) | ||||||
|                 setImageDataUrl(url) |                 setImageDataUrl(fileURL.url || '') | ||||||
|  |                 setFileInfo(fileURL) | ||||||
|             } |             } | ||||||
|             loadImage() |             loadImage() | ||||||
|         } |         } | ||||||
|     }) |     }, []) | ||||||
|  |  | ||||||
|  |     if (fileInfo.archived) { | ||||||
|  |         return ( | ||||||
|  |             <ArchivedFile fileInfo={fileInfo}/> | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  |  | ||||||
|     if (!imageDataUrl) { |     if (!imageDataUrl) { | ||||||
|         return null |         return null | ||||||
|   | |||||||
| @@ -34,7 +34,7 @@ describe('components/contentBlock', () => { | |||||||
|     const mockedOcto = mocked(octoClient, true) |     const mockedOcto = mocked(octoClient, true) | ||||||
|  |  | ||||||
|     mockedUtils.createGuid.mockReturnValue('test-id') |     mockedUtils.createGuid.mockReturnValue('test-id') | ||||||
|     mockedOcto.getFileAsDataUrl.mockResolvedValue('test.jpg') |     mockedOcto.getFileAsDataUrl.mockResolvedValue({url: 'test.jpg'}) | ||||||
|  |  | ||||||
|     const board = TestBlockFactory.createBoard() |     const board = TestBlockFactory.createBoard() | ||||||
|     board.cardProperties = [] |     board.cardProperties = [] | ||||||
|   | |||||||
| @@ -32,7 +32,7 @@ describe('src/components/gallery/GalleryCard', () => { | |||||||
|     const mockedMutator = mocked(mutator, true) |     const mockedMutator = mocked(mutator, true) | ||||||
|     const mockedUtils = mocked(Utils, true) |     const mockedUtils = mocked(Utils, true) | ||||||
|     const mockedOcto = mocked(octoClient, true) |     const mockedOcto = mocked(octoClient, true) | ||||||
|     mockedOcto.getFileAsDataUrl.mockResolvedValue('test.jpg') |     mockedOcto.getFileAsDataUrl.mockResolvedValue({url: 'test.jpg'}) | ||||||
|  |  | ||||||
|     const board = TestBlockFactory.createBoard() |     const board = TestBlockFactory.createBoard() | ||||||
|     board.id = 'boardId' |     board.id = 'boardId' | ||||||
|   | |||||||
| @@ -28,10 +28,8 @@ exports[`components/kanban/calculations/KanbanCalculationOptions base case 1`] = | |||||||
|         </div> |         </div> | ||||||
|         <input |         <input | ||||||
|           aria-autocomplete="list" |           aria-autocomplete="list" | ||||||
|           aria-controls="react-select-2-listbox" |  | ||||||
|           aria-expanded="false" |           aria-expanded="false" | ||||||
|           aria-haspopup="true" |           aria-haspopup="true" | ||||||
|           aria-owns="react-select-2-listbox" |  | ||||||
|           aria-readonly="true" |           aria-readonly="true" | ||||||
|           class="css-mohuvp-dummyInput-DummyInput" |           class="css-mohuvp-dummyInput-DummyInput" | ||||||
|           id="react-select-2-input" |           id="react-select-2-input" | ||||||
|   | |||||||
| @@ -36,10 +36,8 @@ exports[`components/properties/user not readonly 1`] = ` | |||||||
|         > |         > | ||||||
|           <input |           <input | ||||||
|             aria-autocomplete="list" |             aria-autocomplete="list" | ||||||
|             aria-controls="react-select-3-listbox" |  | ||||||
|             aria-expanded="false" |             aria-expanded="false" | ||||||
|             aria-haspopup="true" |             aria-haspopup="true" | ||||||
|             aria-owns="react-select-3-listbox" |  | ||||||
|             autocapitalize="none" |             autocapitalize="none" | ||||||
|             autocomplete="off" |             autocomplete="off" | ||||||
|             autocorrect="off" |             autocorrect="off" | ||||||
| @@ -133,11 +131,9 @@ exports[`components/properties/user not readonly not existing user 1`] = ` | |||||||
|         > |         > | ||||||
|           <input |           <input | ||||||
|             aria-autocomplete="list" |             aria-autocomplete="list" | ||||||
|             aria-controls="react-select-2-listbox" |  | ||||||
|             aria-describedby="react-select-2-placeholder" |             aria-describedby="react-select-2-placeholder" | ||||||
|             aria-expanded="false" |             aria-expanded="false" | ||||||
|             aria-haspopup="true" |             aria-haspopup="true" | ||||||
|             aria-owns="react-select-2-listbox" |  | ||||||
|             autocapitalize="none" |             autocapitalize="none" | ||||||
|             autocomplete="off" |             autocomplete="off" | ||||||
|             autocorrect="off" |             autocorrect="off" | ||||||
|   | |||||||
| @@ -79,11 +79,9 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Copy l | |||||||
|                   > |                   > | ||||||
|                     <input |                     <input | ||||||
|                       aria-autocomplete="list" |                       aria-autocomplete="list" | ||||||
|                       aria-controls="react-select-4-listbox" |  | ||||||
|                       aria-describedby="react-select-4-placeholder" |                       aria-describedby="react-select-4-placeholder" | ||||||
|                       aria-expanded="false" |                       aria-expanded="false" | ||||||
|                       aria-haspopup="true" |                       aria-haspopup="true" | ||||||
|                       aria-owns="react-select-4-listbox" |  | ||||||
|                       autocapitalize="none" |                       autocapitalize="none" | ||||||
|                       autocomplete="off" |                       autocomplete="off" | ||||||
|                       autocorrect="off" |                       autocorrect="off" | ||||||
| @@ -305,11 +303,9 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Copy l | |||||||
|                   > |                   > | ||||||
|                     <input |                     <input | ||||||
|                       aria-autocomplete="list" |                       aria-autocomplete="list" | ||||||
|                       aria-controls="react-select-4-listbox" |  | ||||||
|                       aria-describedby="react-select-4-placeholder" |                       aria-describedby="react-select-4-placeholder" | ||||||
|                       aria-expanded="false" |                       aria-expanded="false" | ||||||
|                       aria-haspopup="true" |                       aria-haspopup="true" | ||||||
|                       aria-owns="react-select-4-listbox" |  | ||||||
|                       autocapitalize="none" |                       autocapitalize="none" | ||||||
|                       autocomplete="off" |                       autocomplete="off" | ||||||
|                       autocorrect="off" |                       autocorrect="off" | ||||||
| @@ -531,11 +527,9 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Regene | |||||||
|                   > |                   > | ||||||
|                     <input |                     <input | ||||||
|                       aria-autocomplete="list" |                       aria-autocomplete="list" | ||||||
|                       aria-controls="react-select-5-listbox" |  | ||||||
|                       aria-describedby="react-select-5-placeholder" |                       aria-describedby="react-select-5-placeholder" | ||||||
|                       aria-expanded="false" |                       aria-expanded="false" | ||||||
|                       aria-haspopup="true" |                       aria-haspopup="true" | ||||||
|                       aria-owns="react-select-5-listbox" |  | ||||||
|                       autocapitalize="none" |                       autocapitalize="none" | ||||||
|                       autocomplete="off" |                       autocomplete="off" | ||||||
|                       autocorrect="off" |                       autocorrect="off" | ||||||
| @@ -780,11 +774,9 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Select | |||||||
|                   > |                   > | ||||||
|                     <input |                     <input | ||||||
|                       aria-autocomplete="list" |                       aria-autocomplete="list" | ||||||
|                       aria-controls="react-select-10-listbox" |  | ||||||
|                       aria-describedby="react-select-10-placeholder" |                       aria-describedby="react-select-10-placeholder" | ||||||
|                       aria-expanded="false" |                       aria-expanded="false" | ||||||
|                       aria-haspopup="true" |                       aria-haspopup="true" | ||||||
|                       aria-owns="react-select-10-listbox" |  | ||||||
|                       autocapitalize="none" |                       autocapitalize="none" | ||||||
|                       autocomplete="off" |                       autocomplete="off" | ||||||
|                       autocorrect="off" |                       autocorrect="off" | ||||||
| @@ -1302,11 +1294,9 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Select | |||||||
|                   > |                   > | ||||||
|                     <input |                     <input | ||||||
|                       aria-autocomplete="list" |                       aria-autocomplete="list" | ||||||
|                       aria-controls="react-select-11-listbox" |  | ||||||
|                       aria-describedby="react-select-11-placeholder" |                       aria-describedby="react-select-11-placeholder" | ||||||
|                       aria-expanded="false" |                       aria-expanded="false" | ||||||
|                       aria-haspopup="true" |                       aria-haspopup="true" | ||||||
|                       aria-owns="react-select-11-listbox" |  | ||||||
|                       autocapitalize="none" |                       autocapitalize="none" | ||||||
|                       autocomplete="off" |                       autocomplete="off" | ||||||
|                       autocorrect="off" |                       autocorrect="off" | ||||||
| @@ -1824,11 +1814,9 @@ exports[`src/components/shareBoard/shareBoard return shareBoard, and click switc | |||||||
|                   > |                   > | ||||||
|                     <input |                     <input | ||||||
|                       aria-autocomplete="list" |                       aria-autocomplete="list" | ||||||
|                       aria-controls="react-select-6-listbox" |  | ||||||
|                       aria-describedby="react-select-6-placeholder" |                       aria-describedby="react-select-6-placeholder" | ||||||
|                       aria-expanded="false" |                       aria-expanded="false" | ||||||
|                       aria-haspopup="true" |                       aria-haspopup="true" | ||||||
|                       aria-owns="react-select-6-listbox" |  | ||||||
|                       autocapitalize="none" |                       autocapitalize="none" | ||||||
|                       autocomplete="off" |                       autocomplete="off" | ||||||
|                       autocorrect="off" |                       autocorrect="off" | ||||||
| @@ -2073,11 +2061,9 @@ exports[`src/components/shareBoard/shareBoard return shareBoardComponent and cli | |||||||
|                   > |                   > | ||||||
|                     <input |                     <input | ||||||
|                       aria-autocomplete="list" |                       aria-autocomplete="list" | ||||||
|                       aria-controls="react-select-7-listbox" |  | ||||||
|                       aria-describedby="react-select-7-placeholder" |                       aria-describedby="react-select-7-placeholder" | ||||||
|                       aria-expanded="false" |                       aria-expanded="false" | ||||||
|                       aria-haspopup="true" |                       aria-haspopup="true" | ||||||
|                       aria-owns="react-select-7-listbox" |  | ||||||
|                       autocapitalize="none" |                       autocapitalize="none" | ||||||
|                       autocomplete="off" |                       autocomplete="off" | ||||||
|                       autocorrect="off" |                       autocorrect="off" | ||||||
| @@ -2322,11 +2308,9 @@ exports[`src/components/shareBoard/shareBoard should match snapshot 1`] = ` | |||||||
|                   > |                   > | ||||||
|                     <input |                     <input | ||||||
|                       aria-autocomplete="list" |                       aria-autocomplete="list" | ||||||
|                       aria-controls="react-select-2-listbox" |  | ||||||
|                       aria-describedby="react-select-2-placeholder" |                       aria-describedby="react-select-2-placeholder" | ||||||
|                       aria-expanded="false" |                       aria-expanded="false" | ||||||
|                       aria-haspopup="true" |                       aria-haspopup="true" | ||||||
|                       aria-owns="react-select-2-listbox" |  | ||||||
|                       autocapitalize="none" |                       autocapitalize="none" | ||||||
|                       autocomplete="off" |                       autocomplete="off" | ||||||
|                       autocorrect="off" |                       autocorrect="off" | ||||||
| @@ -2548,11 +2532,9 @@ exports[`src/components/shareBoard/shareBoard should match snapshot with sharing | |||||||
|                   > |                   > | ||||||
|                     <input |                     <input | ||||||
|                       aria-autocomplete="list" |                       aria-autocomplete="list" | ||||||
|                       aria-controls="react-select-3-listbox" |  | ||||||
|                       aria-describedby="react-select-3-placeholder" |                       aria-describedby="react-select-3-placeholder" | ||||||
|                       aria-expanded="false" |                       aria-expanded="false" | ||||||
|                       aria-haspopup="true" |                       aria-haspopup="true" | ||||||
|                       aria-owns="react-select-3-listbox" |  | ||||||
|                       autocapitalize="none" |                       autocapitalize="none" | ||||||
|                       autocomplete="off" |                       autocomplete="off" | ||||||
|                       autocorrect="off" |                       autocorrect="off" | ||||||
| @@ -2774,11 +2756,9 @@ exports[`src/components/shareBoard/shareBoard should match snapshot with sharing | |||||||
|                   > |                   > | ||||||
|                     <input |                     <input | ||||||
|                       aria-autocomplete="list" |                       aria-autocomplete="list" | ||||||
|                       aria-controls="react-select-9-listbox" |  | ||||||
|                       aria-describedby="react-select-9-placeholder" |                       aria-describedby="react-select-9-placeholder" | ||||||
|                       aria-expanded="false" |                       aria-expanded="false" | ||||||
|                       aria-haspopup="true" |                       aria-haspopup="true" | ||||||
|                       aria-owns="react-select-9-listbox" |  | ||||||
|                       autocapitalize="none" |                       autocapitalize="none" | ||||||
|                       autocomplete="off" |                       autocomplete="off" | ||||||
|                       autocorrect="off" |                       autocorrect="off" | ||||||
| @@ -3000,11 +2980,9 @@ exports[`src/components/shareBoard/shareBoard should match snapshot with sharing | |||||||
|                   > |                   > | ||||||
|                     <input |                     <input | ||||||
|                       aria-autocomplete="list" |                       aria-autocomplete="list" | ||||||
|                       aria-controls="react-select-8-listbox" |  | ||||||
|                       aria-describedby="react-select-8-placeholder" |                       aria-describedby="react-select-8-placeholder" | ||||||
|                       aria-expanded="false" |                       aria-expanded="false" | ||||||
|                       aria-haspopup="true" |                       aria-haspopup="true" | ||||||
|                       aria-owns="react-select-8-listbox" |  | ||||||
|                       autocapitalize="none" |                       autocapitalize="none" | ||||||
|                       autocomplete="off" |                       autocomplete="off" | ||||||
|                       autocorrect="off" |                       autocorrect="off" | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. | // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. | ||||||
| // See LICENSE.txt for license information. | // See LICENSE.txt for license information. | ||||||
| import {Block, BlockPatch} from './blocks/block' | import {Block, BlockPatch, FileInfo} from './blocks/block' | ||||||
| import {Board, BoardsAndBlocks, BoardsAndBlocksPatch, BoardPatch, BoardMember} from './blocks/board' | import {Board, BoardsAndBlocks, BoardsAndBlocksPatch, BoardPatch, BoardMember} from './blocks/board' | ||||||
| import {ISharing} from './blocks/sharing' | import {ISharing} from './blocks/sharing' | ||||||
| import {OctoUtils} from './octoUtils' | import {OctoUtils} from './octoUtils' | ||||||
| @@ -556,18 +556,23 @@ class OctoClient { | |||||||
|         return undefined |         return undefined | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async getFileAsDataUrl(boardId: string, fileId: string): Promise<string> { |     async getFileAsDataUrl(boardId: string, fileId: string): Promise<FileInfo> { | ||||||
|         let path = '/api/v2/files/teams/' + this.teamId + '/' + boardId + '/' + fileId |         let path = '/api/v2/files/teams/' + this.teamId + '/' + boardId + '/' + fileId | ||||||
|         const readToken = Utils.getReadToken() |         const readToken = Utils.getReadToken() | ||||||
|         if (readToken) { |         if (readToken) { | ||||||
|             path += `?read_token=${readToken}` |             path += `?read_token=${readToken}` | ||||||
|         } |         } | ||||||
|         const response = await fetch(this.getBaseURL() + path, {headers: this.headers()}) |         const response = await fetch(this.getBaseURL() + path, {headers: this.headers()}) | ||||||
|         if (response.status !== 200) { |         let fileInfo: FileInfo = {} | ||||||
|             return '' |  | ||||||
|  |         if (response.status === 200) { | ||||||
|  |             const blob = await response.blob() | ||||||
|  |             fileInfo.url = URL.createObjectURL(blob) | ||||||
|  |         } else if (response.status === 400) { | ||||||
|  |             fileInfo = await this.getJson(response, {}) as FileInfo | ||||||
|         } |         } | ||||||
|         const blob = await response.blob() |  | ||||||
|         return URL.createObjectURL(blob) |         return fileInfo | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async getTeam(): Promise<Team | null> { |     async getTeam(): Promise<Team | null> { | ||||||
|   | |||||||
| @@ -723,6 +723,26 @@ class Utils { | |||||||
|         const newPath = generatePath(match.path, params) |         const newPath = generatePath(match.path, params) | ||||||
|         history.push(newPath) |         history.push(newPath) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     static humanFileSize(bytesParam: number, si = false, dp = 1): string { | ||||||
|  |         let bytes = bytesParam | ||||||
|  |         const thresh = si ? 1000 : 1024 | ||||||
|  |  | ||||||
|  |         if (Math.abs(bytes) < thresh) { | ||||||
|  |             return bytes + ' B' | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         const units = si ? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] : ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'] | ||||||
|  |         let u = -1 | ||||||
|  |         const r = 10 ** dp | ||||||
|  |  | ||||||
|  |         do { | ||||||
|  |             bytes /= thresh | ||||||
|  |             ++u | ||||||
|  |         } while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1) | ||||||
|  |  | ||||||
|  |         return bytes.toFixed(dp) + ' ' + units[u] | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| export {Utils, IDType} | export {Utils, IDType} | ||||||
|   | |||||||
							
								
								
									
										15
									
								
								webapp/src/widgets/icons/brokenFile.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								webapp/src/widgets/icons/brokenFile.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. | ||||||
|  | // See LICENSE.txt for license information. | ||||||
|  |  | ||||||
|  | import React from 'react' | ||||||
|  |  | ||||||
|  | import CompassIcon from './compassIcon' | ||||||
|  |  | ||||||
|  | export default function BrokenFile(): JSX.Element { | ||||||
|  |     return ( | ||||||
|  |         <CompassIcon | ||||||
|  |             icon='file-image-broken-outline' | ||||||
|  |             className='BrokenFile' | ||||||
|  |         /> | ||||||
|  |     ) | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user