diff --git a/mattermost-plugin/server/boards/configuration.go b/mattermost-plugin/server/boards/configuration.go
index ddcbeba18..fcd44a5c4 100644
--- a/mattermost-plugin/server/boards/configuration.go
+++ b/mattermost-plugin/server/boards/configuration.go
@@ -107,6 +107,11 @@ func (b *BoardsApp) OnConfigurationChange() error {
showFullName = *mmconfig.PrivacySettings.ShowFullName
}
b.server.Config().ShowFullName = showFullName
+ maxFileSize := int64(0)
+ if mmconfig.FileSettings.MaxFileSize != nil {
+ maxFileSize = *mmconfig.FileSettings.MaxFileSize
+ }
+ b.server.Config().MaxFileSize = maxFileSize
b.server.UpdateAppConfig()
b.wsPluginAdapter.BroadcastConfigChange(*b.server.App().GetClientConfig())
diff --git a/server/api/files.go b/server/api/files.go
index 825ff22f1..4c2c10379 100644
--- a/server/api/files.go
+++ b/server/api/files.go
@@ -5,7 +5,6 @@ import (
"errors"
"io"
"net/http"
- "path/filepath"
"strings"
"time"
@@ -13,7 +12,9 @@ import (
"github.com/gorilla/mux"
"github.com/mattermost/focalboard/server/model"
+
"github.com/mattermost/focalboard/server/services/audit"
+ mmModel "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/shared/mlog"
)
@@ -35,9 +36,19 @@ func FileUploadResponseFromJSON(data io.Reader) (*FileUploadResponse, error) {
return &fileUploadResponse, nil
}
+func FileInfoResponseFromJSON(data io.Reader) (*mmModel.FileInfo, error) {
+ var fileInfo mmModel.FileInfo
+
+ if err := json.NewDecoder(data).Decode(&fileInfo); err != nil {
+ return nil, err
+ }
+ return &fileInfo, nil
+}
+
func (a *API) registerFilesRoutes(r *mux.Router) {
// Files API
r.HandleFunc("/files/teams/{teamID}/{boardID}/{filename}", a.attachSession(a.handleServeFile, false)).Methods("GET")
+ r.HandleFunc("/files/teams/{teamID}/{boardID}/{filename}/info", a.attachSession(a.getFileInfo, false)).Methods("GET")
r.HandleFunc("/teams/{teamID}/{boardID}/files", a.sessionRequired(a.handleUploadFile)).Methods("POST")
}
@@ -108,19 +119,6 @@ func (a *API) handleServeFile(w http.ResponseWriter, r *http.Request) {
auditRec.AddMeta("teamID", board.TeamID)
auditRec.AddMeta("filename", filename)
- contentType := "image/jpg"
-
- fileExtension := strings.ToLower(filepath.Ext(filename))
- if fileExtension == "png" {
- contentType = "image/png"
- }
-
- if fileExtension == "gif" {
- contentType = "image/gif"
- }
-
- w.Header().Set("Content-Type", contentType)
-
fileInfo, err := a.app.GetFileInfo(filename)
if err != nil && !model.IsErrNotFound(err) {
a.errorResponse(w, r, err)
@@ -172,6 +170,80 @@ func (a *API) handleServeFile(w http.ResponseWriter, r *http.Request) {
auditRec.Success()
}
+func (a *API) getFileInfo(w http.ResponseWriter, r *http.Request) {
+ // swagger:operation GET /files/teams/{teamID}/{boardID}/{filename}/info getFile
+ //
+ // Returns the metadata of an uploaded file
+ //
+ // ---
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: teamID
+ // in: path
+ // description: Team ID
+ // required: true
+ // type: string
+ // - name: boardID
+ // in: path
+ // description: Board ID
+ // required: true
+ // type: string
+ // - name: filename
+ // in: path
+ // description: name of the file
+ // required: true
+ // type: string
+ // security:
+ // - BearerAuth: []
+ // responses:
+ // '200':
+ // description: success
+ // '404':
+ // description: file not found
+ // default:
+ // description: internal error
+ // schema:
+ // "$ref": "#/definitions/ErrorResponse"
+
+ vars := mux.Vars(r)
+ boardID := vars["boardID"]
+ teamID := vars["teamID"]
+ filename := vars["filename"]
+ userID := getUserID(r)
+
+ hasValidReadToken := a.hasValidReadTokenForBoard(r, boardID)
+ if userID == "" && !hasValidReadToken {
+ a.errorResponse(w, r, model.NewErrUnauthorized("access denied to board"))
+ return
+ }
+
+ if !hasValidReadToken && !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
+ a.errorResponse(w, r, model.NewErrPermission("access denied to board"))
+ return
+ }
+
+ auditRec := a.makeAuditRecord(r, "getFile", audit.Fail)
+ defer a.audit.LogRecord(audit.LevelRead, auditRec)
+ auditRec.AddMeta("boardID", boardID)
+ auditRec.AddMeta("teamID", teamID)
+ auditRec.AddMeta("filename", filename)
+
+ fileInfo, err := a.app.GetFileInfo(filename)
+ if err != nil && !model.IsErrNotFound(err) {
+ a.errorResponse(w, r, err)
+ return
+ }
+
+ data, err := json.Marshal(fileInfo)
+ if err != nil {
+ a.errorResponse(w, r, err)
+ return
+ }
+
+ jsonBytesResponse(w, http.StatusOK, data)
+}
+
func (a *API) handleUploadFile(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /teams/{teamID}/boards/{boardID}/files uploadFile
//
diff --git a/server/app/clientConfig.go b/server/app/clientConfig.go
index b681512e6..f78a23d59 100644
--- a/server/app/clientConfig.go
+++ b/server/app/clientConfig.go
@@ -11,5 +11,6 @@ func (a *App) GetClientConfig() *model.ClientConfig {
EnablePublicSharedBoards: a.config.EnablePublicSharedBoards,
TeammateNameDisplay: a.config.TeammateNameDisplay,
FeatureFlags: a.config.FeatureFlags,
+ MaxFileSize: a.config.MaxFileSize,
}
}
diff --git a/server/client/client.go b/server/client/client.go
index 4bc4e08af..3a7ff2487 100644
--- a/server/client/client.go
+++ b/server/client/client.go
@@ -11,6 +11,7 @@ import (
"github.com/mattermost/focalboard/server/api"
"github.com/mattermost/focalboard/server/model"
+ mmModel "github.com/mattermost/mattermost-server/v6/model"
)
const (
@@ -823,6 +824,19 @@ func (c *Client) TeamUploadFile(teamID, boardID string, data io.Reader) (*api.Fi
return fileUploadResponse, BuildResponse(r)
}
+func (c *Client) TeamUploadFileInfo(teamID, boardID string, fileName string) (*mmModel.FileInfo, *Response) {
+ r, err := c.DoAPIGet(fmt.Sprintf("/files/teams/%s/%s/%s/info", teamID, boardID, fileName), "")
+ if err != nil {
+ return nil, BuildErrorResponse(r, err)
+ }
+ defer closeBody(r)
+ fileInfoResponse, error := api.FileInfoResponseFromJSON(r.Body)
+ if error != nil {
+ return nil, BuildErrorResponse(r, error)
+ }
+ return fileInfoResponse, BuildResponse(r)
+}
+
func (c *Client) GetSubscriptionsRoute() string {
return "/subscriptions"
}
diff --git a/server/integrationtests/file_test.go b/server/integrationtests/file_test.go
index 3d3e1610f..9be32b9da 100644
--- a/server/integrationtests/file_test.go
+++ b/server/integrationtests/file_test.go
@@ -69,3 +69,20 @@ func TestUploadFile(t *testing.T) {
require.NotNil(t, file.FileID)
})
}
+
+func TestFileInfo(t *testing.T) {
+ const (
+ testTeamID = "team-id"
+ )
+
+ t.Run("Retrieving file info", func(t *testing.T) {
+ th := SetupTestHelper(t).InitBasic()
+ defer th.TearDown()
+ testBoard := th.CreateBoard(testTeamID, model.BoardTypeOpen)
+
+ fileInfo, resp := th.Client.TeamUploadFileInfo(testTeamID, testBoard.ID, "test")
+ th.CheckOK(resp)
+ require.NotNil(t, fileInfo)
+ require.NotNil(t, fileInfo.Id)
+ })
+}
diff --git a/server/model/clientConfig.go b/server/model/clientConfig.go
index 545b329d7..38d8e3cba 100644
--- a/server/model/clientConfig.go
+++ b/server/model/clientConfig.go
@@ -22,4 +22,8 @@ type ClientConfig struct {
// The server feature flags
// required: true
FeatureFlags map[string]string `json:"featureFlags"`
+
+ // Required for file upload to check the size of the file
+ // required: true
+ MaxFileSize int64 `json:"maxFileSize"`
}
diff --git a/server/services/config/config.go b/server/services/config/config.go
index 9d7ffc22f..dbabe0e1e 100644
--- a/server/services/config/config.go
+++ b/server/services/config/config.go
@@ -38,7 +38,7 @@ type Configuration struct {
FilesDriver string `json:"filesdriver" mapstructure:"filesdriver"`
FilesS3Config AmazonS3Config `json:"filess3config" mapstructure:"filess3config"`
FilesPath string `json:"filespath" mapstructure:"filespath"`
- MaxFileSize int64 `json:"maxfilesize" mapstructure:"mafilesize"`
+ MaxFileSize int64 `json:"maxfilesize" mapstructure:"maxfilesize"`
Telemetry bool `json:"telemetry" mapstructure:"telemetry"`
TelemetryID string `json:"telemetryid" mapstructure:"telemetryid"`
PrometheusAddress string `json:"prometheusaddress" mapstructure:"prometheusaddress"`
diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json
index a716d5176..403efc569 100644
--- a/webapp/i18n/en.json
+++ b/webapp/i18n/en.json
@@ -1,5 +1,15 @@
{
"AppBar.Tooltip": "Toggle Linked Boards",
+ "Attachment.Attachment-title": "Attachment",
+ "AttachmentBlock.DeleteAction": "delete",
+ "AttachmentBlock.addElement": "add {type}",
+ "AttachmentBlock.delete": "Attachment Deleted Successfully.",
+ "AttachmentBlock.failed": "Unable to upload the file. Attachment size limit reached.",
+ "AttachmentBlock.upload": "Attachment uploading.",
+ "AttachmentBlock.uploadSuccess": "Attachment uploaded successfull.",
+ "AttachmentElement.delete-confirmation-dialog-button-text": "Delete",
+ "AttachmentElement.download": "Download",
+ "AttachmentElement.upload-percentage": "Uploading...({uploadPercent}%)",
"BoardComponent.add-a-group": "+ Add a group",
"BoardComponent.delete": "Delete",
"BoardComponent.hidden-columns": "Hidden columns",
@@ -71,6 +81,7 @@
"CardBadges.title-checkboxes": "Checkboxes",
"CardBadges.title-comments": "Comments",
"CardBadges.title-description": "This card has a description",
+ "CardDetail.Attach": "Attach",
"CardDetail.Follow": "Follow",
"CardDetail.Following": "Following",
"CardDetail.add-content": "Add content",
@@ -92,6 +103,7 @@
"CardDetailProperty.property-deleted": "Deleted {propertyName} successfully!",
"CardDetailProperty.property-name-change-subtext": "type from \"{oldPropType}\" to \"{newPropType}\"",
"CardDetial.limited-link": "Learn more about our plans.",
+ "CardDialog.delete-confirmation-dialog-attachment": "Confirm Attachment delete!",
"CardDialog.delete-confirmation-dialog-button-text": "Delete",
"CardDialog.delete-confirmation-dialog-heading": "Confirm card delete!",
"CardDialog.editing-template": "You're editing a template.",
@@ -119,6 +131,7 @@
"ContentBlock.editText": "Edit text...",
"ContentBlock.image": "image",
"ContentBlock.insertAbove": "Insert above",
+ "ContentBlock.moveBlock": "move card content",
"ContentBlock.moveDown": "Move down",
"ContentBlock.moveUp": "Move up",
"ContentBlock.text": "text",
diff --git a/webapp/src/blocks/attachmentBlock.tsx b/webapp/src/blocks/attachmentBlock.tsx
new file mode 100644
index 000000000..bf0568505
--- /dev/null
+++ b/webapp/src/blocks/attachmentBlock.tsx
@@ -0,0 +1,28 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+import {Block, createBlock} from './block'
+
+type AttachmentBlockFields = {
+ attachmentId: string
+}
+
+type AttachmentBlock = Block & {
+ type: 'attachment'
+ fields: AttachmentBlockFields
+ isUploading: boolean
+ uploadingPercent: number
+}
+
+function createAttachmentBlock(block?: Block): AttachmentBlock {
+ return {
+ ...createBlock(block),
+ type: 'attachment',
+ fields: {
+ attachmentId: block?.fields.attachmentId || '',
+ },
+ isUploading: false,
+ uploadingPercent: 0,
+ }
+}
+
+export {AttachmentBlock, createAttachmentBlock}
diff --git a/webapp/src/blocks/block.ts b/webapp/src/blocks/block.ts
index 0fac16f1a..de1bb908a 100644
--- a/webapp/src/blocks/block.ts
+++ b/webapp/src/blocks/block.ts
@@ -8,7 +8,7 @@ import {Utils} from '../utils'
const contentBlockTypes = ['text', 'image', 'divider', 'checkbox', 'h1', 'h2', 'h3', 'list-item', 'attachment', 'quote', 'video'] as const
// ToDo: remove type board
-const blockTypes = [...contentBlockTypes, 'board', 'view', 'card', 'comment', 'unknown'] as const
+const blockTypes = [...contentBlockTypes, 'board', 'view', 'card', 'comment', 'attachment', 'unknown'] as const
type ContentBlockTypes = typeof contentBlockTypes[number]
type BlockTypes = typeof blockTypes[number]
diff --git a/webapp/src/components/__snapshots__/cardDialog.test.tsx.snap b/webapp/src/components/__snapshots__/cardDialog.test.tsx.snap
index 4d2bfa022..8b46e1a21 100644
--- a/webapp/src/components/__snapshots__/cardDialog.test.tsx.snap
+++ b/webapp/src/components/__snapshots__/cardDialog.test.tsx.snap
@@ -29,6 +29,16 @@ exports[`components/cardDialog already following card 1`] = `
class="toolbar--right"
>
+