Improve attachment uploading & constraints
This commit is contained in:
+11
-3
@@ -122,6 +122,9 @@ type (
|
||||
hasResource bool
|
||||
Resource *types.Record
|
||||
|
||||
hasFieldName bool
|
||||
FieldName string
|
||||
|
||||
hasContent bool
|
||||
Content interface{}
|
||||
contentString string
|
||||
@@ -162,6 +165,10 @@ func (h attachmentHandler) Create() *atypes.Function {
|
||||
Name: "resource",
|
||||
Types: []string{"ComposeRecord"}, Required: true,
|
||||
},
|
||||
{
|
||||
Name: "fieldName",
|
||||
Types: []string{"String"},
|
||||
},
|
||||
{
|
||||
Name: "content",
|
||||
Types: []string{"String", "Reader", "Bytes"}, Required: true,
|
||||
@@ -179,9 +186,10 @@ func (h attachmentHandler) Create() *atypes.Function {
|
||||
Handler: func(ctx context.Context, in *expr.Vars) (out *expr.Vars, err error) {
|
||||
var (
|
||||
args = &attachmentCreateArgs{
|
||||
hasName: in.Has("name"),
|
||||
hasResource: in.Has("resource"),
|
||||
hasContent: in.Has("content"),
|
||||
hasName: in.Has("name"),
|
||||
hasResource: in.Has("resource"),
|
||||
hasFieldName: in.Has("fieldName"),
|
||||
hasContent: in.Has("content"),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
type (
|
||||
attachmentService interface {
|
||||
FindByID(ctx context.Context, namespaceID, attachmentID uint64) (*types.Attachment, error)
|
||||
CreateRecordAttachment(ctx context.Context, namespaceID uint64, name string, size int64, fh io.ReadSeeker, moduleID, recordID uint64) (att *types.Attachment, err error)
|
||||
CreateRecordAttachment(ctx context.Context, namespaceID uint64, name string, size int64, fh io.ReadSeeker, moduleID, recordID uint64, fieldName string) (att *types.Attachment, err error)
|
||||
DeleteByID(ctx context.Context, namespaceID uint64, attachmentID uint64) error
|
||||
OpenOriginal(att *types.Attachment) (io.ReadSeeker, error)
|
||||
OpenPreview(att *types.Attachment) (io.ReadSeeker, error)
|
||||
@@ -113,6 +113,7 @@ func (h attachmentHandler) create(ctx context.Context, args *attachmentCreateArg
|
||||
fh,
|
||||
args.Resource.ModuleID,
|
||||
args.Resource.ID,
|
||||
args.FieldName,
|
||||
)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown resource")
|
||||
|
||||
@@ -39,6 +39,10 @@ functions:
|
||||
types:
|
||||
- { wf: ComposeRecord }
|
||||
|
||||
fieldName:
|
||||
types:
|
||||
- { wf: String }
|
||||
|
||||
content:
|
||||
required: true
|
||||
types:
|
||||
|
||||
@@ -269,6 +269,7 @@ func (ctrl *Record) Upload(ctx context.Context, r *request.RecordUpload) (interf
|
||||
file,
|
||||
r.ModuleID,
|
||||
r.RecordID,
|
||||
r.FieldName,
|
||||
)
|
||||
|
||||
return makeAttachmentPayload(ctx, a, err)
|
||||
|
||||
+140
-15
@@ -8,21 +8,30 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/compose/types"
|
||||
"github.com/cortezaproject/corteza-server/pkg/actionlog"
|
||||
"github.com/cortezaproject/corteza-server/pkg/auth"
|
||||
"github.com/cortezaproject/corteza-server/pkg/errors"
|
||||
"github.com/cortezaproject/corteza-server/pkg/objstore"
|
||||
"github.com/cortezaproject/corteza-server/store"
|
||||
systemService "github.com/cortezaproject/corteza-server/system/service"
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/edwvee/exiffix"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
attachmentPreviewMaxWidth = 320
|
||||
attachmentPreviewMaxHeight = 180
|
||||
|
||||
// using base 10, it will be less confusing for the non-techie users
|
||||
megabyte = 1_000_000
|
||||
)
|
||||
|
||||
var (
|
||||
reMimeType = regexp.MustCompile(`\w+/[-.\w]+(?:\+[-.\w]+)?`)
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -48,7 +57,7 @@ type (
|
||||
FindByID(ctx context.Context, namespaceID, attachmentID uint64) (*types.Attachment, error)
|
||||
Find(ctx context.Context, filter types.AttachmentFilter) (types.AttachmentSet, types.AttachmentFilter, error)
|
||||
CreatePageAttachment(ctx context.Context, namespaceID uint64, name string, size int64, fh io.ReadSeeker, pageID uint64) (*types.Attachment, error)
|
||||
CreateRecordAttachment(ctx context.Context, namespaceID uint64, name string, size int64, fh io.ReadSeeker, moduleID, recordID uint64) (*types.Attachment, error)
|
||||
CreateRecordAttachment(ctx context.Context, namespaceID uint64, name string, size int64, fh io.ReadSeeker, moduleID, recordID uint64, fieldName string) (*types.Attachment, error)
|
||||
CreateNamespaceAttachment(ctx context.Context, name string, size int64, fh io.ReadSeeker) (*types.Attachment, error)
|
||||
OpenOriginal(att *types.Attachment) (io.ReadSeeker, error)
|
||||
OpenPreview(att *types.Attachment) (io.ReadSeeker, error)
|
||||
@@ -246,8 +255,12 @@ func (svc attachment) CreatePageAttachment(ctx context.Context, namespaceID uint
|
||||
}
|
||||
)
|
||||
|
||||
err = func() error {
|
||||
ns, p, err = loadPage(ctx, svc.store, namespaceID, pageID)
|
||||
err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
|
||||
if size == 0 {
|
||||
return AttachmentErrNotAllowedToCreateEmptyAttachment()
|
||||
}
|
||||
|
||||
ns, p, err = loadPage(ctx, s, namespaceID, pageID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -259,20 +272,43 @@ func (svc attachment) CreatePageAttachment(ctx context.Context, namespaceID uint
|
||||
return AttachmentErrNotAllowedToUpdatePage()
|
||||
}
|
||||
|
||||
{
|
||||
// Verify size and type of the uploaded page attachment
|
||||
// Max size & allowed mime-types are pulled from the current settings
|
||||
var (
|
||||
maxSize = int64(systemService.CurrentSettings.Compose.Page.Attachments.MaxSize) * megabyte
|
||||
allowedTypes = systemService.CurrentSettings.Compose.Page.Attachments.Mimetypes
|
||||
mimeType string
|
||||
)
|
||||
|
||||
if maxSize > 0 && maxSize < size {
|
||||
return AttachmentErrTooLarge().Apply(
|
||||
errors.Meta("size", size),
|
||||
errors.Meta("maxSize", maxSize),
|
||||
)
|
||||
}
|
||||
|
||||
if mimeType, err = svc.extractMimetype(fh); err != nil {
|
||||
return err
|
||||
} else if !svc.checkMimeType(mimeType, allowedTypes...) {
|
||||
return AttachmentErrNotAllowedToUploadThisType()
|
||||
}
|
||||
}
|
||||
|
||||
att = &types.Attachment{
|
||||
NamespaceID: namespaceID,
|
||||
Name: strings.TrimSpace(name),
|
||||
Kind: types.PageAttachment,
|
||||
}
|
||||
|
||||
return svc.create(ctx, name, size, fh, att)
|
||||
}()
|
||||
return svc.create(ctx, s, name, size, fh, att)
|
||||
})
|
||||
|
||||
return att, svc.recordAction(ctx, aProps, AttachmentActionCreate, err)
|
||||
|
||||
}
|
||||
|
||||
func (svc attachment) CreateRecordAttachment(ctx context.Context, namespaceID uint64, name string, size int64, fh io.ReadSeeker, moduleID, recordID uint64) (att *types.Attachment, err error) {
|
||||
func (svc attachment) CreateRecordAttachment(ctx context.Context, namespaceID uint64, name string, size int64, fh io.ReadSeeker, moduleID, recordID uint64, fieldName string) (att *types.Attachment, err error) {
|
||||
var (
|
||||
ns *types.Namespace
|
||||
m *types.Module
|
||||
@@ -286,6 +322,10 @@ func (svc attachment) CreateRecordAttachment(ctx context.Context, namespaceID ui
|
||||
)
|
||||
|
||||
err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
|
||||
if size == 0 {
|
||||
return AttachmentErrNotAllowedToCreateEmptyAttachment()
|
||||
}
|
||||
|
||||
ns, m, err = loadModuleWithNamespace(ctx, s, namespaceID, moduleID)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -320,13 +360,50 @@ func (svc attachment) CreateRecordAttachment(ctx context.Context, namespaceID ui
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// Verify size and type of the uploaded record attachment
|
||||
// Max size & allowed mime-types are pulled from the current settings
|
||||
var (
|
||||
maxSize = int64(systemService.CurrentSettings.Compose.Record.Attachments.MaxSize) * megabyte
|
||||
allowedTypes = systemService.CurrentSettings.Compose.Record.Attachments.Mimetypes
|
||||
mimeType string
|
||||
)
|
||||
|
||||
f := m.Fields.FindByName(fieldName)
|
||||
if f == nil || f.Kind != "File" {
|
||||
return AttachmentErrInvalidModuleField().Apply(
|
||||
errors.Meta("fieldName", fieldName),
|
||||
)
|
||||
}
|
||||
|
||||
if aux := f.Options.Int64("maxSize"); aux > 0 {
|
||||
maxSize = aux * megabyte
|
||||
}
|
||||
if aux := f.Options.String("mimetypes"); len(aux) > 0 {
|
||||
allowedTypes = strings.Split(aux, ",")
|
||||
}
|
||||
|
||||
if maxSize > 0 && maxSize < size {
|
||||
return AttachmentErrTooLarge().Apply(
|
||||
errors.Meta("size", size),
|
||||
errors.Meta("maxSize", maxSize),
|
||||
)
|
||||
}
|
||||
|
||||
if mimeType, err = svc.extractMimetype(fh); err != nil {
|
||||
return err
|
||||
} else if !svc.checkMimeType(mimeType, allowedTypes...) {
|
||||
return AttachmentErrNotAllowedToUploadThisType().Apply(errors.Meta("mimetype", mimeType))
|
||||
}
|
||||
}
|
||||
|
||||
att = &types.Attachment{
|
||||
NamespaceID: namespaceID,
|
||||
Name: strings.TrimSpace(name),
|
||||
Kind: types.RecordAttachment,
|
||||
}
|
||||
|
||||
return svc.create(ctx, name, size, fh, att)
|
||||
return svc.create(ctx, s, name, size, fh, att)
|
||||
})
|
||||
|
||||
return att, svc.recordAction(ctx, aProps, AttachmentActionCreate, err)
|
||||
@@ -338,23 +415,50 @@ func (svc attachment) CreateNamespaceAttachment(ctx context.Context, name string
|
||||
)
|
||||
|
||||
err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
|
||||
if size == 0 {
|
||||
return AttachmentErrNotAllowedToCreateEmptyAttachment()
|
||||
}
|
||||
|
||||
if !svc.ac.CanCreateNamespace(ctx) {
|
||||
return AttachmentErrNotAllowedToUpdateNamespace()
|
||||
}
|
||||
|
||||
{
|
||||
// Verify file size and
|
||||
var (
|
||||
// use max-file-size from page attachments for now
|
||||
maxSize = int64(systemService.CurrentSettings.Compose.Page.Attachments.MaxSize) * megabyte
|
||||
mimeType string
|
||||
)
|
||||
|
||||
if maxSize > 0 && maxSize < size {
|
||||
return AttachmentErrTooLarge().Apply(
|
||||
errors.Meta("size", size),
|
||||
errors.Meta("maxSize", maxSize),
|
||||
)
|
||||
}
|
||||
|
||||
if mimeType, err = svc.extractMimetype(fh); err != nil {
|
||||
return err
|
||||
} else if !svc.checkMimeType(mimeType, "image/png", "image/gif", "image/jpeg") {
|
||||
return AttachmentErrNotAllowedToUploadThisType()
|
||||
}
|
||||
}
|
||||
|
||||
att = &types.Attachment{
|
||||
Name: strings.TrimSpace(name),
|
||||
Kind: types.NamespaceAttachment,
|
||||
}
|
||||
|
||||
return svc.create(ctx, name, size, fh, att)
|
||||
// @todo limit upload on image/* only!
|
||||
|
||||
return svc.create(ctx, s, name, size, fh, att)
|
||||
})
|
||||
|
||||
return att, svc.recordAction(ctx, aProps, AttachmentActionCreate, err)
|
||||
}
|
||||
|
||||
func (svc attachment) create(ctx context.Context, name string, size int64, fh io.ReadSeeker, att *types.Attachment) (err error) {
|
||||
func (svc attachment) create(ctx context.Context, s store.ComposeAttachments, name string, size int64, fh io.ReadSeeker, att *types.Attachment) (err error) {
|
||||
var (
|
||||
aProps = &attachmentActionProps{}
|
||||
)
|
||||
@@ -368,7 +472,7 @@ func (svc attachment) create(ctx context.Context, name string, size int64, fh io
|
||||
}
|
||||
|
||||
if svc.objects == nil {
|
||||
return errors.New("cannot create attachment: store handler not set")
|
||||
return errors.Internal("cannot create attachment: store handler not set")
|
||||
}
|
||||
|
||||
if size == 0 {
|
||||
@@ -399,7 +503,7 @@ func (svc attachment) create(ctx context.Context, name string, size int64, fh io
|
||||
return AttachmentErrFailedToProcessImage(aProps).Wrap(err)
|
||||
}
|
||||
|
||||
if err = store.CreateComposeAttachment(ctx, svc.store, att); err != nil {
|
||||
if err = store.CreateComposeAttachment(ctx, s, att); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -451,7 +555,7 @@ func (svc attachment) processImage(original io.ReadSeeker, att *types.Attachment
|
||||
}
|
||||
|
||||
if format, err = imaging.FormatFromExtension(att.Meta.Original.Extension); err != nil {
|
||||
return errors.Wrapf(err, "Could not get format from extension '%s'", att.Meta.Original.Extension)
|
||||
return errors.Internal("could not get format from extension '%s'", att.Meta.Original.Extension).Wrap(err)
|
||||
}
|
||||
|
||||
previewFormat = format
|
||||
@@ -472,7 +576,7 @@ func (svc attachment) processImage(original io.ReadSeeker, att *types.Attachment
|
||||
// Use first image for the preview
|
||||
preview = cfg.Image[0]
|
||||
} else {
|
||||
return errors.Wrapf(err, "Could not decode gif config")
|
||||
return errors.Internal("Could not decode gif config").Wrap(err)
|
||||
}
|
||||
|
||||
} else {
|
||||
@@ -487,7 +591,7 @@ func (svc attachment) processImage(original io.ReadSeeker, att *types.Attachment
|
||||
// other cases are handled here
|
||||
if preview == nil {
|
||||
if preview, err = imaging.Decode(original); err != nil {
|
||||
return errors.Wrapf(err, "Could not decode original image")
|
||||
return errors.Internal("Could not decode original image").Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -521,4 +625,25 @@ func (svc attachment) processImage(original io.ReadSeeker, att *types.Attachment
|
||||
return svc.objects.Save(att.PreviewUrl, buf)
|
||||
}
|
||||
|
||||
func (attachment) checkMimeType(test string, vv ...string) bool {
|
||||
if len(vv) == 0 {
|
||||
// return true if there are no type constraints to check against
|
||||
return true
|
||||
}
|
||||
|
||||
for _, v := range vv {
|
||||
v = strings.TrimSpace(v)
|
||||
|
||||
if !reMimeType.MatchString(v) {
|
||||
continue
|
||||
}
|
||||
|
||||
if v == test {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
var _ AttachmentService = &attachment{}
|
||||
|
||||
+109
-2
@@ -11,12 +11,13 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/compose/types"
|
||||
"github.com/cortezaproject/corteza-server/pkg/actionlog"
|
||||
"github.com/cortezaproject/corteza-server/pkg/errors"
|
||||
"github.com/cortezaproject/corteza-server/pkg/locale"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -781,6 +782,40 @@ func AttachmentErrInvalidModuleID(mm ...*attachmentActionProps) *errors.Error {
|
||||
return e
|
||||
}
|
||||
|
||||
// AttachmentErrInvalidModuleField returns "compose:attachment.invalidModuleField" as *errors.Error
|
||||
//
|
||||
//
|
||||
// This function is auto-generated.
|
||||
//
|
||||
func AttachmentErrInvalidModuleField(mm ...*attachmentActionProps) *errors.Error {
|
||||
var p = &attachmentActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("invalid module field", nil),
|
||||
|
||||
errors.Meta("type", "invalidModuleField"),
|
||||
errors.Meta("resource", "compose:attachment"),
|
||||
|
||||
errors.Meta(attachmentPropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "compose"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "attachment.errors.invalidModuleField"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// AttachmentErrInvalidPageID returns "compose:attachment.invalidPageID" as *errors.Error
|
||||
//
|
||||
//
|
||||
@@ -849,6 +884,78 @@ func AttachmentErrInvalidRecordID(mm ...*attachmentActionProps) *errors.Error {
|
||||
return e
|
||||
}
|
||||
|
||||
// AttachmentErrTooLarge returns "compose:attachment.tooLarge" as *errors.Error
|
||||
//
|
||||
//
|
||||
// This function is auto-generated.
|
||||
//
|
||||
func AttachmentErrTooLarge(mm ...*attachmentActionProps) *errors.Error {
|
||||
var p = &attachmentActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("file too large", nil),
|
||||
|
||||
errors.Meta("type", "tooLarge"),
|
||||
errors.Meta("resource", "compose:attachment"),
|
||||
|
||||
// action log entry; no formatting, it will be applied inside recordAction fn.
|
||||
errors.Meta(attachmentLogMetaKey{}, "could not upload this file, file size too large"),
|
||||
errors.Meta(attachmentPropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "compose"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "attachment.errors.tooLarge"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// AttachmentErrNotAllowedToUploadThisType returns "compose:attachment.notAllowedToUploadThisType" as *errors.Error
|
||||
//
|
||||
//
|
||||
// This function is auto-generated.
|
||||
//
|
||||
func AttachmentErrNotAllowedToUploadThisType(mm ...*attachmentActionProps) *errors.Error {
|
||||
var p = &attachmentActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("file type not allowed", nil),
|
||||
|
||||
errors.Meta("type", "notAllowedToUploadThisType"),
|
||||
errors.Meta("resource", "compose:attachment"),
|
||||
|
||||
// action log entry; no formatting, it will be applied inside recordAction fn.
|
||||
errors.Meta(attachmentLogMetaKey{}, "could not upload this file, file type nto allowed"),
|
||||
errors.Meta(attachmentPropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "compose"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "attachment.errors.notAllowedToUploadThisType"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// AttachmentErrNotAllowedToListAttachments returns "compose:attachment.notAllowedToListAttachments" as *errors.Error
|
||||
//
|
||||
//
|
||||
|
||||
@@ -85,6 +85,10 @@ errors:
|
||||
message: "invalid module ID"
|
||||
severity: warning
|
||||
|
||||
- error: invalidModuleField
|
||||
message: "invalid module field"
|
||||
severity: warning
|
||||
|
||||
- error: invalidPageID
|
||||
message: "invalid page ID"
|
||||
severity: warning
|
||||
@@ -93,6 +97,14 @@ errors:
|
||||
message: "invalid record ID"
|
||||
severity: warning
|
||||
|
||||
- error: tooLarge
|
||||
message: "file too large"
|
||||
log: "could not upload this file, file size too large"
|
||||
|
||||
- error: notAllowedToUploadThisType
|
||||
message: "file type not allowed"
|
||||
log: "could not upload this file, file type not allowed"
|
||||
|
||||
- error: notAllowedToListAttachments
|
||||
message: "not allowed to list attachments"
|
||||
log: "could not list attachments; insufficient permissions"
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package compose
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -12,6 +14,7 @@ import (
|
||||
"github.com/cortezaproject/corteza-server/compose/types"
|
||||
"github.com/cortezaproject/corteza-server/pkg/id"
|
||||
"github.com/cortezaproject/corteza-server/store"
|
||||
systemService "github.com/cortezaproject/corteza-server/system/service"
|
||||
"github.com/cortezaproject/corteza-server/tests/helpers"
|
||||
"github.com/steinfletcher/apitest-jsonpath"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -343,6 +346,102 @@ func TestPageTreeRead(t *testing.T) {
|
||||
End()
|
||||
}
|
||||
|
||||
func TestPageAttachment(t *testing.T) {
|
||||
h := newHelper(t)
|
||||
h.clearPages()
|
||||
|
||||
ns := h.makeNamespace("page attachment testing namespace")
|
||||
page := h.repoMakePage(ns, "some-page")
|
||||
|
||||
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read")
|
||||
helpers.AllowMe(h, types.PageRbacResource(0, 0), "read", "update")
|
||||
|
||||
xxlBlob := bytes.Repeat([]byte("0"), 1_000_001)
|
||||
|
||||
testImgFh, err := os.ReadFile("./testdata/test.png")
|
||||
h.noError(err)
|
||||
|
||||
defer func() {
|
||||
// reset settings after we're done
|
||||
systemService.CurrentSettings.Compose.Page.Attachments.MaxSize = 0
|
||||
systemService.CurrentSettings.Compose.Page.Attachments.Mimetypes = nil
|
||||
}()
|
||||
|
||||
// one megabyte limit
|
||||
systemService.CurrentSettings.Compose.Page.Attachments.MaxSize = 1
|
||||
systemService.CurrentSettings.Compose.Page.Attachments.Mimetypes = []string{
|
||||
"application/octet-stream",
|
||||
}
|
||||
|
||||
cc := []struct {
|
||||
name string
|
||||
file []byte
|
||||
fname string
|
||||
mtype string
|
||||
form map[string]string
|
||||
test func(*http.Response, *http.Request) error
|
||||
}{
|
||||
{
|
||||
"empty file",
|
||||
[]byte(""),
|
||||
"empty",
|
||||
"plain/text",
|
||||
map[string]string{},
|
||||
helpers.AssertError("attachment.errors.notAllowedToCreateEmptyAttachment"),
|
||||
},
|
||||
{
|
||||
"no file",
|
||||
nil,
|
||||
"empty",
|
||||
"plain/text",
|
||||
map[string]string{},
|
||||
helpers.AssertError("attachment.errors.notAllowedToCreateEmptyAttachment"),
|
||||
},
|
||||
{
|
||||
"valid upload, no constraints",
|
||||
[]byte("."),
|
||||
"dot",
|
||||
"plain/text",
|
||||
map[string]string{},
|
||||
helpers.AssertNoErrors,
|
||||
},
|
||||
{
|
||||
"global max size - over sized",
|
||||
xxlBlob,
|
||||
"numbers",
|
||||
"plain/text",
|
||||
map[string]string{},
|
||||
helpers.AssertError("attachment.errors.tooLarge"),
|
||||
},
|
||||
{
|
||||
"global mimetype - invalid",
|
||||
testImgFh,
|
||||
"numbers.gif",
|
||||
"image/gif",
|
||||
map[string]string{},
|
||||
helpers.AssertError("attachment.errors.notAllowedToUploadThisType"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cc {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
h.t = t
|
||||
|
||||
helpers.InitFileUpload(t, h.apiInit(),
|
||||
fmt.Sprintf("/namespace/%d/page/%d/attachment", page.NamespaceID, page.ID),
|
||||
c.form,
|
||||
c.file,
|
||||
c.fname,
|
||||
c.mtype,
|
||||
).
|
||||
Status(http.StatusOK).
|
||||
Assert(c.test).
|
||||
End()
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPageLabels(t *testing.T) {
|
||||
h := newHelper(t)
|
||||
h.clearPages()
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -17,6 +18,7 @@ import (
|
||||
"github.com/cortezaproject/corteza-server/compose/types"
|
||||
"github.com/cortezaproject/corteza-server/pkg/id"
|
||||
"github.com/cortezaproject/corteza-server/store"
|
||||
systemService "github.com/cortezaproject/corteza-server/system/service"
|
||||
"github.com/cortezaproject/corteza-server/tests/helpers"
|
||||
"github.com/steinfletcher/apitest"
|
||||
jsonpath "github.com/steinfletcher/apitest-jsonpath"
|
||||
@@ -763,6 +765,172 @@ func TestRecordDelete(t *testing.T) {
|
||||
h.a.NotNil(r.DeletedAt)
|
||||
}
|
||||
|
||||
func TestRecordAttachment(t *testing.T) {
|
||||
h := newHelper(t)
|
||||
h.clearRecords()
|
||||
|
||||
namespace := h.makeNamespace("record attachment testing namespace")
|
||||
|
||||
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read")
|
||||
helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "read", "record.create")
|
||||
helpers.AllowMe(h, types.RecordRbacResource(0, 0, 0), "read")
|
||||
helpers.AllowMe(h, types.ModuleFieldRbacResource(0, 0, 0), "record.value.read", "record.value.update")
|
||||
|
||||
const maxSizeLimit = 1
|
||||
|
||||
module := h.makeModule(
|
||||
namespace,
|
||||
"module",
|
||||
&types.ModuleField{Name: "no_constraints", Kind: "File"},
|
||||
&types.ModuleField{
|
||||
Name: "max_size",
|
||||
Kind: "File",
|
||||
Options: types.ModuleFieldOptions{"maxSize": maxSizeLimit},
|
||||
},
|
||||
&types.ModuleField{
|
||||
Name: "img_only",
|
||||
Kind: "File",
|
||||
Options: types.ModuleFieldOptions{
|
||||
"maxSize": maxSizeLimit,
|
||||
"mimetypes": "image/gif, image/png, image/jpeg",
|
||||
},
|
||||
},
|
||||
&types.ModuleField{Name: "str", Kind: "String"},
|
||||
)
|
||||
|
||||
xxlBlob := bytes.Repeat([]byte("0"), maxSizeLimit*1_000_000+1)
|
||||
|
||||
testImgFh, err := os.ReadFile("./testdata/test.png")
|
||||
h.noError(err)
|
||||
|
||||
defer func() {
|
||||
// reset settings after we're done
|
||||
systemService.CurrentSettings.Compose.Record.Attachments.MaxSize = 0
|
||||
systemService.CurrentSettings.Compose.Record.Attachments.Mimetypes = nil
|
||||
}()
|
||||
|
||||
systemService.CurrentSettings.Compose.Record.Attachments.MaxSize = maxSizeLimit
|
||||
systemService.CurrentSettings.Compose.Record.Attachments.Mimetypes = []string{
|
||||
"application/octet-stream",
|
||||
}
|
||||
|
||||
cc := []struct {
|
||||
name string
|
||||
file []byte
|
||||
fname string
|
||||
mtype string
|
||||
form map[string]string
|
||||
test func(*http.Response, *http.Request) error
|
||||
}{
|
||||
{
|
||||
"empty file",
|
||||
[]byte(""),
|
||||
"empty",
|
||||
"plain/text",
|
||||
map[string]string{"fieldName": "no_constraints"},
|
||||
helpers.AssertError("attachment.errors.notAllowedToCreateEmptyAttachment"),
|
||||
},
|
||||
{
|
||||
"no file",
|
||||
nil,
|
||||
"empty",
|
||||
"plain/text",
|
||||
map[string]string{"fieldName": "no_constraints"},
|
||||
helpers.AssertError("attachment.errors.notAllowedToCreateEmptyAttachment"),
|
||||
},
|
||||
{
|
||||
"no field",
|
||||
[]byte("."),
|
||||
"dot",
|
||||
"plain/text",
|
||||
nil,
|
||||
helpers.AssertError("attachment.errors.invalidModuleField"),
|
||||
},
|
||||
{
|
||||
"invalid field",
|
||||
[]byte("."),
|
||||
"dot",
|
||||
"plain/text",
|
||||
map[string]string{"fieldName": "str"},
|
||||
helpers.AssertError("attachment.errors.invalidModuleField"),
|
||||
},
|
||||
{
|
||||
"valid upload, no constraints",
|
||||
[]byte("."),
|
||||
"dot",
|
||||
"plain/text",
|
||||
map[string]string{"fieldName": "no_constraints"},
|
||||
helpers.AssertNoErrors,
|
||||
},
|
||||
{
|
||||
"global max size - over sized",
|
||||
xxlBlob,
|
||||
"numbers",
|
||||
"plain/text",
|
||||
map[string]string{"fieldName": "no_constraints"},
|
||||
helpers.AssertError("attachment.errors.tooLarge"),
|
||||
},
|
||||
{
|
||||
"field max size - ok",
|
||||
[]byte("12345"),
|
||||
"numbers",
|
||||
"plain/text",
|
||||
map[string]string{"fieldName": "max_size"},
|
||||
helpers.AssertNoErrors,
|
||||
},
|
||||
{
|
||||
"field max size - over sized",
|
||||
xxlBlob,
|
||||
"numbers",
|
||||
"plain/text",
|
||||
map[string]string{"fieldName": "max_size"},
|
||||
helpers.AssertError("attachment.errors.tooLarge"),
|
||||
},
|
||||
{
|
||||
"global mimetype - invalid",
|
||||
testImgFh,
|
||||
"numbers.gif",
|
||||
"image/gif",
|
||||
map[string]string{"fieldName": "no_constraints"},
|
||||
helpers.AssertError("attachment.errors.notAllowedToUploadThisType"),
|
||||
},
|
||||
{
|
||||
"field mimetype - ok",
|
||||
testImgFh,
|
||||
"image.png",
|
||||
"image/gif",
|
||||
map[string]string{"fieldName": "img_only"},
|
||||
helpers.AssertNoErrors,
|
||||
},
|
||||
{
|
||||
"field mimetype - invalid",
|
||||
testImgFh,
|
||||
"image.png",
|
||||
"image/gif",
|
||||
map[string]string{"fieldName": "img_only"},
|
||||
helpers.AssertNoErrors,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cc {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
h.t = t
|
||||
|
||||
helpers.InitFileUpload(t, h.apiInit(),
|
||||
fmt.Sprintf("/namespace/%d/module/%d/record/attachment", module.NamespaceID, module.ID),
|
||||
c.form,
|
||||
c.file,
|
||||
c.fname,
|
||||
c.mtype,
|
||||
).
|
||||
Status(http.StatusOK).
|
||||
Assert(c.test).
|
||||
End()
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordExport(t *testing.T) {
|
||||
h := newHelper(t)
|
||||
h.clearRecords()
|
||||
|
||||
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1,40 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"net/textproto"
|
||||
"testing"
|
||||
|
||||
"github.com/steinfletcher/apitest"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func InitFileUpload(t *testing.T, apiTest *apitest.APITest, endpoint string, form map[string]string, file []byte, name, mimetype string) *apitest.Response {
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
|
||||
for k, v := range form {
|
||||
require.NoError(t, writer.WriteField(k, v))
|
||||
}
|
||||
|
||||
header := make(textproto.MIMEHeader)
|
||||
header.Set("Content-Disposition",
|
||||
fmt.Sprintf(`form-data; name="%s"; filename="%s"`,
|
||||
"upload", name))
|
||||
header.Set("Content-Type", mimetype)
|
||||
part, err := writer.CreatePart(header)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = part.Write(file)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, writer.Close())
|
||||
|
||||
return apiTest.
|
||||
Post(endpoint).
|
||||
Body(body.String()).
|
||||
Header("Content-Type", writer.FormDataContentType()).
|
||||
Header("Accept", "application/json").
|
||||
Expect(t)
|
||||
}
|
||||
@@ -6,4 +6,4 @@ modules:
|
||||
mod1:
|
||||
name: Module#1
|
||||
fields:
|
||||
f1: { label: 'Field1' }
|
||||
f1: { label: 'Field1', kind: 'File' }
|
||||
|
||||
@@ -27,9 +27,10 @@ workflows:
|
||||
kind: function
|
||||
ref: attachmentCreate
|
||||
arguments:
|
||||
- { target: content, type: String, expr: "base64blackGif" }
|
||||
- { target: name, type: String, value: "black-from-base64-string.gif" }
|
||||
- { target: resource, type: ComposeRecord, expr: "attachable" }
|
||||
- { target: content, type: String, expr: "base64blackGif" }
|
||||
- { target: name, type: String, value: "black-from-base64-string.gif" }
|
||||
- { target: resource, type: ComposeRecord, expr: "attachable" }
|
||||
- { target: fieldName, type: String, value: "f1" }
|
||||
results:
|
||||
- { target: storedAttBlackGif, expr: "attachment" }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user