Files
RenewCMS/internal/infrastructure/useCases/CreateImage.go
T
2026-10-06 22:25:23 +02:00

141 lines
3.5 KiB
Go

package useCases
import (
"RenewCMS/internal/domain/audit"
"RenewCMS/internal/domain/image"
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/xml"
stdimage "image"
"io"
"strings"
_ "golang.org/x/image/webp"
_ "image/jpeg"
_ "image/png"
)
const svgRootElement = "svg"
var rasterFormats = map[string]string{
"image/png": "png",
"image/jpeg": "jpeg",
"image/webp": "webp",
}
type CreateImageUseCase struct {
imageRepository image.Repository
storage image.Storage
recorder audit.Recorder
}
func NewCreateImageUseCase(imageRepository image.Repository, storage image.Storage, recorder audit.Recorder) *CreateImageUseCase {
return &CreateImageUseCase{imageRepository, storage, recorder}
}
func (g *CreateImageUseCase) CreateImage(ctx context.Context, input image.ImageInput) (image.Image, error) {
created, err := g.create(input)
event := audit.NewEvent(audit.ActionMediaUpload, audit.CategoryMedia)
if err != nil {
event.Target = attemptedUploadTarget(input)
recordFailure(ctx, g.recorder, event, err)
return image.Image{}, err
}
event.Target = imageTarget(created)
g.recorder.Record(ctx, event)
return created, nil
}
func imageTarget(img image.Image) audit.Target {
return audit.NewTarget(audit.TargetImage, img.ID, img.Filename)
}
func attemptedUploadTarget(input image.ImageInput) audit.Target {
if strings.TrimSpace(input.Filename) == "" {
return audit.Target{}
}
return audit.Target{Type: audit.TargetImage, Name: image.NormalizeUploadFilename(input.Filename, input.ContentType)}
}
func (g *CreateImageUseCase) create(input image.ImageInput) (image.Image, error) {
if !image.IsAllowedContentType(input.ContentType) {
return image.Image{}, image.ErrImageTypeNotAllowed
}
data, err := readWithinLimit(input.Content)
if err != nil {
return image.Image{}, err
}
width, height, err := measure(data, input.ContentType)
if err != nil {
return image.Image{}, err
}
checksum := sha256.Sum256(data)
storedName, err := g.storage.Put(input.ContentType, bytes.NewReader(data))
if err != nil {
return image.Image{}, err
}
created, err := g.imageRepository.Create(image.Image{
Path: image.PublicPath(storedName),
Filename: image.NormalizeUploadFilename(input.Filename, input.ContentType),
Size: int64(len(data)),
ContentType: input.ContentType,
Width: width,
Height: height,
SHA256: hex.EncodeToString(checksum[:]),
UploadedByID: input.UploadedByID,
})
if err != nil {
_ = g.storage.Delete(storedName)
return image.Image{}, err
}
return created, nil
}
func readWithinLimit(content io.Reader) ([]byte, error) {
if content == nil {
return nil, image.ErrImageUnreadable
}
data, err := io.ReadAll(io.LimitReader(content, image.MaxImageSize+1))
if err != nil {
return nil, err
}
if int64(len(data)) > image.MaxImageSize {
return nil, image.ErrImageTooLarge
}
return data, nil
}
func measure(data []byte, contentType string) (int, int, error) {
if contentType == "image/svg+xml" {
if !hasSvgRoot(data) {
return 0, 0, image.ErrImageUnreadable
}
return 0, 0, nil
}
config, format, err := stdimage.DecodeConfig(bytes.NewReader(data))
if err != nil || format != rasterFormats[contentType] {
return 0, 0, image.ErrImageUnreadable
}
return config.Width, config.Height, nil
}
func hasSvgRoot(data []byte) bool {
decoder := xml.NewDecoder(bytes.NewReader(data))
for {
token, err := decoder.Token()
if err != nil {
return false
}
if start, ok := token.(xml.StartElement); ok {
return start.Name.Local == svgRootElement
}
}
}