snapshot of https://github.com/cortezaproject/corteza.git (2024.9.x, 2026-09-18; blobs > 131072B omitted)
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s
This commit is contained in:
717
server/compose/service/attachment.go
Normal file
717
server/compose/service/attachment.go
Normal file
@@ -0,0 +1,717 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"image"
|
||||
"image/gif"
|
||||
"io"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/compose/dalutils"
|
||||
"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/gabriel-vasile/mimetype"
|
||||
"github.com/mat/besticon/ico"
|
||||
)
|
||||
|
||||
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 (
|
||||
attachment struct {
|
||||
actionlog actionlog.Recorder
|
||||
objects objstore.Store
|
||||
ac attachmentAccessController
|
||||
store store.Storer
|
||||
dal dalDater
|
||||
}
|
||||
|
||||
attachmentAccessController interface {
|
||||
CanReadNamespace(context.Context, *types.Namespace) bool
|
||||
CanCreateNamespace(context.Context) bool
|
||||
CanReadModule(context.Context, *types.Module) bool
|
||||
CanReadPage(context.Context, *types.Page) bool
|
||||
CanUpdatePage(context.Context, *types.Page) bool
|
||||
CanReadRecord(context.Context, *types.Record) bool
|
||||
CanUpdateRecord(context.Context, *types.Record) bool
|
||||
CanCreateRecordOnModule(context.Context, *types.Module) bool
|
||||
}
|
||||
|
||||
AttachmentService interface {
|
||||
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)
|
||||
OpenPreview(att *types.Attachment) (io.ReadSeekCloser, error)
|
||||
DeleteByID(ctx context.Context, namespaceID, attachmentID uint64) error
|
||||
}
|
||||
)
|
||||
|
||||
func Attachment(store objstore.Store, dal dalDater) *attachment {
|
||||
return &attachment{
|
||||
objects: store,
|
||||
ac: DefaultAccessControl,
|
||||
store: DefaultStore,
|
||||
dal: dal,
|
||||
}
|
||||
}
|
||||
|
||||
func (svc attachment) Find(ctx context.Context, filter types.AttachmentFilter) (set types.AttachmentSet, f types.AttachmentFilter, err error) {
|
||||
var (
|
||||
aProps = &attachmentActionProps{filter: &filter}
|
||||
)
|
||||
|
||||
err = func() error {
|
||||
if filter.NamespaceID == 0 && filter.Kind != types.IconAttachment {
|
||||
return AttachmentErrInvalidNamespaceID()
|
||||
}
|
||||
|
||||
if filter.PageID > 0 {
|
||||
aProps.namespace, aProps.page, err = loadPageCombo(ctx, svc.store, filter.NamespaceID, filter.PageID)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if svc.ac.CanReadPage(ctx, aProps.page) {
|
||||
return AttachmentErrNotAllowedToReadPage()
|
||||
}
|
||||
}
|
||||
|
||||
if filter.RecordID > 0 {
|
||||
aProps.namespace, aProps.module, aProps.record, err = loadRecordCombo(ctx, svc.store, svc.dal, filter.NamespaceID, filter.ModuleID, filter.RecordID)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if svc.ac.CanReadRecord(ctx, aProps.record) {
|
||||
return AttachmentErrNotAllowedToReadRecord()
|
||||
}
|
||||
} else if filter.ModuleID > 0 {
|
||||
aProps.namespace, aProps.module, err = loadModuleCombo(ctx, svc.store, filter.NamespaceID, filter.ModuleID)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if svc.ac.CanReadRecord(ctx, aProps.record) {
|
||||
return AttachmentErrNotAllowedToReadRecord()
|
||||
}
|
||||
}
|
||||
|
||||
set, f, err = store.SearchComposeAttachments(ctx, svc.store, filter)
|
||||
return err
|
||||
}()
|
||||
|
||||
return set, f, svc.recordAction(ctx, aProps, AttachmentActionSearch, err)
|
||||
}
|
||||
|
||||
func (svc attachment) FindByID(ctx context.Context, namespaceID, attachmentID uint64) (att *types.Attachment, err error) {
|
||||
var (
|
||||
aProps = &attachmentActionProps{}
|
||||
)
|
||||
|
||||
err = func() error {
|
||||
if attachmentID == 0 {
|
||||
return AttachmentErrInvalidID()
|
||||
}
|
||||
|
||||
if att, err = store.LookupComposeAttachmentByID(ctx, svc.store, attachmentID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aProps.setAttachment(att)
|
||||
return nil
|
||||
}()
|
||||
|
||||
return att, svc.recordAction(ctx, aProps, AttachmentActionLookup, err)
|
||||
}
|
||||
|
||||
func (svc attachment) DeleteByID(ctx context.Context, namespaceID, attachmentID uint64) (err error) {
|
||||
var (
|
||||
att *types.Attachment
|
||||
aProps = &attachmentActionProps{attachment: &types.Attachment{ID: attachmentID}}
|
||||
)
|
||||
|
||||
err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
|
||||
if attachmentID == 0 {
|
||||
return AttachmentErrInvalidID()
|
||||
}
|
||||
|
||||
if att, err = store.LookupComposeAttachmentByID(ctx, s, attachmentID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aProps.setAttachment(att)
|
||||
|
||||
att.DeletedAt = now()
|
||||
return store.UpdateComposeAttachment(ctx, s, att)
|
||||
})
|
||||
|
||||
return svc.recordAction(ctx, aProps, AttachmentActionDelete, err)
|
||||
}
|
||||
|
||||
// func (svc attachment) findNamespaceByID(namespaceID uint64) (ns *types.Namespace, err error) {
|
||||
// if namespaceID == 0 {
|
||||
// return nil, AttachmentErrInvalidNamespaceID()
|
||||
// }
|
||||
//
|
||||
// ns, err = svc.namespaceRepo.FindByID(namespaceID)
|
||||
// if repository.ErrNamespaceNotFound.Eq(err) {
|
||||
// return nil, AttachmentErrNamespaceNotFound()
|
||||
// }
|
||||
//
|
||||
// if !svc.ac.CanReadNamespace(ctx, ns) {
|
||||
// return nil, AttachmentErrNotAllowedToReadNamespace()
|
||||
// }
|
||||
//
|
||||
// return ns, nil
|
||||
// }
|
||||
//
|
||||
// func (svc attachment) findPageByID(namespaceID, pageID uint64) (p *types.Page, err error) {
|
||||
// if pageID == 0 {
|
||||
// return nil, AttachmentErrInvalidPageID()
|
||||
// }
|
||||
//
|
||||
// p, err = svc.pageRepo.FindByID(namespaceID, pageID)
|
||||
// if repository.ErrPageNotFound.Eq(err) {
|
||||
// return nil, AttachmentErrPageNotFound()
|
||||
// }
|
||||
//
|
||||
// if !svc.ac.CanReadPage(ctx, p) {
|
||||
// return nil, AttachmentErrNotAllowedToReadPage()
|
||||
// }
|
||||
//
|
||||
// return p, nil
|
||||
// }
|
||||
//
|
||||
// func (svc attachment) findModuleByID(namespaceID, moduleID uint64) (m *types.Module, err error) {
|
||||
// if moduleID == 0 {
|
||||
// return nil, AttachmentErrInvalidModuleID()
|
||||
// }
|
||||
//
|
||||
// m, err = svc.moduleRepo.FindByID(namespaceID, moduleID)
|
||||
// if repository.ErrModuleNotFound.Eq(err) {
|
||||
// return nil, AttachmentErrModuleNotFound()
|
||||
// }
|
||||
//
|
||||
// if !svc.ac.CanReadModule(ctx, m) {
|
||||
// return nil, AttachmentErrNotAllowedToReadModule()
|
||||
// }
|
||||
//
|
||||
// return m, nil
|
||||
// }
|
||||
//
|
||||
// func (svc attachment) findRecordByID(m *types.Module, recordID uint64) (r *types.Record, err error) {
|
||||
// if recordID == 0 {
|
||||
// return nil, AttachmentErrInvalidRecordID()
|
||||
// }
|
||||
//
|
||||
// r, err = svc.recordRepo.FindByID(m.NamespaceID, recordID)
|
||||
// if repository.ErrRecordNotFound.Eq(err) {
|
||||
// return nil, AttachmentErrRecordNotFound()
|
||||
// }
|
||||
//
|
||||
// if !svc.ac.CanReadRecord(ctx, m) {
|
||||
// return nil, AttachmentErrNotAllowedToReadRecord()
|
||||
// }
|
||||
//
|
||||
// return r, nil
|
||||
// }
|
||||
|
||||
func (svc attachment) OpenOriginal(att *types.Attachment) (io.ReadSeekCloser, error) {
|
||||
if len(att.Url) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return svc.objects.Open(att.Url)
|
||||
}
|
||||
|
||||
func (svc attachment) OpenPreview(att *types.Attachment) (io.ReadSeekCloser, error) {
|
||||
if len(att.PreviewUrl) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return svc.objects.Open(att.PreviewUrl)
|
||||
}
|
||||
|
||||
func (svc attachment) CreatePageAttachment(ctx context.Context, namespaceID uint64, name string, size int64, fh io.ReadSeeker, pageID uint64) (att *types.Attachment, err error) {
|
||||
var (
|
||||
ns *types.Namespace
|
||||
p *types.Page
|
||||
|
||||
aProps = &attachmentActionProps{
|
||||
namespace: &types.Namespace{ID: namespaceID},
|
||||
page: &types.Page{ID: pageID},
|
||||
}
|
||||
)
|
||||
|
||||
err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
|
||||
if size == 0 {
|
||||
return AttachmentErrNotAllowedToCreateEmptyAttachment()
|
||||
}
|
||||
|
||||
ns, p, err = loadPageCombo(ctx, s, namespaceID, pageID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aProps.setNamespace(ns)
|
||||
aProps.setPage(p)
|
||||
|
||||
if !svc.ac.CanUpdatePage(ctx, p) {
|
||||
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
|
||||
)
|
||||
|
||||
if err = svc.verifySizeAndMimetype(fh, size, maxSize, allowedTypes); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
att = &types.Attachment{
|
||||
NamespaceID: namespaceID,
|
||||
Name: strings.TrimSpace(name),
|
||||
Kind: types.PageAttachment,
|
||||
}
|
||||
|
||||
return svc.create(ctx, s, name, size, fh, att)
|
||||
})
|
||||
|
||||
return att, svc.recordAction(ctx, aProps, AttachmentActionCreate, err)
|
||||
|
||||
}
|
||||
|
||||
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
|
||||
m *types.Module
|
||||
r *types.Record
|
||||
|
||||
aProps = &attachmentActionProps{
|
||||
namespace: &types.Namespace{ID: namespaceID},
|
||||
module: &types.Module{ID: moduleID},
|
||||
record: &types.Record{ID: recordID},
|
||||
}
|
||||
)
|
||||
|
||||
err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
|
||||
if size == 0 {
|
||||
return AttachmentErrNotAllowedToCreateEmptyAttachment()
|
||||
}
|
||||
|
||||
ns, m, err = loadModuleCombo(ctx, s, namespaceID, moduleID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aProps.setNamespace(ns)
|
||||
aProps.setModule(m)
|
||||
|
||||
if recordID > 0 {
|
||||
// Uploading to existing record
|
||||
//
|
||||
// To allow upload (attachment creation) user must have permissions to
|
||||
// alter that record
|
||||
|
||||
r, err = dalutils.ComposeRecordsFind(ctx, svc.dal, m, recordID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aProps.setRecord(r)
|
||||
|
||||
if !svc.ac.CanUpdateRecord(ctx, r) {
|
||||
return AttachmentErrNotAllowedToUpdateRecord()
|
||||
}
|
||||
} else {
|
||||
// Uploading to non-existing record
|
||||
//
|
||||
// To allow upload (attachment creation) user must have permissions to
|
||||
// create records
|
||||
if !svc.ac.CanCreateRecordOnModule(ctx, m) {
|
||||
return AttachmentErrNotAllowedToCreateRecords()
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// 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 *mimetype.MIME
|
||||
)
|
||||
|
||||
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, s, name, size, fh, att)
|
||||
})
|
||||
|
||||
return att, svc.recordAction(ctx, aProps, AttachmentActionCreate, err)
|
||||
}
|
||||
|
||||
func (svc attachment) CreateNamespaceAttachment(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()
|
||||
}
|
||||
|
||||
if !svc.ac.CanCreateNamespace(ctx) {
|
||||
return AttachmentErrNotAllowedToUpdateNamespace()
|
||||
}
|
||||
|
||||
{
|
||||
// Verify file size and mimetype
|
||||
var (
|
||||
maxSize = int64(systemService.CurrentSettings.Compose.Namespace.Attachments.MaxSize) * megabyte
|
||||
allowedTypes = systemService.CurrentSettings.Compose.Namespace.Attachments.Mimetypes
|
||||
)
|
||||
|
||||
if err = svc.verifySizeAndMimetype(fh, size, maxSize, allowedTypes); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
att = &types.Attachment{
|
||||
Name: strings.TrimSpace(name),
|
||||
Kind: types.NamespaceAttachment,
|
||||
}
|
||||
|
||||
// @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, s store.ComposeAttachments, name string, size int64, fh io.ReadSeeker, att *types.Attachment) (err error) {
|
||||
var (
|
||||
aProps = &attachmentActionProps{}
|
||||
)
|
||||
|
||||
// preset attachment ID because we need ref for storage
|
||||
att.ID = nextID()
|
||||
att.CreatedAt = *now()
|
||||
|
||||
if att.OwnerID == 0 {
|
||||
att.OwnerID = auth.GetIdentityFromContext(ctx).Identity()
|
||||
}
|
||||
|
||||
if svc.objects == nil {
|
||||
return errors.Internal("cannot create attachment: store handler not set")
|
||||
}
|
||||
|
||||
if size == 0 {
|
||||
return AttachmentErrNotAllowedToCreateEmptyAttachment()
|
||||
}
|
||||
|
||||
aProps.setName(name)
|
||||
aProps.setSize(size)
|
||||
|
||||
// Extract extension but make sure path.Ext is not confused by any leading/trailing dots
|
||||
att.Meta.Original.Extension = strings.Trim(path.Ext(strings.Trim(name, ".")), ".")
|
||||
|
||||
att.Meta.Original.Size = size
|
||||
if att.Meta.Original.Mimetype, err = svc.extractMimetypeS(fh); err != nil {
|
||||
return AttachmentErrFailedToExtractMimeType(aProps).Wrap(err)
|
||||
}
|
||||
|
||||
att.Url = svc.objects.Original(att.ID, att.Meta.Original.Extension)
|
||||
aProps.setUrl(att.Url)
|
||||
|
||||
if err = svc.objects.Save(att.Url, fh); err != nil {
|
||||
return AttachmentErrFailedToStoreFile(aProps).Wrap(err)
|
||||
}
|
||||
|
||||
// Process image: extract width, height, make preview
|
||||
err = svc.processImage(fh, att)
|
||||
if err != nil {
|
||||
return AttachmentErrFailedToProcessImage(aProps).Wrap(err)
|
||||
}
|
||||
|
||||
if err = store.CreateComposeAttachment(ctx, s, att); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc attachment) extractMimetype(file io.ReadSeeker) (mType *mimetype.MIME, err error) {
|
||||
if _, err = file.Seek(0, 0); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Make sure we rewind when we're done
|
||||
defer func(file io.ReadSeeker, offset int64, whence int) {
|
||||
_, _ = file.Seek(offset, whence)
|
||||
}(file, 0, 0)
|
||||
|
||||
var mime *mimetype.MIME
|
||||
if mime, err = mimetype.DetectReader(file); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return mime, nil
|
||||
}
|
||||
|
||||
func (svc attachment) extractMimetypeS(file io.ReadSeeker) (mType string, err error) {
|
||||
aux, err := svc.extractMimetype(file)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return aux.String(), nil
|
||||
}
|
||||
|
||||
func (svc attachment) processImage(original io.ReadSeeker, att *types.Attachment) (err error) {
|
||||
if !strings.HasPrefix(att.Meta.Original.Mimetype, "image/") {
|
||||
// Only supporting previews from images (for now)
|
||||
return
|
||||
}
|
||||
|
||||
if att.Meta.Original.Mimetype == "image/svg+xml" {
|
||||
// SVG is a vector format, skip raster image processing/preview generation
|
||||
return
|
||||
}
|
||||
|
||||
const (
|
||||
iconMimetype = "image/x-icon"
|
||||
iconExtension = "ico"
|
||||
)
|
||||
|
||||
var (
|
||||
preview image.Image
|
||||
opts []imaging.EncodeOption
|
||||
format imaging.Format
|
||||
previewFormat imaging.Format
|
||||
animated bool
|
||||
f2m = map[imaging.Format]string{
|
||||
imaging.JPEG: "image/jpeg",
|
||||
imaging.GIF: "image/gif",
|
||||
}
|
||||
f2e = map[imaging.Format]string{
|
||||
imaging.JPEG: "jpg",
|
||||
imaging.GIF: "gif",
|
||||
}
|
||||
isIcon = att.Meta.Original.Mimetype == iconMimetype
|
||||
)
|
||||
|
||||
if _, err = original.Seek(0, 0); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if isIcon {
|
||||
preview, err = ico.Decode(original)
|
||||
if err != nil {
|
||||
return errors.Internal("Could not decode ico config").Wrap(err)
|
||||
}
|
||||
} else {
|
||||
if format, err = imaging.FormatFromExtension(att.Meta.Original.Extension); err != nil {
|
||||
return errors.Internal("could not get format from extension '%s'", att.Meta.Original.Extension).Wrap(err)
|
||||
}
|
||||
|
||||
previewFormat = format
|
||||
|
||||
if imaging.JPEG == format {
|
||||
// Rotate image if needed
|
||||
// if preview, _, err = exiffix.Decode(original); err != nil {
|
||||
// return fmt.Errorf("Could not decode EXIF from JPEG", err)
|
||||
// }
|
||||
preview, _, _ = exiffix.Decode(original)
|
||||
}
|
||||
|
||||
if imaging.GIF == format {
|
||||
// Decode all and check loops & delay to determine if GIF is animated or not
|
||||
if cfg, err := gif.DecodeAll(original); err == nil {
|
||||
animated = cfg.LoopCount > 0 || len(cfg.Delay) > 1
|
||||
|
||||
// Use first image for the preview
|
||||
preview = cfg.Image[0]
|
||||
} else {
|
||||
return errors.Internal("Could not decode gif config").Wrap(err)
|
||||
}
|
||||
|
||||
} else {
|
||||
// Use GIF preview for GIFs and JPEG for everything else!
|
||||
previewFormat = imaging.JPEG
|
||||
|
||||
// Store with a bit lower quality
|
||||
opts = append(opts, imaging.JPEGQuality(85))
|
||||
}
|
||||
}
|
||||
|
||||
// In case of JPEG we decode the image and rotate it beforehand
|
||||
// other cases are handled here
|
||||
if preview == nil {
|
||||
if preview, err = imaging.Decode(original); err != nil {
|
||||
return errors.Internal("Could not decode original image").Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
var width, height = preview.Bounds().Max.X, preview.Bounds().Max.Y
|
||||
att.SetOriginalImageMeta(width, height, animated)
|
||||
|
||||
if width > attachmentPreviewMaxWidth && width > height {
|
||||
// Landscape does not fit
|
||||
preview = imaging.Resize(preview, attachmentPreviewMaxWidth, 0, imaging.Lanczos)
|
||||
} else if height > attachmentPreviewMaxHeight {
|
||||
// Height does not fit
|
||||
preview = imaging.Resize(preview, 0, attachmentPreviewMaxHeight, imaging.Lanczos)
|
||||
}
|
||||
|
||||
// Get dimensions from the preview
|
||||
width, height = preview.Bounds().Max.X, preview.Bounds().Max.Y
|
||||
|
||||
var buf = &bytes.Buffer{}
|
||||
if err = imaging.Encode(buf, preview, previewFormat, opts...); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
meta := att.SetPreviewImageMeta(width, height, false)
|
||||
meta.Size = int64(buf.Len())
|
||||
|
||||
if isIcon {
|
||||
meta.Mimetype = iconMimetype
|
||||
meta.Extension = iconExtension
|
||||
} else {
|
||||
meta.Mimetype = f2m[previewFormat]
|
||||
meta.Extension = f2e[previewFormat]
|
||||
}
|
||||
|
||||
// Can and how we make a preview of this attachment?
|
||||
att.PreviewUrl = svc.objects.Preview(att.ID, meta.Extension)
|
||||
|
||||
return svc.objects.Save(att.PreviewUrl, buf)
|
||||
}
|
||||
|
||||
func (attachment) checkMimeType(test *mimetype.MIME, vv ...string) bool {
|
||||
if len(vv) == 0 {
|
||||
// return true if there are no type constraints to check against
|
||||
return true
|
||||
}
|
||||
|
||||
for _, v := range vv {
|
||||
if test.Is(v) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
var _ AttachmentService = &attachment{}
|
||||
Reference in New Issue
Block a user