Add support for icons

- Add icon endpoint for list and upload
- Add endpoint to update icon to specific page
- Add compose icon settings for max size and mimetype limitation
This commit is contained in:
Vivek Patel
2023-03-23 08:50:29 +01:00
committed by Jože Fortun
parent a98610913a
commit c9740a6527
23 changed files with 977 additions and 83 deletions
+65 -8
View File
@@ -255,6 +255,8 @@ endpoints:
- sqlxTypes github.com/jmoiron/sqlx/types
- github.com/cortezaproject/corteza/server/pkg/locale
- github.com/cortezaproject/corteza/server/pkg/label
- github.com/cortezaproject/corteza/server/pkg/str
- github.com/cortezaproject/corteza/server/compose/types
parameters:
path:
- type: uint64
@@ -484,15 +486,70 @@ endpoints:
path: "/{pageID}/translation"
parameters:
path:
- type: uint64
name: pageID
required: true
title: ID
- type: uint64
name: pageID
required: true
title: ID
post:
- name: translations
type: locale.ResourceTranslationSet
title: Resource translation to upsert
required: true
- name: translations
type: locale.ResourceTranslationSet
title: Resource translation to upsert
required: true
- name: updateIcon
path: "/{pageID}/icon"
method: PATCH
title: Update icon for page
parameters:
path:
- type: uint64
name: pageID
required: true
title: Page ID
post:
- name: type
type: "types.IconType"
required: true
title: Icon type
- name: source
type: string
title: Icon source/library
- name: style
type: map[string]string
title: Icon style
parser: str.ParseStrings
- title: icons
description: Compose icons
entrypoint: icon
path: "/icon"
apis:
- name: list
method: GET
title: List icons
path: "/"
parameters:
get:
- type: uint
name: limit
title: Limit
- type: bool
name: incTotal
title: Include total counter
- type: string
name: pageCursor
title: Page cursor
- type: string
name: sort
title: Sort items
- name: upload
path: "/"
method: POST
title: Upload icon
parameters:
post:
- name: icon
type: "*multipart.FileHeader"
title: Icon to upload
- title: Modules
description: Compose module definitions
+76
View File
@@ -0,0 +1,76 @@
package handlers
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
// Definitions file that controls how this file is generated:
//
import (
"context"
"github.com/cortezaproject/corteza/server/compose/rest/request"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/go-chi/chi/v5"
"net/http"
)
type (
// Internal API interface
IconAPI interface {
List(context.Context, *request.IconList) (interface{}, error)
Upload(context.Context, *request.IconUpload) (interface{}, error)
}
// HTTP API interface
Icon struct {
List func(http.ResponseWriter, *http.Request)
Upload func(http.ResponseWriter, *http.Request)
}
)
func NewIcon(h IconAPI) *Icon {
return &Icon{
List: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewIconList()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.List(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Upload: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewIconUpload()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Upload(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
}
}
func (h Icon) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
r.Group(func(r chi.Router) {
r.Use(middlewares...)
r.Get("/icon/", h.List)
r.Post("/icon/", h.Upload)
})
}
+19
View File
@@ -30,6 +30,7 @@ type (
TriggerScript(context.Context, *request.PageTriggerScript) (interface{}, error)
ListTranslations(context.Context, *request.PageListTranslations) (interface{}, error)
UpdateTranslations(context.Context, *request.PageUpdateTranslations) (interface{}, error)
UpdateIcon(context.Context, *request.PageUpdateIcon) (interface{}, error)
}
// HTTP API interface
@@ -45,6 +46,7 @@ type (
TriggerScript func(http.ResponseWriter, *http.Request)
ListTranslations func(http.ResponseWriter, *http.Request)
UpdateTranslations func(http.ResponseWriter, *http.Request)
UpdateIcon func(http.ResponseWriter, *http.Request)
}
)
@@ -224,6 +226,22 @@ func NewPage(h PageAPI) *Page {
return
}
api.Send(w, r, value)
},
UpdateIcon: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewPageUpdateIcon()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.UpdateIcon(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
}
@@ -243,5 +261,6 @@ func (h Page) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.H
r.Post("/namespace/{namespaceID}/page/{pageID}/trigger", h.TriggerScript)
r.Get("/namespace/{namespaceID}/page/{pageID}/translation", h.ListTranslations)
r.Patch("/namespace/{namespaceID}/page/{pageID}/translation", h.UpdateTranslations)
r.Patch("/namespace/{namespaceID}/page/{pageID}/icon", h.UpdateIcon)
})
}
+107
View File
@@ -0,0 +1,107 @@
package rest
import (
"context"
"mime/multipart"
"github.com/cortezaproject/corteza/server/compose/rest/request"
"github.com/cortezaproject/corteza/server/compose/service"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/filter"
)
type (
iconPayload struct {
*attachmentPayload
}
iconSetPayload struct {
Filter types.IconFilter `json:"filter"`
Set []*iconPayload `json:"set"`
}
Icon struct {
locale service.ResourceTranslationsManagerService
attachment service.AttachmentService
ac iconAccessController
}
iconAccessController interface {
CanGrant(context.Context) bool
}
)
func (Icon) New() *Icon {
return &Icon{
locale: service.DefaultResourceTranslation,
attachment: service.DefaultAttachment,
ac: service.DefaultAccessControl,
}
}
func (ctrl *Icon) List(ctx context.Context, r *request.IconList) (interface{}, error) {
var (
err error
f = types.AttachmentFilter{
Kind: types.IconAttachment,
}
set types.AttachmentSet
)
if f.Paging, err = filter.NewPaging(r.Limit, r.PageCursor); err != nil {
return nil, err
}
if f.Sorting, err = filter.NewSorting(r.Sort); err != nil {
return nil, err
}
set, f, err = ctrl.attachment.Find(ctx, f)
return ctrl.makeIconFilterPayload(ctx, set, f, err)
}
func (ctrl *Icon) Upload(ctx context.Context, r *request.IconUpload) (interface{}, error) {
file, err := r.Icon.Open()
if err != nil {
return nil, err
}
defer func(file multipart.File) {
err = file.Close()
if err != nil {
return
}
}(file)
a, err := ctrl.attachment.CreateIconAttachment(
ctx,
r.Icon.Filename,
r.Icon.Size,
file,
)
return makeAttachmentPayload(ctx, a, err)
}
func (ctrl *Icon) makeIconFilterPayload(ctx context.Context, nn types.AttachmentSet, f types.AttachmentFilter, err error) (*iconSetPayload, error) {
if err != nil {
return nil, err
}
var (
a *attachmentPayload
ff types.IconFilter
)
ff.Paging = f.Paging
ff.Sorting = f.Sorting
res := &iconSetPayload{Filter: ff, Set: make([]*iconPayload, len(nn))}
for i := range nn {
a, _ = makeAttachmentPayload(ctx, nn[i], nil)
res.Set[i] = &iconPayload{a}
}
return res, nil
}
+31 -1
View File
@@ -2,7 +2,6 @@ package rest
import (
"context"
"github.com/cortezaproject/corteza/server/compose/rest/request"
"github.com/cortezaproject/corteza/server/compose/service"
"github.com/cortezaproject/corteza/server/compose/service/event"
@@ -29,6 +28,10 @@ type (
Set []*pagePayload `json:"set"`
}
pageIconPayload struct {
*types.PageConfigIcon
}
Page struct {
page interface {
FindByID(ctx context.Context, namespaceID, pageID uint64) (*types.Page, error)
@@ -42,6 +45,8 @@ type (
Update(ctx context.Context, page *types.Page) (*types.Page, error)
DeleteByID(ctx context.Context, namespaceID, pageID uint64, pds types.PageChildrenDeleteStrategy) error
UpdateIcon(ctx context.Context, namespaceID, pageID uint64, icon *types.PageConfigIcon) (out *types.PageConfigIcon, err error)
Reorder(ctx context.Context, namespaceID, selfID uint64, pageIDs []uint64) error
}
locale service.ResourceTranslationsManagerService
@@ -91,6 +96,7 @@ func (ctrl *Page) List(ctx context.Context, r *request.PageList) (interface{}, e
}
set, filter, err := ctrl.page.Find(ctx, f)
return ctrl.makeFilterPayload(ctx, set, filter, err)
}
@@ -238,6 +244,20 @@ func (ctrl *Page) TriggerScript(ctx context.Context, r *request.PageTriggerScrip
return ctrl.makePayload(ctx, page, err)
}
func (ctrl *Page) UpdateIcon(ctx context.Context, r *request.PageUpdateIcon) (interface{}, error) {
var (
err error
icon = &types.PageConfigIcon{
Type: r.Type,
Src: r.Source,
Style: r.Style,
}
)
icon, err = ctrl.page.UpdateIcon(ctx, r.NamespaceID, r.PageID, icon)
return ctrl.makeIconPayload(ctx, icon, err)
}
func (ctrl Page) makePayload(ctx context.Context, c *types.Page, err error) (*pagePayload, error) {
if err != nil || c == nil {
return nil, err
@@ -253,6 +273,16 @@ func (ctrl Page) makePayload(ctx context.Context, c *types.Page, err error) (*pa
}, nil
}
func (ctrl Page) makeIconPayload(_ context.Context, i *types.PageConfigIcon, err error) (*pageIconPayload, error) {
if err != nil || i == nil {
return nil, err
}
return &pageIconPayload{
PageConfigIcon: i,
}, nil
}
func (ctrl Page) makeTreePayload(ctx context.Context, pp types.PageSet, err error) ([]*pagePayload, error) {
if err != nil {
return nil, err
+193
View File
@@ -0,0 +1,193 @@
package request
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
// Definitions file that controls how this file is generated:
//
import (
"encoding/json"
"fmt"
"github.com/cortezaproject/corteza/server/pkg/payload"
"github.com/go-chi/chi/v5"
"io"
"mime/multipart"
"net/http"
"strings"
)
// dummy vars to prevent
// unused imports complain
var (
_ = chi.URLParam
_ = multipart.ErrMessageTooLarge
_ = payload.ParseUint64s
_ = strings.ToLower
_ = io.EOF
_ = fmt.Errorf
_ = json.NewEncoder
)
type (
// Internal API interface
IconList struct {
// Limit GET parameter
//
// Limit
Limit uint
// IncTotal GET parameter
//
// Include total counter
IncTotal bool
// PageCursor GET parameter
//
// Page cursor
PageCursor string
// Sort GET parameter
//
// Sort items
Sort string
}
IconUpload struct {
// Icon POST parameter
//
// Icon to upload
Icon *multipart.FileHeader
}
)
// NewIconList request
func NewIconList() *IconList {
return &IconList{}
}
// Auditable returns all auditable/loggable parameters
func (r IconList) Auditable() map[string]interface{} {
return map[string]interface{}{
"limit": r.Limit,
"incTotal": r.IncTotal,
"pageCursor": r.PageCursor,
"sort": r.Sort,
}
}
// Auditable returns all auditable/loggable parameters
func (r IconList) GetLimit() uint {
return r.Limit
}
// Auditable returns all auditable/loggable parameters
func (r IconList) GetIncTotal() bool {
return r.IncTotal
}
// Auditable returns all auditable/loggable parameters
func (r IconList) GetPageCursor() string {
return r.PageCursor
}
// Auditable returns all auditable/loggable parameters
func (r IconList) GetSort() string {
return r.Sort
}
// Fill processes request and fills internal variables
func (r *IconList) Fill(req *http.Request) (err error) {
{
// GET params
tmp := req.URL.Query()
if val, ok := tmp["limit"]; ok && len(val) > 0 {
r.Limit, err = payload.ParseUint(val[0]), nil
if err != nil {
return err
}
}
if val, ok := tmp["incTotal"]; ok && len(val) > 0 {
r.IncTotal, err = payload.ParseBool(val[0]), nil
if err != nil {
return err
}
}
if val, ok := tmp["pageCursor"]; ok && len(val) > 0 {
r.PageCursor, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := tmp["sort"]; ok && len(val) > 0 {
r.Sort, err = val[0], nil
if err != nil {
return err
}
}
}
return err
}
// NewIconUpload request
func NewIconUpload() *IconUpload {
return &IconUpload{}
}
// Auditable returns all auditable/loggable parameters
func (r IconUpload) Auditable() map[string]interface{} {
return map[string]interface{}{
"icon": r.Icon,
}
}
// Auditable returns all auditable/loggable parameters
func (r IconUpload) GetIcon() *multipart.FileHeader {
return r.Icon
}
// Fill processes request and fills internal variables
func (r *IconUpload) Fill(req *http.Request) (err error) {
if strings.HasPrefix(strings.ToLower(req.Header.Get("content-type")), "application/json") {
err = json.NewDecoder(req.Body).Decode(r)
switch {
case err == io.EOF:
err = nil
case err != nil:
return fmt.Errorf("error parsing http request body: %w", err)
}
}
{
// Caching 32MB to memory, the rest to disk
if err = req.ParseMultipartForm(32 << 20); err != nil && err != http.ErrNotMultipart {
return err
} else if err == nil {
// Multipart params
// Ignoring icon as its handled in the POST params section
}
}
{
if err = req.ParseForm(); err != nil {
return err
}
// POST params
if _, r.Icon, err = req.FormFile("icon"); err != nil {
return fmt.Errorf("error processing uploaded file: %w", err)
}
}
return err
}
+174
View File
@@ -11,9 +11,11 @@ package request
import (
"encoding/json"
"fmt"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/label"
"github.com/cortezaproject/corteza/server/pkg/locale"
"github.com/cortezaproject/corteza/server/pkg/payload"
"github.com/cortezaproject/corteza/server/pkg/str"
"github.com/go-chi/chi/v5"
sqlxTypes "github.com/jmoiron/sqlx/types"
"io"
@@ -322,6 +324,33 @@ type (
// Resource translation to upsert
Translations locale.ResourceTranslationSet
}
PageUpdateIcon struct {
// NamespaceID PATH parameter
//
// Namespace ID
NamespaceID uint64 `json:",string"`
// PageID PATH parameter
//
// Page ID
PageID uint64 `json:",string"`
// Type POST parameter
//
// Icon type
Type types.IconType
// Source POST parameter
//
// Icon source/library
Source string
// Style POST parameter
//
// Icon style
Style map[string]string
}
)
// NewPageList request
@@ -1608,3 +1637,148 @@ func (r *PageUpdateTranslations) Fill(req *http.Request) (err error) {
return err
}
// NewPageUpdateIcon request
func NewPageUpdateIcon() *PageUpdateIcon {
return &PageUpdateIcon{}
}
// Auditable returns all auditable/loggable parameters
func (r PageUpdateIcon) Auditable() map[string]interface{} {
return map[string]interface{}{
"namespaceID": r.NamespaceID,
"pageID": r.PageID,
"type": r.Type,
"source": r.Source,
"style": r.Style,
}
}
// Auditable returns all auditable/loggable parameters
func (r PageUpdateIcon) GetNamespaceID() uint64 {
return r.NamespaceID
}
// Auditable returns all auditable/loggable parameters
func (r PageUpdateIcon) GetPageID() uint64 {
return r.PageID
}
// Auditable returns all auditable/loggable parameters
func (r PageUpdateIcon) GetType() types.IconType {
return r.Type
}
// Auditable returns all auditable/loggable parameters
func (r PageUpdateIcon) GetSource() string {
return r.Source
}
// Auditable returns all auditable/loggable parameters
func (r PageUpdateIcon) GetStyle() map[string]string {
return r.Style
}
// Fill processes request and fills internal variables
func (r *PageUpdateIcon) Fill(req *http.Request) (err error) {
if strings.HasPrefix(strings.ToLower(req.Header.Get("content-type")), "application/json") {
err = json.NewDecoder(req.Body).Decode(r)
switch {
case err == io.EOF:
err = nil
case err != nil:
return fmt.Errorf("error parsing http request body: %w", err)
}
}
{
// Caching 32MB to memory, the rest to disk
if err = req.ParseMultipartForm(32 << 20); err != nil && err != http.ErrNotMultipart {
return err
} else if err == nil {
// Multipart params
if val, ok := req.MultipartForm.Value["type"]; ok && len(val) > 0 {
r.Type, err = types.IconType(val[0]), nil
if err != nil {
return err
}
}
if val, ok := req.MultipartForm.Value["source"]; ok && len(val) > 0 {
r.Source, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := req.MultipartForm.Value["style[]"]; ok {
r.Style, err = str.ParseStrings(val)
if err != nil {
return err
}
} else if val, ok := req.MultipartForm.Value["style"]; ok {
r.Style, err = str.ParseStrings(val)
if err != nil {
return err
}
}
}
}
{
if err = req.ParseForm(); err != nil {
return err
}
// POST params
if val, ok := req.Form["type"]; ok && len(val) > 0 {
r.Type, err = types.IconType(val[0]), nil
if err != nil {
return err
}
}
if val, ok := req.Form["source"]; ok && len(val) > 0 {
r.Source, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := req.Form["style[]"]; ok {
r.Style, err = str.ParseStrings(val)
if err != nil {
return err
}
} else if val, ok := req.Form["style"]; ok {
r.Style, err = str.ParseStrings(val)
if err != nil {
return err
}
}
}
{
var val string
// path params
val = chi.URLParam(req, "namespaceID")
r.NamespaceID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
val = chi.URLParam(req, "pageID")
r.PageID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
}
return err
}
+2
View File
@@ -14,6 +14,7 @@ func MountRoutes() func(r chi.Router) {
module = Module{}.New()
record = Record{}.New()
page = Page{}.New()
pageIcon = Icon{}.New()
chart = Chart{}.New()
notification = Notification{}.New()
attachment = Attachment{}.New()
@@ -34,6 +35,7 @@ func MountRoutes() func(r chi.Router) {
handlers.NewPermissions(Permissions{}.New()).MountRoutes(r)
handlers.NewNamespace(namespace).MountRoutes(r)
handlers.NewPage(page).MountRoutes(r)
handlers.NewIcon(pageIcon).MountRoutes(r)
handlers.NewAutomation(automation).MountRoutes(r)
handlers.NewModule(module).MountRoutes(r)
handlers.NewRecord(record).MountRoutes(r)
+69 -21
View File
@@ -59,6 +59,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)
CreateIconAttachment(ctx context.Context, name string, size int64, fh io.ReadSeeker) (*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.ReadSeekCloser, error)
@@ -82,7 +83,7 @@ func (svc attachment) Find(ctx context.Context, filter types.AttachmentFilter) (
)
err = func() error {
if filter.NamespaceID == 0 {
if filter.NamespaceID == 0 && filter.Kind != types.IconAttachment {
return AttachmentErrInvalidNamespaceID()
}
@@ -111,7 +112,7 @@ func (svc attachment) Find(ctx context.Context, filter types.AttachmentFilter) (
}
}
set, f, err = store.SearchComposeAttachments(ctx, svc.store, f)
set, f, err = store.SearchComposeAttachments(ctx, svc.store, filter)
return err
}()
@@ -163,7 +164,7 @@ func (svc attachment) DeleteByID(ctx context.Context, namespaceID, attachmentID
return svc.recordAction(ctx, aProps, AttachmentActionDelete, err)
}
//func (svc attachment) findNamespaceByID(namespaceID uint64) (ns *types.Namespace, err error) {
// func (svc attachment) findNamespaceByID(namespaceID uint64) (ns *types.Namespace, err error) {
// if namespaceID == 0 {
// return nil, AttachmentErrInvalidNamespaceID()
// }
@@ -178,9 +179,9 @@ func (svc attachment) DeleteByID(ctx context.Context, namespaceID, attachmentID
// }
//
// return ns, nil
//}
// }
//
//func (svc attachment) findPageByID(namespaceID, pageID uint64) (p *types.Page, err error) {
// func (svc attachment) findPageByID(namespaceID, pageID uint64) (p *types.Page, err error) {
// if pageID == 0 {
// return nil, AttachmentErrInvalidPageID()
// }
@@ -195,9 +196,9 @@ func (svc attachment) DeleteByID(ctx context.Context, namespaceID, attachmentID
// }
//
// return p, nil
//}
// }
//
//func (svc attachment) findModuleByID(namespaceID, moduleID uint64) (m *types.Module, err error) {
// func (svc attachment) findModuleByID(namespaceID, moduleID uint64) (m *types.Module, err error) {
// if moduleID == 0 {
// return nil, AttachmentErrInvalidModuleID()
// }
@@ -212,9 +213,9 @@ func (svc attachment) DeleteByID(ctx context.Context, namespaceID, attachmentID
// }
//
// return m, nil
//}
// }
//
//func (svc attachment) findRecordByID(m *types.Module, recordID uint64) (r *types.Record, err error) {
// func (svc attachment) findRecordByID(m *types.Module, recordID uint64) (r *types.Record, err error) {
// if recordID == 0 {
// return nil, AttachmentErrInvalidRecordID()
// }
@@ -229,7 +230,7 @@ func (svc attachment) DeleteByID(ctx context.Context, namespaceID, attachmentID
// }
//
// return r, nil
//}
// }
func (svc attachment) OpenOriginal(att *types.Attachment) (io.ReadSeekCloser, error) {
if len(att.Url) == 0 {
@@ -281,20 +282,10 @@ func (svc attachment) CreatePageAttachment(ctx context.Context, namespaceID uint
var (
maxSize = int64(systemService.CurrentSettings.Compose.Page.Attachments.MaxSize) * megabyte
allowedTypes = systemService.CurrentSettings.Compose.Page.Attachments.Mimetypes
mimeType *mimetype.MIME
)
if maxSize > 0 && maxSize < size {
return AttachmentErrTooLarge().Apply(
errors.Meta("size", size),
errors.Meta("maxSize", maxSize),
)
}
if mimeType, err = svc.extractMimetype(fh); err != nil {
if err = svc.verifySizeAndMimetype(fh, size, maxSize, allowedTypes); err != nil {
return err
} else if !svc.checkMimeType(mimeType, allowedTypes...) {
return AttachmentErrNotAllowedToUploadThisType()
}
}
@@ -311,6 +302,63 @@ func (svc attachment) CreatePageAttachment(ctx context.Context, namespaceID uint
}
func (svc attachment) CreateIconAttachment(ctx context.Context, name string, size int64, fh io.ReadSeeker) (att *types.Attachment, err error) {
var (
aProps = &attachmentActionProps{}
)
err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
if size == 0 {
return AttachmentErrNotAllowedToCreateEmptyAttachment()
}
{
// 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.Icon.Attachments.MaxSize) * megabyte
allowedTypes = systemService.CurrentSettings.Compose.Icon.Attachments.Mimetypes
)
if err = svc.verifySizeAndMimetype(fh, size, maxSize, allowedTypes); err != nil {
return err
}
}
att = &types.Attachment{
Name: strings.TrimSpace(name),
Kind: types.IconAttachment,
}
return svc.create(ctx, s, name, size, fh, att)
})
return att, svc.recordAction(ctx, aProps, AttachmentActionCreate, err)
}
func (svc attachment) verifySizeAndMimetype(fh io.ReadSeeker, size, maxSize int64, allowedTypes []string) (err error) {
// Verify size and type of the uploaded page attachment
// Max size & allowed mime-types are pulled from the current settings
var (
mimeType *mimetype.MIME
)
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()
}
return
}
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
+17 -1
View File
@@ -231,7 +231,6 @@ func (svc page) Tree(ctx context.Context, namespaceID uint64) (tree types.PageSe
}
// Reorder pages
//
func (svc page) Reorder(ctx context.Context, namespaceID, parentID uint64, pageIDs []uint64) (err error) {
var (
aProps = &pageActionProps{page: &types.Page{ID: parentID}}
@@ -442,6 +441,23 @@ func (svc page) UndeleteByID(ctx context.Context, namespaceID, pageID uint64) er
})
}
func (svc page) UpdateIcon(ctx context.Context, namespaceID, pageID uint64, icon *types.PageConfigIcon) (out *types.PageConfigIcon, err error) {
err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
ns, p, err := loadPageCombo(ctx, s, namespaceID, pageID)
if err != nil {
return
}
p.Config.NavItem.Icon = icon
p, err = svc.updater(ctx, svc.store, ns, p, PageActionUpdate, svc.handleUpdate(ctx, p))
out = p.Config.NavItem.Icon
return
})
return
}
func (svc page) updater(ctx context.Context, s store.Storer, ns *types.Namespace, res *types.Page, action func(...*pageActionProps) *pageAction, fn pageUpdateHandler) (*types.Page, error) {
var (
changes pageChanges
+13
View File
@@ -60,14 +60,27 @@ type (
Image *AttachmentImageMeta `json:"image,omitempty"`
}
AttachmentIconMeta struct {
Name string `json:"name"`
Library string `json:"library"`
}
AttachmentIconSvgMeta struct {
Src string `json:"src"`
}
AttachmentMeta struct {
Original AttachmentFileMeta `json:"original"`
Preview *AttachmentFileMeta `json:"preview,omitempty"`
Icon *AttachmentIconMeta `json:"icon,omitempty"`
IconSvg *AttachmentIconSvgMeta `json:"iconSvg,omitempty"`
}
)
const (
PageAttachment string = "page"
IconAttachment string = "icon"
RecordAttachment string = "record"
NamespaceAttachment string = "namespace"
)
+44
View File
@@ -0,0 +1,44 @@
package types
import (
"github.com/cortezaproject/corteza/server/pkg/filter"
)
type (
Icon struct {
}
IconFilter struct {
// Check fn is called by store backend for each resource found function can
// modify the resource and return false if store should not return it
//
// Store then loads additional resources to satisfy the paging parameters
Check func(icon *Icon) (bool, error) `json:"-"`
// Standard helpers for paging and sorting
filter.Sorting
filter.Paging
}
IconType string
)
const (
// IconTypeLink empty or "link" (default):
// Indicate that src will contain an absolute or relative link to an icon.
// Can also be used for inline images (storing "base64:" prefixed string in source).
// This type and reference is not validated by the backend.
IconTypeLink IconType = "link" // type, source
// IconTypeLibrary "library"
// Source references an icon from a library. Ref's value should be in the following
// notation: "font-awesome://<icon-identifier>".
// This type and source is not validated by the backend.
IconTypeLibrary IconType = "library" // type, library(font-awesome), icon name
// IconTypeInlineSvg "svg"
// SRC contains raw SVG document
IconTypeInlineSvg IconType = "inline-svg" // source
// IconTypeAttachment "attachment"
// Reference (ID) to an existing attachment in local Corteza instance is expected
// This type and reference must be validated by the backend.
IconTypeAttachment IconType = "attachment" // type, Icon, name
)
+9 -7
View File
@@ -95,19 +95,21 @@ type (
PageConfig struct {
// How page is presented in the navigation
NavItem struct {
Icon *PageConfigIcon `json:"icon,omitempty"`
// Expanded menu
Expanded bool `json:"expanded"`
Icon *PageConfigIcon `json:"icon,omitempty"`
} `json:"navItem"`
Buttons *PageButtonConfig `json:"buttons,omitempty"`
//// Example how page-config structure can evolve in the future
//Views []struct {
// // Example how page-config structure can evolve in the future
// Views []struct {
// // what kind of output is this view intended for (screen, mobile...?)
// Output string
//
// // Migrated page blocks, might be replaced someday with a more complex structure
// Blocks []PageBlock
//}
// }
}
PageConfigIcon struct {
@@ -129,14 +131,14 @@ type (
// Type: "svg"
// SRC contains raw SVG document
////////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////
// Other types that might be implemented in the future:
// "attachment"
// Reference (ID) to an existing attachment in local Corteza instance is expected
// This type and reference must be validated by the backend.
Type string `json:"type,omitempty"`
Src string `json:"src"`
Type IconType `json:"type,omitempty"`
Src string `json:"src"`
// Any custom styling that should be applied to the icon
Style map[string]string `json:"style,omitempty"`
+35
View File
@@ -25,6 +25,11 @@ type (
// This type is auto-generated.
DeDupRuleSet []*DeDupRule
// IconSet slice of Icon
//
// This type is auto-generated.
IconSet []*Icon
// ModuleSet slice of Module
//
// This type is auto-generated.
@@ -203,6 +208,36 @@ func (set DeDupRuleSet) Filter(f func(*DeDupRule) (bool, error)) (out DeDupRuleS
return
}
// Walk iterates through every slice item and calls w(Icon) err
//
// This function is auto-generated.
func (set IconSet) Walk(w func(*Icon) error) (err error) {
for i := range set {
if err = w(set[i]); err != nil {
return
}
}
return
}
// Filter iterates through every slice item, calls f(Icon) (bool, err) and return filtered slice
//
// This function is auto-generated.
func (set IconSet) Filter(f func(*Icon) (bool, error)) (out IconSet, err error) {
var ok bool
out = IconSet{}
for i := range set {
if ok, err = f(set[i]); err != nil {
return
} else if ok {
out = append(out, set[i])
}
}
return
}
// Walk iterates through every slice item and calls w(Module) err
//
// This function is auto-generated.
+56
View File
@@ -250,6 +250,62 @@ func TestDeDupRuleSetFilter(t *testing.T) {
}
}
func TestIconSetWalk(t *testing.T) {
var (
value = make(IconSet, 3)
req = require.New(t)
)
// check walk with no errors
{
err := value.Walk(func(*Icon) error {
return nil
})
req.NoError(err)
}
// check walk with error
req.Error(value.Walk(func(*Icon) error { return fmt.Errorf("walk error") }))
}
func TestIconSetFilter(t *testing.T) {
var (
value = make(IconSet, 3)
req = require.New(t)
)
// filter nothing
{
set, err := value.Filter(func(*Icon) (bool, error) {
return true, nil
})
req.NoError(err)
req.Equal(len(set), len(value))
}
// filter one item
{
found := false
set, err := value.Filter(func(*Icon) (bool, error) {
if !found {
found = true
return found, nil
}
return false, nil
})
req.NoError(err)
req.Len(set, 1)
}
// filter error
{
_, err := value.Filter(func(*Icon) (bool, error) {
return false, fmt.Errorf("filter error")
})
req.Error(err)
}
}
func TestModuleSetWalk(t *testing.T) {
var (
value = make(ModuleSet, 3)
+2
View File
@@ -8,6 +8,8 @@ types:
labelResourceType: compose:module:field
Page:
labelResourceType: compose:page
Icon:
noIdField: true
Chart:
labelResourceType: compose:chart
Record: {}
+2 -33
View File
@@ -2,12 +2,9 @@ package label
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/cortezaproject/corteza/server/pkg/handle"
"github.com/cortezaproject/corteza/server/pkg/label/types"
"github.com/cortezaproject/corteza/server/pkg/str"
"github.com/cortezaproject/corteza/server/store"
)
@@ -45,35 +42,7 @@ func Changed(old, new map[string]string) bool {
// ParseStrings converts slice of strings with "key=val" format into
func ParseStrings(ss []string) (m map[string]string, err error) {
if len(ss) == 0 {
return nil, nil
}
m = make(map[string]string)
for _, s := range ss {
if strings.HasPrefix(s, "{") && strings.HasSuffix(s, "}") {
// assume json
if err = json.Unmarshal([]byte(s), &m); err != nil {
return nil, err
}
continue
}
kv := strings.SplitN(s, "=", 2)
if len(kv) != 2 {
return nil, fmt.Errorf("invalid label format")
}
if !handle.IsValid(kv[0]) {
return nil, fmt.Errorf("invalid label key format")
}
m[kv[0]] = kv[1]
}
return m, nil
return str.ParseStrings(ss)
}
// Search queries all matching (by kind and key-value filter) labels
+35
View File
@@ -1,6 +1,9 @@
package str
import (
"encoding/json"
"fmt"
"github.com/cortezaproject/corteza/server/pkg/handle"
"strings"
)
@@ -29,3 +32,35 @@ func Match(str1, str2 string, algorithm int) bool {
return false
}
}
func ParseStrings(ss []string) (m map[string]string, err error) {
if len(ss) == 0 {
return nil, nil
}
m = make(map[string]string)
for _, s := range ss {
if strings.HasPrefix(s, "{") && strings.HasSuffix(s, "}") {
// assume json
if err = json.Unmarshal([]byte(s), &m); err != nil {
return nil, err
}
continue
}
kv := strings.SplitN(s, "=", 2)
if len(kv) != 2 {
return nil, fmt.Errorf("invalid label format")
}
if !handle.IsValid(kv[0]) {
return nil, fmt.Errorf("invalid label key format")
}
m[kv[0]] = kv[1]
}
return m, nil
}
@@ -4,3 +4,7 @@ settings:
compose.page.attachments.max-size: 10
compose.page.attachments.mimetypes: []
compose.icon.attachments.max-size: 10
compose.icon.attachments.mimetypes: []
+2
View File
@@ -89,6 +89,8 @@ func DefaultFilters() (f *extendedFilters) {
return
}
case composeType.IconAttachment:
case composeType.RecordAttachment:
panic("@todo pending implementation")
// query = query.
+4 -5
View File
@@ -11,15 +11,14 @@ package request
import (
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"strings"
"github.com/cortezaproject/corteza/server/pkg/label"
"github.com/cortezaproject/corteza/server/pkg/payload"
"github.com/cortezaproject/corteza/server/system/types"
"github.com/go-chi/chi/v5"
"io"
"mime/multipart"
"net/http"
"strings"
)
// dummy vars to prevent
+2 -3
View File
@@ -11,13 +11,12 @@ package service
import (
"context"
"fmt"
"strings"
"time"
"github.com/cortezaproject/corteza/server/pkg/actionlog"
"github.com/cortezaproject/corteza/server/pkg/errors"
"github.com/cortezaproject/corteza/server/pkg/locale"
"github.com/cortezaproject/corteza/server/system/types"
"strings"
"time"
)
type (
+16 -4
View File
@@ -75,7 +75,7 @@ type (
PasswordConstraints PasswordConstraints `kv:"password-constraints" json:"passwordConstraints"`
ProfileAvatar struct { Enabled bool } `kv:"profile-avatar" json:"profile-avatar"`
ProfileAvatar struct{ Enabled bool } `kv:"profile-avatar" json:"profile-avatar"`
} `json:"internal"`
External struct {
@@ -130,7 +130,7 @@ type (
Enforced bool
// Require fresh Email OTP on every client authorization
//Strict bool
// Strict bool
Expires uint
} `kv:"email-otp"`
@@ -143,7 +143,7 @@ type (
Enforced bool
// Require fresh TOTP on every client authorization
//Strict bool
// Strict bool
// TOTP issuer, defaults to "Corteza"
Issuer string
@@ -155,7 +155,7 @@ type (
FromName string `kv:"from-name"`
} `json:"-"`
//Auth Background Image settings
// Auth Background Image settings
UI struct {
BackgroundImageSrc string `kv:"background-image-src" json:"backgroundImageSrc"`
Styles string `kv:"styles" json:"styles"`
@@ -208,6 +208,18 @@ type (
Mimetypes []string
}
}
// Icon related settings
Icon struct {
// @todo implementation
Attachments struct {
// What is max size (in MB, so: MaxSize x 2^20)
MaxSize uint `kv:"max-size"`
// List of mime-types we support,
Mimetypes []string
}
}
} `kv:"compose" json:"compose"`
// Federation settings