feat: image variants cache panel

This commit is contained in:
fsylvain committed 2026-10-08 21:13:32 +02:00
1 parent 951fef18ba
commit d768668014
53 files changed
+5809 -46

No files matched your search

@@ -107,6 +107,21 @@ func (i ImageRepository) ExistsByStoredName(storedName string) (bool, error) {
return matches > 0, err
}
func (i ImageRepository) ListByStoredNames(storedNames []string) ([]domain.Image, error) {
if len(storedNames) == 0 {
return []domain.Image{}, nil
}
paths := make([]string, 0, len(storedNames))
for _, storedName := range storedNames {
paths = append(paths, domain.PublicPath(storedName))
}
var models []entity.Image
if err := i.db.Where("path IN ?", paths).Order("id asc").Find(&models).Error; err != nil {
return nil, err
}
return imagesToDomain(models), nil
}
func (i ImageRepository) duplicatedHashes() *gorm.DB {
return i.db.Model(&entity.Image{}).
Select("sha256").
@@ -15,6 +15,7 @@ type VariantFileRepository struct {
}
var _ domain.VariantFileRepository = &VariantFileRepository{}
var _ domain.VariantCacheRepository = &VariantFileRepository{}
func NewVariantFileRepository(db *gorm.DB) *VariantFileRepository {
return &VariantFileRepository{db}
@@ -90,6 +91,76 @@ func (v *VariantFileRepository) TotalsBySource() (map[string]domain.VariantFileT
return totals, nil
}
const (
unusedCondition = "variant_name IN ? AND COALESCE(last_requested_at, created_at) < ?"
topSourcesOrder = "bytes DESC, source_name ASC"
)
func (v *VariantFileRepository) UnusedTotals(filter domain.UnusedFilter) (domain.VariantFileTotals, error) {
if len(filter.VariantNames) == 0 {
return domain.VariantFileTotals{}, nil
}
var row struct {
Files int
Bytes int64
}
err := v.db.Model(&entity.ImageVariantFile{}).
Select("COUNT(*) AS files, COALESCE(SUM(size), 0) AS bytes").
Where(unusedCondition, filter.VariantNames, filter.Cutoff.UTC()).
Scan(&row).Error
if err != nil {
return domain.VariantFileTotals{}, err
}
return domain.VariantFileTotals{Files: row.Files, Bytes: row.Bytes}, nil
}
func (v *VariantFileRepository) TopSources(limit int) ([]domain.SourceTotals, error) {
var rows []struct {
SourceName string
Files int
Bytes int64
}
err := v.db.Model(&entity.ImageVariantFile{}).
Select("source_name, COUNT(*) AS files, COALESCE(SUM(size), 0) AS bytes").
Group("source_name").
Order(topSourcesOrder).
Limit(limit).
Scan(&rows).Error
if err != nil {
return nil, err
}
sources := make([]domain.SourceTotals, 0, len(rows))
for _, row := range rows {
sources = append(sources, domain.SourceTotals{SourceName: row.SourceName, Files: row.Files, Bytes: row.Bytes})
}
return sources, nil
}
func (v *VariantFileRepository) ListPage(filter domain.VariantFileFilter, afterID uint32, limit int) ([]domain.VariantFile, error) {
statement := v.db.Where("id > ?", afterID)
if filter.VariantName != "" {
statement = statement.Where("variant_name = ?", filter.VariantName)
}
if filter.SourceName != "" {
statement = statement.Where("source_name = ?", filter.SourceName)
}
if filter.Unused != nil {
if len(filter.Unused.VariantNames) == 0 {
return []domain.VariantFile{}, nil
}
statement = statement.Where(unusedCondition, filter.Unused.VariantNames, filter.Unused.Cutoff.UTC())
}
var found []entity.ImageVariantFile
if err := statement.Order("id asc").Limit(limit).Find(&found).Error; err != nil {
return nil, err
}
files := make([]domain.VariantFile, 0, len(found))
for _, model := range found {
files = append(files, mappers.VariantFileToDomain(model))
}
return files, nil
}
func (v *VariantFileRepository) Totals() (map[string]domain.VariantFileTotals, error) {
var rows []struct {
VariantName string
@@ -0,0 +1,64 @@
package persistence
import (
domain "RenewCMS/internal/domain/image"
entity "RenewCMS/internal/infrastructure/persistence/models"
"time"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type VariantUsageRepository struct {
db *gorm.DB
}
var _ domain.VariantUsageRepository = &VariantUsageRepository{}
func NewVariantUsageRepository(db *gorm.DB) *VariantUsageRepository {
return &VariantUsageRepository{db}
}
func (v *VariantUsageRepository) Record(batch domain.VariantUsageBatch) error {
return v.db.Transaction(func(tx *gorm.DB) error {
for _, hit := range batch.Hits {
if err := recordHit(tx, hit); err != nil {
return err
}
}
for _, rejection := range batch.Rejections {
if err := recordRejections(tx, rejection); err != nil {
return err
}
}
if batch.PruneBefore.IsZero() {
return nil
}
return tx.Where("day < ?", batch.PruneBefore.UTC().Format(domain.RejectionDayLayout)).Delete(&entity.ImageVariantRejectionDay{}).Error
})
}
func recordHit(tx *gorm.DB, hit domain.VariantHit) error {
at := hit.At.UTC()
return tx.Model(&entity.ImageVariantFile{}).
Where("variant_name = ? AND source_name = ?", hit.VariantName, hit.SourceName).
Where("last_requested_at IS NULL OR last_requested_at < ?", at).
Update("last_requested_at", at).Error
}
func recordRejections(tx *gorm.DB, rejection domain.RejectionCount) error {
row := entity.ImageVariantRejectionDay{Day: rejection.Day.UTC().Format(domain.RejectionDayLayout), Count: rejection.Count}
return tx.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "day"}},
DoUpdates: clause.Assignments(map[string]any{"count": gorm.Expr("? + ?", clause.Column{Table: clause.CurrentTable, Name: "count"}, rejection.Count)}),
}).Create(&row).Error
}
func (v *VariantUsageRepository) RejectedSince(day time.Time) (int, error) {
var total int
err := v.db.Model(&entity.ImageVariantRejectionDay{}).
Select("COALESCE(SUM(count), 0)").
Where("day >= ?", day.UTC().Format(domain.RejectionDayLayout)).
Scan(&total).Error
return total, err
}