package persistence import ( domain "RenewCMS/internal/domain/image" "RenewCMS/internal/infrastructure/persistence/mappers" entity "RenewCMS/internal/infrastructure/persistence/models" "errors" "gorm.io/gorm" "gorm.io/gorm/clause" ) type VariantFileRepository struct { db *gorm.DB } var _ domain.VariantFileRepository = &VariantFileRepository{} var _ domain.VariantCacheRepository = &VariantFileRepository{} func NewVariantFileRepository(db *gorm.DB) *VariantFileRepository { return &VariantFileRepository{db} } func (v *VariantFileRepository) Find(variantName string, sourceName string) (domain.VariantFile, bool, error) { var model entity.ImageVariantFile err := v.db.Where("variant_name = ? AND source_name = ?", variantName, sourceName).First(&model).Error if errors.Is(err, gorm.ErrRecordNotFound) { return domain.VariantFile{}, false, nil } if err != nil { return domain.VariantFile{}, false, err } return mappers.VariantFileToDomain(model), true, nil } func (v *VariantFileRepository) Save(file domain.VariantFile) (domain.VariantFile, error) { model := mappers.VariantFileToModel(file) model.ID = 0 err := v.db.Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "variant_name"}, {Name: "source_name"}}, DoUpdates: clause.AssignmentColumns([]string{"cached_name", "content_type", "size", "fingerprint"}), }).Create(&model).Error if err != nil { return domain.VariantFile{}, err } saved, _, err := v.Find(file.VariantName, file.SourceName) return saved, err } func (v *VariantFileRepository) ListByVariant(variantName string) ([]domain.VariantFile, error) { return v.list("variant_name = ?", variantName) } func (v *VariantFileRepository) ListBySource(sourceName string) ([]domain.VariantFile, error) { return v.list("source_name = ?", sourceName) } func (v *VariantFileRepository) list(condition string, value string) ([]domain.VariantFile, error) { var found []entity.ImageVariantFile if err := v.db.Where(condition, value).Order("id asc").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) Delete(id uint32) error { return v.db.Delete(&entity.ImageVariantFile{}, id).Error } func (v *VariantFileRepository) TotalsBySource() (map[string]domain.VariantFileTotals, 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"). Scan(&rows).Error if err != nil { return nil, err } totals := make(map[string]domain.VariantFileTotals, len(rows)) for _, row := range rows { totals[row.SourceName] = domain.VariantFileTotals{Files: row.Files, Bytes: row.Bytes} } 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 Files int Bytes int64 } err := v.db.Model(&entity.ImageVariantFile{}). Select("variant_name, COUNT(*) AS files, COALESCE(SUM(size), 0) AS bytes"). Group("variant_name"). Scan(&rows).Error if err != nil { return nil, err } totals := make(map[string]domain.VariantFileTotals, len(rows)) for _, row := range rows { totals[row.VariantName] = domain.VariantFileTotals{Files: row.Files, Bytes: row.Bytes} } return totals, nil }