feat: image variants engine
This commit is contained in:
1 parent
f0845de643
commit
f1d56ef50e
80 files changed
+7312
-84
No files matched your search
@@ -0,0 +1,378 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
domain "RenewCMS/internal/domain/image"
|
||||
"RenewCMS/internal/infrastructure/imaging"
|
||||
"RenewCMS/internal/infrastructure/useCases"
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
var errVariantStorageFailure = errors.New("variant storage failure")
|
||||
|
||||
var variantStorageExtensions = map[string]string{
|
||||
"image/png": ".png",
|
||||
"image/jpeg": ".jpeg",
|
||||
"image/webp": ".webp",
|
||||
}
|
||||
|
||||
type mockVariantRepo struct {
|
||||
mutex sync.Mutex
|
||||
variants map[string]domain.Variant
|
||||
failGet error
|
||||
calls int
|
||||
}
|
||||
|
||||
var _ domain.VariantRepository = &mockVariantRepo{}
|
||||
|
||||
func newMockVariantRepo() *mockVariantRepo {
|
||||
return &mockVariantRepo{variants: map[string]domain.Variant{}}
|
||||
}
|
||||
|
||||
func (m *mockVariantRepo) List() ([]domain.Variant, error) {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
m.calls++
|
||||
listed := make([]domain.Variant, 0, len(m.variants))
|
||||
for _, variant := range m.variants {
|
||||
listed = append(listed, variant)
|
||||
}
|
||||
sort.Slice(listed, func(i, j int) bool { return listed[i].Name < listed[j].Name })
|
||||
return listed, nil
|
||||
}
|
||||
|
||||
func (m *mockVariantRepo) Get(name string) (domain.Variant, error) {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
m.calls++
|
||||
if m.failGet != nil {
|
||||
return domain.Variant{}, m.failGet
|
||||
}
|
||||
variant, found := m.variants[name]
|
||||
if !found {
|
||||
return domain.Variant{}, domain.ErrVariantNotFound
|
||||
}
|
||||
return variant, nil
|
||||
}
|
||||
|
||||
func (m *mockVariantRepo) Create(variant domain.Variant) (domain.Variant, error) {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
m.calls++
|
||||
if _, found := m.variants[variant.Name]; found {
|
||||
return domain.Variant{}, domain.ErrVariantExists
|
||||
}
|
||||
m.variants[variant.Name] = variant
|
||||
return variant, nil
|
||||
}
|
||||
|
||||
func (m *mockVariantRepo) Update(variant domain.Variant) (domain.Variant, error) {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
m.calls++
|
||||
if _, found := m.variants[variant.Name]; !found {
|
||||
return domain.Variant{}, domain.ErrVariantNotFound
|
||||
}
|
||||
m.variants[variant.Name] = variant
|
||||
return variant, nil
|
||||
}
|
||||
|
||||
func (m *mockVariantRepo) Delete(name string) error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
m.calls++
|
||||
if _, found := m.variants[name]; !found {
|
||||
return domain.ErrVariantNotFound
|
||||
}
|
||||
delete(m.variants, name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockVariantRepo) stored(name string) domain.Variant {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
return m.variants[name]
|
||||
}
|
||||
|
||||
func (m *mockVariantRepo) count() int {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
return len(m.variants)
|
||||
}
|
||||
|
||||
type mockVariantFileRepo struct {
|
||||
mutex sync.Mutex
|
||||
rows map[uint32]domain.VariantFile
|
||||
nextID uint32
|
||||
deleted []uint32
|
||||
saves int
|
||||
calls int
|
||||
}
|
||||
|
||||
var _ domain.VariantFileRepository = &mockVariantFileRepo{}
|
||||
|
||||
func newMockVariantFileRepo() *mockVariantFileRepo {
|
||||
return &mockVariantFileRepo{rows: map[uint32]domain.VariantFile{}}
|
||||
}
|
||||
|
||||
func (m *mockVariantFileRepo) Find(variantName string, sourceName string) (domain.VariantFile, bool, error) {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
m.calls++
|
||||
for _, row := range m.rows {
|
||||
if row.VariantName == variantName && row.SourceName == sourceName {
|
||||
return row, true, nil
|
||||
}
|
||||
}
|
||||
return domain.VariantFile{}, false, nil
|
||||
}
|
||||
|
||||
func (m *mockVariantFileRepo) Save(file domain.VariantFile) (domain.VariantFile, error) {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
m.calls++
|
||||
m.saves++
|
||||
for id, row := range m.rows {
|
||||
if row.VariantName == file.VariantName && row.SourceName == file.SourceName {
|
||||
file.ID = id
|
||||
m.rows[id] = file
|
||||
return file, nil
|
||||
}
|
||||
}
|
||||
m.nextID++
|
||||
file.ID = m.nextID
|
||||
m.rows[file.ID] = file
|
||||
return file, nil
|
||||
}
|
||||
|
||||
func (m *mockVariantFileRepo) ListByVariant(variantName string) ([]domain.VariantFile, error) {
|
||||
return m.filtered(func(row domain.VariantFile) bool { return row.VariantName == variantName }), nil
|
||||
}
|
||||
|
||||
func (m *mockVariantFileRepo) ListBySource(sourceName string) ([]domain.VariantFile, error) {
|
||||
return m.filtered(func(row domain.VariantFile) bool { return row.SourceName == sourceName }), nil
|
||||
}
|
||||
|
||||
func (m *mockVariantFileRepo) filtered(keep func(domain.VariantFile) bool) []domain.VariantFile {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
m.calls++
|
||||
kept := []domain.VariantFile{}
|
||||
for _, row := range m.rows {
|
||||
if keep(row) {
|
||||
kept = append(kept, row)
|
||||
}
|
||||
}
|
||||
sort.Slice(kept, func(i, j int) bool { return kept[i].ID < kept[j].ID })
|
||||
return kept
|
||||
}
|
||||
|
||||
func (m *mockVariantFileRepo) Delete(id uint32) error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
m.calls++
|
||||
m.deleted = append(m.deleted, id)
|
||||
delete(m.rows, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockVariantFileRepo) Totals() (map[string]domain.VariantFileTotals, error) {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
m.calls++
|
||||
totals := map[string]domain.VariantFileTotals{}
|
||||
for _, row := range m.rows {
|
||||
current := totals[row.VariantName]
|
||||
totals[row.VariantName] = domain.VariantFileTotals{Files: current.Files + 1, Bytes: current.Bytes + row.Size}
|
||||
}
|
||||
return totals, nil
|
||||
}
|
||||
|
||||
func (m *mockVariantFileRepo) rowsOf(variantName string) []domain.VariantFile {
|
||||
rows, _ := m.ListByVariant(variantName)
|
||||
return rows
|
||||
}
|
||||
|
||||
func (m *mockVariantFileRepo) rowCount() int {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
return len(m.rows)
|
||||
}
|
||||
|
||||
type mockVariantStorage struct {
|
||||
mutex sync.Mutex
|
||||
blobs map[string][]byte
|
||||
puts int
|
||||
opens int
|
||||
openCounts map[string]int
|
||||
nextName int
|
||||
deleted []string
|
||||
failDelete map[string]error
|
||||
}
|
||||
|
||||
var _ domain.Storage = &mockVariantStorage{}
|
||||
|
||||
func newMockVariantStorage() *mockVariantStorage {
|
||||
return &mockVariantStorage{blobs: map[string][]byte{}, failDelete: map[string]error{}, openCounts: map[string]int{}}
|
||||
}
|
||||
|
||||
func (m *mockVariantStorage) Put(contentType string, content io.Reader) (string, error) {
|
||||
data, err := io.ReadAll(content)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
m.puts++
|
||||
m.nextName++
|
||||
name := "gen-" + strconv.Itoa(m.nextName) + variantStorageExtensions[contentType]
|
||||
m.blobs[name] = data
|
||||
return name, nil
|
||||
}
|
||||
|
||||
func (m *mockVariantStorage) Open(storedName string) (domain.Blob, error) {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
m.opens++
|
||||
m.openCounts[storedName]++
|
||||
data, found := m.blobs[storedName]
|
||||
if !found || strings.Contains(storedName, "/") {
|
||||
return domain.Blob{}, domain.ErrBlobNotFound
|
||||
}
|
||||
return domain.Blob{Content: io.NopCloser(bytes.NewReader(data)), Size: int64(len(data)), ModTime: time.Date(2026, 10, 8, 9, 0, 0, 0, time.UTC)}, nil
|
||||
}
|
||||
|
||||
func (m *mockVariantStorage) Delete(storedName string) error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
m.deleted = append(m.deleted, storedName)
|
||||
if err := m.failDelete[storedName]; err != nil {
|
||||
return err
|
||||
}
|
||||
delete(m.blobs, storedName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockVariantStorage) seed(storedName string, content []byte) {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
m.blobs[storedName] = content
|
||||
}
|
||||
|
||||
func (m *mockVariantStorage) has(storedName string) bool {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
_, found := m.blobs[storedName]
|
||||
return found
|
||||
}
|
||||
|
||||
func (m *mockVariantStorage) putCount() int {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
return m.puts
|
||||
}
|
||||
|
||||
func (m *mockVariantStorage) deletedNames() []string {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
return append([]string(nil), m.deleted...)
|
||||
}
|
||||
|
||||
type countingRenderer struct {
|
||||
inner domain.VariantRenderer
|
||||
calls atomic.Int32
|
||||
failFirst bool
|
||||
onRender func()
|
||||
}
|
||||
|
||||
var _ domain.VariantRenderer = &countingRenderer{}
|
||||
|
||||
func newCountingRenderer() *countingRenderer {
|
||||
return &countingRenderer{inner: imaging.NewVariantRenderer()}
|
||||
}
|
||||
|
||||
func newCountingRendererWithoutWebP() *countingRenderer {
|
||||
return &countingRenderer{inner: imaging.NewVariantRendererEncoding(domain.FormatOriginal, domain.FormatJPEG, domain.FormatPNG)}
|
||||
}
|
||||
|
||||
func (c *countingRenderer) Render(source io.Reader, settings domain.VariantSettings) (domain.RenderedVariant, error) {
|
||||
call := c.calls.Add(1)
|
||||
if c.onRender != nil {
|
||||
c.onRender()
|
||||
}
|
||||
if c.failFirst && call == 1 {
|
||||
return domain.RenderedVariant{}, errors.New("render failure")
|
||||
}
|
||||
return c.inner.Render(source, settings)
|
||||
}
|
||||
|
||||
func (c *countingRenderer) DeclaredSize(source io.Reader) (int, int, error) {
|
||||
return c.inner.DeclaredSize(source)
|
||||
}
|
||||
|
||||
func defaultServeLimits(maxSourceBytes int64) useCases.ServeVariantLimits {
|
||||
return useCases.ServeVariantLimits{MaxSourceBytes: maxSourceBytes, MaxSourcePixels: variantDefaultPixels, MaxConcurrentRenders: variantDefaultRenders}
|
||||
}
|
||||
|
||||
func (c *countingRenderer) EncodableFormats() []domain.VariantFormat {
|
||||
return c.inner.EncodableFormats()
|
||||
}
|
||||
|
||||
func (c *countingRenderer) renderCount() int {
|
||||
return int(c.calls.Load())
|
||||
}
|
||||
|
||||
func seedVariant(repo *mockVariantRepo, name string, width int, height int, fit domain.VariantFit, format domain.VariantFormat, quality int) domain.Variant {
|
||||
settings := domain.VariantSettings{Fit: fit, Format: format}
|
||||
if width > 0 {
|
||||
settings.Width = &width
|
||||
}
|
||||
if height > 0 {
|
||||
settings.Height = &height
|
||||
}
|
||||
if format != domain.FormatPNG {
|
||||
settings.Quality = &quality
|
||||
}
|
||||
stamp := time.Date(2026, 10, 7, 8, 0, 0, 0, time.UTC)
|
||||
variant, err := domain.NewVariant(name, settings, false, true, stamp)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
repo.variants[name] = variant
|
||||
return variant
|
||||
}
|
||||
|
||||
func seedVariantFiles(files *mockVariantFileRepo, storage *mockVariantStorage, variantName string, sourceName string, count int, size int64) []domain.VariantFile {
|
||||
seeded := []domain.VariantFile{}
|
||||
for index := 0; index < count; index++ {
|
||||
cached := "seed-" + variantName + "-" + sourceName + "-" + strconv.Itoa(index) + ".jpeg"
|
||||
storage.seed(cached, []byte("generated"))
|
||||
saved, _ := files.Save(domain.VariantFile{
|
||||
VariantName: variantName,
|
||||
SourceName: sourceName + "-" + strconv.Itoa(index),
|
||||
CachedName: cached,
|
||||
ContentType: "image/jpeg",
|
||||
Size: size,
|
||||
Fingerprint: "seeded",
|
||||
})
|
||||
seeded = append(seeded, saved)
|
||||
}
|
||||
return seeded
|
||||
}
|
||||
|
||||
func newInertServeVariantUseCase(storage domain.Storage) *useCases.ServeVariantUseCase {
|
||||
return useCases.NewServeVariantUseCase(newMockVariantRepo(), newMockVariantFileRepo(), newMockImageRepo(), storage, newCountingRenderer(), defaultServeLimits(variantDefaultCap))
|
||||
}
|
||||
|
||||
func (m *mockVariantStorage) openCountOf(storedName string) int {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
return m.openCounts[storedName]
|
||||
}
|
||||
Reference in new issue
Block a user