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) | ||||
|  | ||||
| 	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) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
|   | ||||
| @@ -1,18 +1,23 @@ | ||||
| package app | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
|  | ||||
| 	mmModel "github.com/mattermost/mattermost-server/v6/model" | ||||
|  | ||||
| 	"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/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) { | ||||
| 	// NOTE: File extension includes the dot | ||||
| 	fileExtension := strings.ToLower(filepath.Ext(filename)) | ||||
| @@ -20,15 +25,63 @@ func (a *App) SaveFile(reader io.Reader, teamID, rootID, filename string) (strin | ||||
| 		fileExtension = ".jpg" | ||||
| 	} | ||||
|  | ||||
| 	createdFilename := fmt.Sprintf(`%s%s`, utils.NewID(utils.IDTypeNone), fileExtension) | ||||
| 	filePath := filepath.Join(teamID, rootID, createdFilename) | ||||
| 	createdFilename := utils.NewID(utils.IDTypeNone) | ||||
| 	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 { | ||||
| 		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) { | ||||
|   | ||||
| @@ -1,14 +1,17 @@ | ||||
| package app | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"io" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/golang/mock/gomock" | ||||
| 	"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/shared/filestore" | ||||
| 	"github.com/mattermost/mattermost-server/v6/shared/filestore/mocks" | ||||
| @@ -19,6 +22,8 @@ const ( | ||||
| 	testBoardID  = "test-board-id" | ||||
| ) | ||||
|  | ||||
| var errDummy = errors.New("hello") | ||||
|  | ||||
| type TestError struct{} | ||||
|  | ||||
| func (err *TestError) Error() string { return "Mocked File backend error" } | ||||
| @@ -186,6 +191,7 @@ func TestSaveFile(t *testing.T) { | ||||
| 		fileName := "temp-file-name.txt" | ||||
| 		mockedFileBackend := &mocks.FileBackend{} | ||||
| 		th.App.filesBackend = mockedFileBackend | ||||
| 		th.Store.EXPECT().SaveFileInfo(gomock.Any()).Return(nil) | ||||
|  | ||||
| 		writeFileFunc := func(reader io.Reader, path string) int64 { | ||||
| 			paths := strings.Split(path, string(os.PathSeparator)) | ||||
| @@ -209,6 +215,7 @@ func TestSaveFile(t *testing.T) { | ||||
| 		fileName := "temp-file-name.jpeg" | ||||
| 		mockedFileBackend := &mocks.FileBackend{} | ||||
| 		th.App.filesBackend = mockedFileBackend | ||||
| 		th.Store.EXPECT().SaveFileInfo(gomock.Any()).Return(nil) | ||||
|  | ||||
| 		writeFileFunc := func(reader io.Reader, path string) int64 { | ||||
| 			paths := strings.Split(path, string(os.PathSeparator)) | ||||
| @@ -233,6 +240,7 @@ func TestSaveFile(t *testing.T) { | ||||
| 		mockedFileBackend := &mocks.FileBackend{} | ||||
| 		th.App.filesBackend = mockedFileBackend | ||||
| 		mockedError := &TestError{} | ||||
| 		th.Store.EXPECT().SaveFileInfo(gomock.Any()).Return(nil) | ||||
|  | ||||
| 		writeFileFunc := func(reader io.Reader, path string) int64 { | ||||
| 			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()) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| 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 ( | ||||
| 	"database/sql" | ||||
| 	"encoding/json" | ||||
| 	"net/http" | ||||
|  | ||||
| 	mmModel "github.com/mattermost/mattermost-server/v6/model" | ||||
| 	"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 { | ||||
| 	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) | ||||
| } | ||||
|  | ||||
| // 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. | ||||
| func (m *MockStore) GetLicense() *model0.License { | ||||
| 	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) | ||||
| } | ||||
|  | ||||
| // 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. | ||||
| func (m *MockStore) SaveMember(arg0 *model.BoardMember) (*model.BoardMember, error) { | ||||
| 	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 { | ||||
| 	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) { | ||||
| 	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("NotificationHintStore", func(t *testing.T) { storetests.StoreTestNotificationHintsStore(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) | ||||
|  | ||||
| 	GetFileInfo(id string) (*mmModel.FileInfo, error) | ||||
| 	SaveFileInfo(fileInfo *mmModel.FileInfo) error | ||||
|  | ||||
| 	// @withTransaction | ||||
| 	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) | ||||
| 	}) | ||||
| } | ||||
							
								
								
									
										3908
									
								
								webapp/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3908
									
								
								webapp/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -42,6 +42,14 @@ interface Block { | ||||
|     deleteAt: number | ||||
| } | ||||
|  | ||||
| interface FileInfo { | ||||
|     url?: string | ||||
|     archived?: boolean | ||||
|     extension?: string | ||||
|     name?: string | ||||
|     size?: number | ||||
| } | ||||
|  | ||||
| function createBlock(block?: Block): Block { | ||||
|     const now = Date.now() | ||||
|     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} | ||||
|   | ||||
| @@ -116,10 +116,8 @@ exports[`components/calculations/Calculation should match snapshot - option chan | ||||
|             </div> | ||||
|             <input | ||||
|               aria-autocomplete="list" | ||||
|               aria-controls="react-select-2-listbox" | ||||
|               aria-expanded="false" | ||||
|               aria-haspopup="true" | ||||
|               aria-owns="react-select-2-listbox" | ||||
|               aria-readonly="true" | ||||
|               class="css-mohuvp-dummyInput-DummyInput" | ||||
|               id="react-select-2-input" | ||||
|   | ||||
| @@ -28,10 +28,8 @@ exports[`components/calculations/Options should match snapshot 1`] = ` | ||||
|         </div> | ||||
|         <input | ||||
|           aria-autocomplete="list" | ||||
|           aria-controls="react-select-2-listbox" | ||||
|           aria-expanded="false" | ||||
|           aria-haspopup="true" | ||||
|           aria-owns="react-select-2-listbox" | ||||
|           aria-readonly="true" | ||||
|           class="css-mohuvp-dummyInput-DummyInput" | ||||
|           id="react-select-2-input" | ||||
|   | ||||
| @@ -1,5 +1,31 @@ | ||||
| // 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`] = ` | ||||
| <div> | ||||
|   <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') | ||||
| const mockedOcto = mocked(octoClient, true) | ||||
| mockedOcto.getFileAsDataUrl.mockResolvedValue('test.jpg') | ||||
| mockedOcto.getFileAsDataUrl.mockResolvedValue({url: 'test.jpg'}) | ||||
|  | ||||
| describe('components/content/ImageElement', () => { | ||||
|     const defaultBlock: ImageBlock = { | ||||
| @@ -51,4 +51,25 @@ describe('components/content/ImageElement', () => { | ||||
|         }) | ||||
|         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 {sendFlashMessage} from '../../components/flashMessages' | ||||
|  | ||||
| import {FileInfo} from '../../blocks/block' | ||||
|  | ||||
| import {contentRegistry} from './contentRegistry' | ||||
| import ArchivedFile from './archivedFile/archivedFile' | ||||
|  | ||||
| type Props = { | ||||
|     block: ContentBlock | ||||
| @@ -18,18 +21,26 @@ type Props = { | ||||
|  | ||||
| const ImageElement = (props: Props): JSX.Element|null => { | ||||
|     const [imageDataUrl, setImageDataUrl] = useState<string|null>(null) | ||||
|     const [fileInfo, setFileInfo] = useState<FileInfo>({}) | ||||
|  | ||||
|     const {block} = props | ||||
|  | ||||
|     useEffect(() => { | ||||
|         if (!imageDataUrl) { | ||||
|             const loadImage = async () => { | ||||
|                 const url = await octoClient.getFileAsDataUrl(block.boardId, props.block.fields.fileId) | ||||
|                 setImageDataUrl(url) | ||||
|                 const fileURL = await octoClient.getFileAsDataUrl(block.boardId, props.block.fields.fileId) | ||||
|                 setImageDataUrl(fileURL.url || '') | ||||
|                 setFileInfo(fileURL) | ||||
|             } | ||||
|             loadImage() | ||||
|         } | ||||
|     }) | ||||
|     }, []) | ||||
|  | ||||
|     if (fileInfo.archived) { | ||||
|         return ( | ||||
|             <ArchivedFile fileInfo={fileInfo}/> | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     if (!imageDataUrl) { | ||||
|         return null | ||||
|   | ||||
| @@ -34,7 +34,7 @@ describe('components/contentBlock', () => { | ||||
|     const mockedOcto = mocked(octoClient, true) | ||||
|  | ||||
|     mockedUtils.createGuid.mockReturnValue('test-id') | ||||
|     mockedOcto.getFileAsDataUrl.mockResolvedValue('test.jpg') | ||||
|     mockedOcto.getFileAsDataUrl.mockResolvedValue({url: 'test.jpg'}) | ||||
|  | ||||
|     const board = TestBlockFactory.createBoard() | ||||
|     board.cardProperties = [] | ||||
|   | ||||
| @@ -32,7 +32,7 @@ describe('src/components/gallery/GalleryCard', () => { | ||||
|     const mockedMutator = mocked(mutator, true) | ||||
|     const mockedUtils = mocked(Utils, true) | ||||
|     const mockedOcto = mocked(octoClient, true) | ||||
|     mockedOcto.getFileAsDataUrl.mockResolvedValue('test.jpg') | ||||
|     mockedOcto.getFileAsDataUrl.mockResolvedValue({url: 'test.jpg'}) | ||||
|  | ||||
|     const board = TestBlockFactory.createBoard() | ||||
|     board.id = 'boardId' | ||||
|   | ||||
| @@ -28,10 +28,8 @@ exports[`components/kanban/calculations/KanbanCalculationOptions base case 1`] = | ||||
|         </div> | ||||
|         <input | ||||
|           aria-autocomplete="list" | ||||
|           aria-controls="react-select-2-listbox" | ||||
|           aria-expanded="false" | ||||
|           aria-haspopup="true" | ||||
|           aria-owns="react-select-2-listbox" | ||||
|           aria-readonly="true" | ||||
|           class="css-mohuvp-dummyInput-DummyInput" | ||||
|           id="react-select-2-input" | ||||
|   | ||||
| @@ -36,10 +36,8 @@ exports[`components/properties/user not readonly 1`] = ` | ||||
|         > | ||||
|           <input | ||||
|             aria-autocomplete="list" | ||||
|             aria-controls="react-select-3-listbox" | ||||
|             aria-expanded="false" | ||||
|             aria-haspopup="true" | ||||
|             aria-owns="react-select-3-listbox" | ||||
|             autocapitalize="none" | ||||
|             autocomplete="off" | ||||
|             autocorrect="off" | ||||
| @@ -133,11 +131,9 @@ exports[`components/properties/user not readonly not existing user 1`] = ` | ||||
|         > | ||||
|           <input | ||||
|             aria-autocomplete="list" | ||||
|             aria-controls="react-select-2-listbox" | ||||
|             aria-describedby="react-select-2-placeholder" | ||||
|             aria-expanded="false" | ||||
|             aria-haspopup="true" | ||||
|             aria-owns="react-select-2-listbox" | ||||
|             autocapitalize="none" | ||||
|             autocomplete="off" | ||||
|             autocorrect="off" | ||||
|   | ||||
| @@ -79,11 +79,9 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Copy l | ||||
|                   > | ||||
|                     <input | ||||
|                       aria-autocomplete="list" | ||||
|                       aria-controls="react-select-4-listbox" | ||||
|                       aria-describedby="react-select-4-placeholder" | ||||
|                       aria-expanded="false" | ||||
|                       aria-haspopup="true" | ||||
|                       aria-owns="react-select-4-listbox" | ||||
|                       autocapitalize="none" | ||||
|                       autocomplete="off" | ||||
|                       autocorrect="off" | ||||
| @@ -305,11 +303,9 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Copy l | ||||
|                   > | ||||
|                     <input | ||||
|                       aria-autocomplete="list" | ||||
|                       aria-controls="react-select-4-listbox" | ||||
|                       aria-describedby="react-select-4-placeholder" | ||||
|                       aria-expanded="false" | ||||
|                       aria-haspopup="true" | ||||
|                       aria-owns="react-select-4-listbox" | ||||
|                       autocapitalize="none" | ||||
|                       autocomplete="off" | ||||
|                       autocorrect="off" | ||||
| @@ -531,11 +527,9 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Regene | ||||
|                   > | ||||
|                     <input | ||||
|                       aria-autocomplete="list" | ||||
|                       aria-controls="react-select-5-listbox" | ||||
|                       aria-describedby="react-select-5-placeholder" | ||||
|                       aria-expanded="false" | ||||
|                       aria-haspopup="true" | ||||
|                       aria-owns="react-select-5-listbox" | ||||
|                       autocapitalize="none" | ||||
|                       autocomplete="off" | ||||
|                       autocorrect="off" | ||||
| @@ -780,11 +774,9 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Select | ||||
|                   > | ||||
|                     <input | ||||
|                       aria-autocomplete="list" | ||||
|                       aria-controls="react-select-10-listbox" | ||||
|                       aria-describedby="react-select-10-placeholder" | ||||
|                       aria-expanded="false" | ||||
|                       aria-haspopup="true" | ||||
|                       aria-owns="react-select-10-listbox" | ||||
|                       autocapitalize="none" | ||||
|                       autocomplete="off" | ||||
|                       autocorrect="off" | ||||
| @@ -1302,11 +1294,9 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Select | ||||
|                   > | ||||
|                     <input | ||||
|                       aria-autocomplete="list" | ||||
|                       aria-controls="react-select-11-listbox" | ||||
|                       aria-describedby="react-select-11-placeholder" | ||||
|                       aria-expanded="false" | ||||
|                       aria-haspopup="true" | ||||
|                       aria-owns="react-select-11-listbox" | ||||
|                       autocapitalize="none" | ||||
|                       autocomplete="off" | ||||
|                       autocorrect="off" | ||||
| @@ -1824,11 +1814,9 @@ exports[`src/components/shareBoard/shareBoard return shareBoard, and click switc | ||||
|                   > | ||||
|                     <input | ||||
|                       aria-autocomplete="list" | ||||
|                       aria-controls="react-select-6-listbox" | ||||
|                       aria-describedby="react-select-6-placeholder" | ||||
|                       aria-expanded="false" | ||||
|                       aria-haspopup="true" | ||||
|                       aria-owns="react-select-6-listbox" | ||||
|                       autocapitalize="none" | ||||
|                       autocomplete="off" | ||||
|                       autocorrect="off" | ||||
| @@ -2073,11 +2061,9 @@ exports[`src/components/shareBoard/shareBoard return shareBoardComponent and cli | ||||
|                   > | ||||
|                     <input | ||||
|                       aria-autocomplete="list" | ||||
|                       aria-controls="react-select-7-listbox" | ||||
|                       aria-describedby="react-select-7-placeholder" | ||||
|                       aria-expanded="false" | ||||
|                       aria-haspopup="true" | ||||
|                       aria-owns="react-select-7-listbox" | ||||
|                       autocapitalize="none" | ||||
|                       autocomplete="off" | ||||
|                       autocorrect="off" | ||||
| @@ -2322,11 +2308,9 @@ exports[`src/components/shareBoard/shareBoard should match snapshot 1`] = ` | ||||
|                   > | ||||
|                     <input | ||||
|                       aria-autocomplete="list" | ||||
|                       aria-controls="react-select-2-listbox" | ||||
|                       aria-describedby="react-select-2-placeholder" | ||||
|                       aria-expanded="false" | ||||
|                       aria-haspopup="true" | ||||
|                       aria-owns="react-select-2-listbox" | ||||
|                       autocapitalize="none" | ||||
|                       autocomplete="off" | ||||
|                       autocorrect="off" | ||||
| @@ -2548,11 +2532,9 @@ exports[`src/components/shareBoard/shareBoard should match snapshot with sharing | ||||
|                   > | ||||
|                     <input | ||||
|                       aria-autocomplete="list" | ||||
|                       aria-controls="react-select-3-listbox" | ||||
|                       aria-describedby="react-select-3-placeholder" | ||||
|                       aria-expanded="false" | ||||
|                       aria-haspopup="true" | ||||
|                       aria-owns="react-select-3-listbox" | ||||
|                       autocapitalize="none" | ||||
|                       autocomplete="off" | ||||
|                       autocorrect="off" | ||||
| @@ -2774,11 +2756,9 @@ exports[`src/components/shareBoard/shareBoard should match snapshot with sharing | ||||
|                   > | ||||
|                     <input | ||||
|                       aria-autocomplete="list" | ||||
|                       aria-controls="react-select-9-listbox" | ||||
|                       aria-describedby="react-select-9-placeholder" | ||||
|                       aria-expanded="false" | ||||
|                       aria-haspopup="true" | ||||
|                       aria-owns="react-select-9-listbox" | ||||
|                       autocapitalize="none" | ||||
|                       autocomplete="off" | ||||
|                       autocorrect="off" | ||||
| @@ -3000,11 +2980,9 @@ exports[`src/components/shareBoard/shareBoard should match snapshot with sharing | ||||
|                   > | ||||
|                     <input | ||||
|                       aria-autocomplete="list" | ||||
|                       aria-controls="react-select-8-listbox" | ||||
|                       aria-describedby="react-select-8-placeholder" | ||||
|                       aria-expanded="false" | ||||
|                       aria-haspopup="true" | ||||
|                       aria-owns="react-select-8-listbox" | ||||
|                       autocapitalize="none" | ||||
|                       autocomplete="off" | ||||
|                       autocorrect="off" | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. | ||||
| // 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 {ISharing} from './blocks/sharing' | ||||
| import {OctoUtils} from './octoUtils' | ||||
| @@ -556,18 +556,23 @@ class OctoClient { | ||||
|         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 | ||||
|         const readToken = Utils.getReadToken() | ||||
|         if (readToken) { | ||||
|             path += `?read_token=${readToken}` | ||||
|         } | ||||
|         const response = await fetch(this.getBaseURL() + path, {headers: this.headers()}) | ||||
|         if (response.status !== 200) { | ||||
|             return '' | ||||
|         } | ||||
|         let fileInfo: FileInfo = {} | ||||
|  | ||||
|         if (response.status === 200) { | ||||
|             const blob = await response.blob() | ||||
|         return URL.createObjectURL(blob) | ||||
|             fileInfo.url = URL.createObjectURL(blob) | ||||
|         } else if (response.status === 400) { | ||||
|             fileInfo = await this.getJson(response, {}) as FileInfo | ||||
|         } | ||||
|  | ||||
|         return fileInfo | ||||
|     } | ||||
|  | ||||
|     async getTeam(): Promise<Team | null> { | ||||
|   | ||||
| @@ -723,6 +723,26 @@ class Utils { | ||||
|         const newPath = generatePath(match.path, params) | ||||
|         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} | ||||
|   | ||||
							
								
								
									
										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