mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-10-04 16:59:03 +02:00
179 lines
4.6 KiB
Go
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
|
|
}
|