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:
555
server/system/service/attachment.go
Normal file
555
server/system/service/attachment.go
Normal file
@@ -0,0 +1,555 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/cortezaproject/corteza/server/assets"
|
||||
"github.com/cortezaproject/corteza/server/pkg/actionlog"
|
||||
intAuth "github.com/cortezaproject/corteza/server/pkg/auth"
|
||||
files "github.com/cortezaproject/corteza/server/pkg/objstore"
|
||||
"github.com/cortezaproject/corteza/server/pkg/options"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
"github.com/cortezaproject/corteza/server/system/types"
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/edwvee/exiffix"
|
||||
"github.com/fogleman/gg"
|
||||
"github.com/golang/freetype/truetype"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/image/font"
|
||||
"image"
|
||||
"image/gif"
|
||||
"io"
|
||||
"net/http"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
attachmentPreviewMaxWidth = 320
|
||||
attachmentPreviewMaxHeight = 180
|
||||
avatarWidth = 300
|
||||
avatarHeight = 300
|
||||
)
|
||||
|
||||
type (
|
||||
attachment struct {
|
||||
actionlog actionlog.Recorder
|
||||
files files.Store
|
||||
ac attachmentAccessController
|
||||
store store.Storer
|
||||
opt options.AttachmentOpt
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
attachmentAccessController interface {
|
||||
CanManageSettings(context.Context) bool
|
||||
CanCreateApplication(context.Context) bool
|
||||
}
|
||||
|
||||
AttachmentService interface {
|
||||
FindByID(ctx context.Context, ID uint64) (*types.Attachment, error)
|
||||
Find(ctx context.Context, filter types.AttachmentFilter) (types.AttachmentSet, types.AttachmentFilter, error)
|
||||
CreateSettingsAttachment(ctx context.Context, name string, size int64, fh io.ReadSeeker, labels map[string]string) (*types.Attachment, error)
|
||||
CreateApplicationAttachment(ctx context.Context, name string, size int64, fh io.ReadSeeker, labels map[string]string) (*types.Attachment, error)
|
||||
CreateAuthAttachment(ctx context.Context, name string, size int64, fh io.ReadSeeker, labels map[string]string) (*types.Attachment, error)
|
||||
CreateAvatarInitialsAttachment(ctx context.Context, initials string, bgColor string, textColor string) (att *types.Attachment, err error)
|
||||
OpenOriginal(att *types.Attachment) (io.ReadSeekCloser, error)
|
||||
OpenPreview(att *types.Attachment) (io.ReadSeekCloser, error)
|
||||
DeleteByID(ctx context.Context, ID uint64) error
|
||||
}
|
||||
)
|
||||
|
||||
func Attachment(store files.Store, opt options.AttachmentOpt, log *zap.Logger) *attachment {
|
||||
return &attachment{
|
||||
files: store,
|
||||
actionlog: DefaultActionlog,
|
||||
ac: DefaultAccessControl,
|
||||
store: DefaultStore,
|
||||
opt: opt,
|
||||
logger: log.Named("attachment"),
|
||||
}
|
||||
}
|
||||
|
||||
func (svc attachment) FindByID(ctx context.Context, ID uint64) (att *types.Attachment, err error) {
|
||||
var (
|
||||
aaProps = &attachmentActionProps{}
|
||||
)
|
||||
|
||||
err = func() (err error) {
|
||||
if ID == 0 {
|
||||
return AttachmentErrInvalidID()
|
||||
}
|
||||
|
||||
if att, err = store.LookupAttachmentByID(ctx, svc.store, ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aaProps.setAttachment(att)
|
||||
return nil
|
||||
}()
|
||||
|
||||
return att, svc.recordAction(ctx, aaProps, AttachmentActionLookup, err)
|
||||
}
|
||||
|
||||
func (svc attachment) DeleteByID(ctx context.Context, ID uint64) (err error) {
|
||||
var (
|
||||
att *types.Attachment
|
||||
aaProps = &attachmentActionProps{attachment: &types.Attachment{ID: ID}}
|
||||
)
|
||||
|
||||
err = func() (err error) {
|
||||
if ID == 0 {
|
||||
return AttachmentErrInvalidID()
|
||||
}
|
||||
|
||||
if att, err = store.LookupAttachmentByID(ctx, svc.store, ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
att.DeletedAt = now()
|
||||
aaProps.setAttachment(att)
|
||||
|
||||
return store.UpdateAttachment(ctx, svc.store, att)
|
||||
}()
|
||||
|
||||
return svc.recordAction(ctx, aaProps, AttachmentActionDelete, err)
|
||||
}
|
||||
|
||||
func (svc attachment) Find(ctx context.Context, filter types.AttachmentFilter) (aa types.AttachmentSet, f types.AttachmentFilter, err error) {
|
||||
var (
|
||||
aaProps = &attachmentActionProps{filter: &filter}
|
||||
)
|
||||
|
||||
err = func() (err error) {
|
||||
aa, f, err = store.SearchAttachments(ctx, svc.store, filter)
|
||||
return err
|
||||
}()
|
||||
|
||||
return aa, f, svc.recordAction(ctx, aaProps, AttachmentActionSearch, err)
|
||||
}
|
||||
|
||||
func (svc attachment) OpenOriginal(att *types.Attachment) (io.ReadSeekCloser, error) {
|
||||
if len(att.Url) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return svc.files.Open(att.Url)
|
||||
}
|
||||
|
||||
func (svc attachment) OpenPreview(att *types.Attachment) (io.ReadSeekCloser, error) {
|
||||
if len(att.PreviewUrl) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return svc.files.Open(att.PreviewUrl)
|
||||
}
|
||||
|
||||
func (svc attachment) CreateSettingsAttachment(ctx context.Context, name string, size int64, fh io.ReadSeeker, labels map[string]string) (att *types.Attachment, err error) {
|
||||
var (
|
||||
aaProps = &attachmentActionProps{}
|
||||
currentUserID = intAuth.GetIdentityFromContext(ctx).Identity()
|
||||
)
|
||||
|
||||
err = func() (err error) {
|
||||
if !svc.ac.CanManageSettings(ctx) {
|
||||
return AttachmentErrNotAllowedToCreate()
|
||||
}
|
||||
|
||||
att = &types.Attachment{
|
||||
OwnerID: currentUserID,
|
||||
Name: strings.TrimSpace(name),
|
||||
Kind: types.AttachmentKindSettings,
|
||||
}
|
||||
|
||||
aaProps.setAttachment(att)
|
||||
|
||||
if labels != nil {
|
||||
att.Meta.Labels = labels
|
||||
}
|
||||
|
||||
if err = svc.create(ctx, name, size, fh, att); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}()
|
||||
|
||||
return att, svc.recordAction(ctx, aaProps, AttachmentActionCreate, err)
|
||||
}
|
||||
|
||||
func (svc attachment) CreateApplicationAttachment(ctx context.Context, name string, size int64, fh io.ReadSeeker, labels map[string]string) (att *types.Attachment, err error) {
|
||||
var (
|
||||
aaProps = &attachmentActionProps{}
|
||||
currentUserID = intAuth.GetIdentityFromContext(ctx).Identity()
|
||||
)
|
||||
|
||||
err = func() (err error) {
|
||||
att = &types.Attachment{
|
||||
OwnerID: currentUserID,
|
||||
Name: strings.TrimSpace(name),
|
||||
Kind: types.AttachmentKindSettings,
|
||||
}
|
||||
|
||||
aaProps.setAttachment(att)
|
||||
|
||||
if !svc.ac.CanCreateApplication(ctx) {
|
||||
return AttachmentErrNotAllowedToCreate()
|
||||
}
|
||||
|
||||
if labels != nil {
|
||||
att.Meta.Labels = labels
|
||||
}
|
||||
|
||||
if err = svc.create(ctx, name, size, fh, att); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}()
|
||||
|
||||
return att, svc.recordAction(ctx, aaProps, AttachmentActionCreate, err)
|
||||
}
|
||||
|
||||
func (svc attachment) CreateAuthAttachment(ctx context.Context, name string, size int64, fh io.ReadSeeker, labels map[string]string) (att *types.Attachment, err error) {
|
||||
var (
|
||||
aaProps = &attachmentActionProps{}
|
||||
currentUserID = intAuth.GetIdentityFromContext(ctx).Identity()
|
||||
)
|
||||
|
||||
// check the file type
|
||||
ext := filepath.Ext(name)
|
||||
if ext != ".jpg" && ext != ".jpeg" && ext != ".png" {
|
||||
return nil, AttachmentErrInvalidAvatarFileType()
|
||||
}
|
||||
|
||||
//check the file size
|
||||
if size > svc.opt.AvatarMaxFileSize {
|
||||
return nil, AttachmentErrInvalidAvatarFileSize()
|
||||
}
|
||||
|
||||
err = func() (err error) {
|
||||
att = &types.Attachment{
|
||||
OwnerID: currentUserID,
|
||||
Name: strings.TrimSpace(name),
|
||||
Kind: types.AttachmentKindAvatar,
|
||||
}
|
||||
|
||||
aaProps.setAttachment(att)
|
||||
|
||||
if labels != nil {
|
||||
att.Meta.Labels = labels
|
||||
}
|
||||
|
||||
if err = svc.create(ctx, name, size, fh, att); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}()
|
||||
|
||||
return att, svc.recordAction(ctx, aaProps, AttachmentActionCreate, err)
|
||||
}
|
||||
|
||||
func (svc attachment) CreateAvatarInitialsAttachment(ctx context.Context, initials string, bgColor string, textColor string) (att *types.Attachment, err error) {
|
||||
var (
|
||||
aaProps = &attachmentActionProps{}
|
||||
currentUserID = intAuth.GetIdentityFromContext(ctx).Identity()
|
||||
)
|
||||
|
||||
if bgColor == "" {
|
||||
bgColor = svc.opt.AvatarInitialsBackgroundColor
|
||||
}
|
||||
|
||||
if textColor == "" {
|
||||
textColor = svc.opt.AvatarInitialsColor
|
||||
}
|
||||
|
||||
// Set initials image background color and
|
||||
// draw the initials text in the center of the image
|
||||
dc := gg.NewContext(avatarWidth, avatarHeight)
|
||||
dc.SetHexColor(bgColor)
|
||||
dc.Clear()
|
||||
|
||||
fontBytes, err := svc.processFontsFile()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get the font face properties
|
||||
f, _ := truetype.Parse(fontBytes)
|
||||
|
||||
face := truetype.NewFace(f, &truetype.Options{
|
||||
Size: 120,
|
||||
DPI: 72,
|
||||
Hinting: font.HintingFull,
|
||||
})
|
||||
|
||||
// set initials text color
|
||||
dc.SetFontFace(face)
|
||||
dc.SetHexColor(textColor)
|
||||
|
||||
textWidth, textHeight := dc.MeasureString(initials)
|
||||
dc.DrawString(initials, (avatarWidth-textWidth)/2, (avatarHeight+textHeight)/2.2)
|
||||
|
||||
// Generate and save the initials image in PNG format
|
||||
err = func() (err error) {
|
||||
att = &types.Attachment{
|
||||
OwnerID: currentUserID,
|
||||
Name: initials,
|
||||
Kind: types.AttachmentKindAvatarInitials,
|
||||
}
|
||||
|
||||
att.ID = nextID()
|
||||
att.CreatedAt = *now()
|
||||
att.Meta.Original.Extension = "png"
|
||||
att.Meta.Labels = map[string]string{"key": types.AttachmentKindAvatarInitials}
|
||||
|
||||
if att.Meta.Original.Image == nil {
|
||||
att.Meta.Original.Image = &types.AttachmentImageMeta{}
|
||||
}
|
||||
|
||||
att.Meta.Original.Image.Initial = initials
|
||||
att.Meta.Original.Image.InitialColor = textColor
|
||||
att.Meta.Original.Image.BackgroundColor = bgColor
|
||||
|
||||
att.Url = svc.files.Original(att.ID, att.Meta.Original.Extension)
|
||||
aaProps.setUrl(att.Url)
|
||||
|
||||
aaProps.setAttachment(att)
|
||||
|
||||
var buf = &bytes.Buffer{}
|
||||
if err = imaging.Encode(buf, dc.Image(), imaging.PNG); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = svc.files.Save(att.Url, buf); err != nil {
|
||||
return AttachmentErrFailedToStoreFile(aaProps).Wrap(err)
|
||||
}
|
||||
|
||||
if err = store.CreateAttachment(ctx, svc.store, att); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
|
||||
return att, nil
|
||||
}
|
||||
|
||||
func (svc attachment) create(ctx context.Context, name string, size int64, fh io.ReadSeeker, att *types.Attachment) (err error) {
|
||||
var (
|
||||
avatar image.Image
|
||||
aaProps = &attachmentActionProps{}
|
||||
)
|
||||
|
||||
att.ID = nextID()
|
||||
att.CreatedAt = *now()
|
||||
|
||||
if svc.files == nil {
|
||||
return fmt.Errorf("cannot create attachment: store handler not set")
|
||||
}
|
||||
|
||||
if size == 0 {
|
||||
return AttachmentErrNotAllowedToCreateEmptyAttachment(aaProps)
|
||||
}
|
||||
|
||||
aaProps.setName(name)
|
||||
aaProps.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.extractMimetype(fh); err != nil {
|
||||
return AttachmentErrFailedToExtractMimeType(aaProps).Wrap(err)
|
||||
}
|
||||
|
||||
att.Url = svc.files.Original(att.ID, att.Meta.Original.Extension)
|
||||
aaProps.setUrl(att.Url)
|
||||
|
||||
// process avatar
|
||||
if att.Kind == types.AttachmentKindAvatar {
|
||||
if avatar, err = imaging.Decode(fh); err != nil {
|
||||
return fmt.Errorf("could not decode original avatar: %w", err)
|
||||
}
|
||||
|
||||
avatar = imaging.Resize(avatar, avatarWidth, avatarHeight, imaging.Lanczos)
|
||||
|
||||
var buf = &bytes.Buffer{}
|
||||
if err = imaging.Encode(buf, avatar, imaging.JPEG); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = svc.files.Save(att.Url, buf); err != nil {
|
||||
return AttachmentErrFailedToStoreFile(aaProps).Wrap(err)
|
||||
}
|
||||
|
||||
if err = store.CreateAttachment(ctx, svc.store, att); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if err = svc.files.Save(att.Url, fh); err != nil {
|
||||
return AttachmentErrFailedToStoreFile(aaProps).Wrap(err)
|
||||
}
|
||||
|
||||
// Process image: extract width, height, make preview
|
||||
err = svc.processImage(fh, att)
|
||||
if err != nil {
|
||||
return AttachmentErrFailedToProcessImage(aaProps).Wrap(err)
|
||||
}
|
||||
|
||||
if err = store.CreateAttachment(ctx, svc.store, att); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc attachment) extractMimetype(file io.ReadSeeker) (mimetype string, err error) {
|
||||
if _, err = file.Seek(0, 0); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Make sure we rewind when we're done
|
||||
defer file.Seek(0, 0)
|
||||
|
||||
// See http.DetectContentType about 512 bytes
|
||||
var buf = make([]byte, 512)
|
||||
if _, err = file.Read(buf); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return http.DetectContentType(buf), nil
|
||||
}
|
||||
|
||||
func (svc attachment) processImage(original io.ReadSeeker, att *types.Attachment) (err error) {
|
||||
if !strings.HasPrefix(att.Meta.Original.Mimetype, "image/") || att.Meta.Original.Mimetype == "image/x-icon" {
|
||||
// Only supporting previews from images (for now)
|
||||
return
|
||||
}
|
||||
|
||||
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",
|
||||
}
|
||||
)
|
||||
|
||||
if _, err = original.Seek(0, 0); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if format, err = imaging.FormatFromExtension(att.Meta.Original.Extension); err != nil {
|
||||
return fmt.Errorf("could not get format from extension '%s': %w", att.Meta.Original.Extension, 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 fmt.Errorf("could not decode gif config: %w", 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 fmt.Errorf("could not decode original image: %w", 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())
|
||||
meta.Mimetype = f2m[previewFormat]
|
||||
meta.Extension = f2e[previewFormat]
|
||||
|
||||
// Can and how we make a preview of this attachment?
|
||||
att.PreviewUrl = svc.files.Preview(att.ID, meta.Extension)
|
||||
|
||||
return svc.files.Save(att.PreviewUrl, buf)
|
||||
}
|
||||
|
||||
// processFontsFile checks if the file exists and has the correct file extension,
|
||||
// It validates the file path provided in the AVATAR_INITIALS_FONT_PATH environment variable,
|
||||
// then reads and returns the file content
|
||||
func (svc attachment) processFontsFile() (fontBytes []byte, err error) {
|
||||
ext := strings.ToLower(filepath.Ext(svc.opt.AvatarInitialsFontPath))
|
||||
if ext != ".ttf" {
|
||||
err = fmt.Errorf("invalid font file extension, please provide a truetype font (.ttf) file")
|
||||
svc.logger.Error(err.Error())
|
||||
return nil, AttachmentErrInvalidAvatarGenerateFontFile().Wrap(err)
|
||||
}
|
||||
|
||||
assetsFile := assets.Files(svc.logger, "")
|
||||
fontFile, err := assetsFile.Open(svc.opt.AvatarInitialsFontPath)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("%w : please ensure that the correct AVATAR_INITIALS_FONT_PATH is set", err)
|
||||
svc.logger.Error(err.Error())
|
||||
return nil, AttachmentErrInvalidAvatarGenerateFontFile().Wrap(err)
|
||||
}
|
||||
|
||||
fontBytes, err = io.ReadAll(fontFile)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to read font file: %w", err)
|
||||
svc.logger.Error(err.Error())
|
||||
return nil, AttachmentErrInvalidAvatarGenerateFontFile().Wrap(err)
|
||||
}
|
||||
|
||||
return fontBytes, nil
|
||||
}
|
||||
Reference in New Issue
Block a user