1
0
mirror of https://github.com/mattermost/focalboard.git synced 2024-12-24 13:43:12 +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:
Harshil Sharma 2022-06-13 13:35:42 +05:30 committed by GitHub
parent 04223a3f76
commit f3faf39eaa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 2820 additions and 1774 deletions

View File

@ -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)

View File

@ -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) {

View File

@ -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)
})
}

View File

@ -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()
}

View File

@ -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()

View 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
}

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS {{.prefix}}file_info;

View File

@ -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}};

View File

@ -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)

View File

@ -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) })
}

View File

@ -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

View 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

File diff suppressed because it is too large Load Diff

View File

@ -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}

View File

@ -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"

View File

@ -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"

View File

@ -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

View File

@ -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>
`;

View 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;
}
}

View File

@ -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()
})
})

View 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

View File

@ -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()
})
})

View File

@ -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

View File

@ -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 = []

View File

@ -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'

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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()
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> {

View File

@ -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}

View 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'
/>
)
}