package persistence import ( domain "RenewCMS/internal/domain/image" "RenewCMS/internal/infrastructure/persistence/mappers" entity "RenewCMS/internal/infrastructure/persistence/models" "errors" "io" "os" "path/filepath" "strings" "github.com/google/uuid" "gorm.io/gorm" ) const uploadedImagesURLPrefix = "/static/uploadedImages/" type ImageRepository struct { db *gorm.DB uploadDir string } func NewImageRepository(db *gorm.DB, uploadDir string) *ImageRepository { return &ImageRepository{db, uploadDir} } var contentTypeExtensions = map[string]string{ "image/png": ".png", "image/jpeg": ".jpeg", "image/webp": ".webp", "image/svg+xml": ".svg", } func (i ImageRepository) Create(input domain.ImageInput) (domain.Image, error) { fileBytes, err := io.ReadAll(input.Content) if err != nil { return domain.Image{}, err } err = os.MkdirAll(i.uploadDir, 0755) if err != nil { return domain.Image{}, err } newName := uuid.NewString() + contentTypeExtensions[input.ContentType] finalPath := filepath.Join(i.uploadDir, newName) err = os.WriteFile(finalPath, fileBytes, 0666) if err != nil { return domain.Image{}, err } model := mappers.ImageToModel(domain.Image{ Path: uploadedImagesURLPrefix + newName, Filename: input.Filename, Size: input.Size, ContentType: input.ContentType, Width: input.Width, Height: input.Height, SHA256: input.SHA256, UploadedByID: input.UploadedByID, }) if err := i.db.Create(&model).Error; err != nil { _ = os.Remove(finalPath) return domain.Image{}, err } return mappers.ImageToDomain(model), nil } func (i ImageRepository) Get(id uint32) (domain.Image, error) { var model entity.Image err := i.db.First(&model, id).Error if err != nil { return domain.Image{}, mapNotFoundErr(err, domain.ErrImageNotFound) } return mappers.ImageToDomain(model), nil } func (i ImageRepository) Delete(id uint32) error { var model entity.Image err := i.db.First(&model, id).Error if err != nil { return mapNotFoundErr(err, domain.ErrImageNotFound) } err = os.Remove(filepath.Join(i.uploadDir, filepath.Base(model.Path))) if err != nil && !errors.Is(err, os.ErrNotExist) { return err } return i.db.Delete(&entity.Image{}, id).Error } func (i ImageRepository) List(query domain.SearchQuery) ([]domain.Image, error) { statement := i.db.Model(&entity.Image{}) if query.Search != "" { statement = statement.Where("instr(lower(filename), ?) > 0", strings.ToLower(query.Search)) } if query.Hash != "" { statement = statement.Where("sha256 = ?", query.Hash) } var models []entity.Image if err := statement.Order("id asc").Find(&models).Error; err != nil { return nil, err } return imagesToDomain(models), nil } func (i ImageRepository) FindByHash(hash string) ([]domain.Image, error) { if hash == "" { return []domain.Image{}, nil } var models []entity.Image if err := i.db.Where("sha256 = ?", hash).Order("id asc").Find(&models).Error; err != nil { return nil, err } return imagesToDomain(models), nil } func (i ImageRepository) Rename(id uint32, filename string) (domain.Image, error) { var model entity.Image err := i.db.First(&model, id).Error if err != nil { return domain.Image{}, mapNotFoundErr(err, domain.ErrImageNotFound) } model.Filename = filename if err := i.db.Save(&model).Error; err != nil { return domain.Image{}, err } return mappers.ImageToDomain(model), nil } func (i ImageRepository) ListDuplicates() ([]domain.Image, error) { var models []entity.Image err := i.db.Where("sha256 IN (?)", i.duplicatedHashes()).Order("sha256 asc, id asc").Find(&models).Error if err != nil { return nil, err } return imagesToDomain(models), nil } func (i ImageRepository) Counts() (domain.Counts, error) { var all int64 if err := i.db.Model(&entity.Image{}).Count(&all).Error; err != nil { return domain.Counts{}, err } var groups int64 if err := i.db.Table("(?) as duplicated", i.duplicatedHashes()).Count(&groups).Error; err != nil { return domain.Counts{}, err } return domain.Counts{All: int(all), DuplicateGroups: int(groups)}, nil } func (i ImageRepository) duplicatedHashes() *gorm.DB { return i.db.Model(&entity.Image{}). Select("sha256"). Where("sha256 <> ''"). Group("sha256"). Having("COUNT(*) > 1") } func imagesToDomain(models []entity.Image) []domain.Image { images := make([]domain.Image, 0, len(models)) for _, model := range models { images = append(images, mappers.ImageToDomain(model)) } return images } var _ domain.Repository = &ImageRepository{}