Files
pocket-id/backend/internal/service/app_images_service.go
T

179 lines
4.6 KiB
Go

package service
import (
"context"
"errors"
"fmt"
"io"
"mime/multipart"
"path"
"strings"
"sync"
"github.com/pocket-id/pocket-id/backend/internal/apperror"
"github.com/pocket-id/pocket-id/backend/internal/storage"
"github.com/pocket-id/pocket-id/backend/internal/utils"
imageutil "github.com/pocket-id/pocket-id/backend/internal/utils/image"
"github.com/pocket-id/pocket-id/backend/resources"
)
type AppImagesService struct {
mu sync.RWMutex
extensions map[string]string
storage storage.FileStorage
}
func NewAppImagesService(extensions map[string]string, storage storage.FileStorage) *AppImagesService {
return &AppImagesService{extensions: extensions, storage: storage}
}
func (s *AppImagesService) GetImage(ctx context.Context, name string) (io.ReadCloser, int64, string, error) {
ext, err := s.getExtension(name)
if err != nil {
return nil, 0, "", err
}
mimeType := utils.GetImageMimeType(ext)
if mimeType == "" {
return nil, 0, "", fmt.Errorf("unsupported image type '%s'", ext)
}
imagePath := path.Join("application-images", name+"."+ext)
reader, size, err := s.storage.Open(ctx, imagePath)
if err != nil {
if storage.IsNotExist(err) {
return nil, 0, "", apperror.ImageNotFound()
}
return nil, 0, "", err
}
return reader, size, mimeType, nil
}
// GetImageWithDefault behaves like GetImage, but falls back to the image embedded in the binary if no custom image has been uploaded
func (s *AppImagesService) GetImageWithDefault(ctx context.Context, name string) (f io.ReadCloser, size int64, mimeType string, err error) {
f, size, mimeType, err = s.GetImage(ctx, name)
if err == nil || !apperror.IsCode(err, apperror.CodeImageNotFound) {
return f, size, mimeType, err
}
return getDefaultImage(name)
}
func getDefaultImage(name string) (io.ReadCloser, int64, string, error) {
// Map an image name to an image embedded in the binary
var imagePath string
switch name {
case "logoLight":
imagePath = "default-images/logoLight.svg"
case "logoDark":
imagePath = "default-images/logoDark.svg"
default:
return nil, 0, "", apperror.ImageNotFound()
}
file, err := resources.FS.Open(imagePath)
if err != nil {
return nil, 0, "", fmt.Errorf("failed to open default image '%s': %w", name, err)
}
stat, err := file.Stat()
if err != nil {
file.Close()
return nil, 0, "", fmt.Errorf("failed to get size of default image '%s': %w", name, err)
}
return file, stat.Size(), utils.GetImageMimeType(utils.GetFileExtension(imagePath)), nil
}
func (s *AppImagesService) UpdateImage(ctx context.Context, file *multipart.FileHeader, imageName string) error {
fileType := strings.ToLower(utils.GetFileExtension(file.Filename))
mimeType := utils.GetImageMimeType(fileType)
if mimeType == "" {
return apperror.UnsupportedFileType("")
}
s.mu.Lock()
defer s.mu.Unlock()
currentExt := s.extensions[imageName]
imagePath := path.Join("application-images", imageName+"."+fileType)
fileReader, err := file.Open()
if err != nil {
return err
}
defer fileReader.Close()
strippedReader, err := imageutil.StripMetadata(fileReader, fileType)
if errors.Is(err, imageutil.ErrInvalidImage) {
return apperror.InvalidImage(err)
}
if err != nil {
return err
}
if err := s.storage.Save(ctx, imagePath, strippedReader); err != nil {
return err
}
if currentExt != "" && currentExt != fileType {
oldImagePath := path.Join("application-images", imageName+"."+currentExt)
if err := s.storage.Delete(ctx, oldImagePath); err != nil {
return err
}
}
s.extensions[imageName] = fileType
if err := s.storage.Delete(ctx, deletedApplicationImagePath(imageName)); err != nil {
return err
}
return nil
}
func (s *AppImagesService) DeleteImage(ctx context.Context, imageName string) error {
s.mu.Lock()
defer s.mu.Unlock()
ext, ok := s.extensions[imageName]
if !ok || ext == "" {
return apperror.ImageNotFound()
}
if err := s.storage.Save(ctx, deletedApplicationImagePath(imageName), strings.NewReader("")); err != nil {
return err
}
imagePath := path.Join("application-images", imageName+"."+ext)
if err := s.storage.Delete(ctx, imagePath); err != nil {
return err
}
delete(s.extensions, imageName)
return nil
}
func deletedApplicationImagePath(imageName string) string {
return path.Join("application-images", ".deleted", imageName)
}
func (s *AppImagesService) IsDefaultProfilePictureSet() bool {
s.mu.RLock()
defer s.mu.RUnlock()
_, ok := s.extensions["default-profile-picture"]
return ok
}
func (s *AppImagesService) getExtension(name string) (string, error) {
s.mu.RLock()
defer s.mu.RUnlock()
ext, ok := s.extensions[name]
if !ok || ext == "" {
return "", apperror.ImageNotFound()
}
return strings.ToLower(ext), nil
}