package useCases import ( "RenewCMS/internal/domain/article" "RenewCMS/internal/domain/image" "RenewCMS/internal/domain/user" "encoding/base64" "strconv" "strings" ) const cursorPrefix = "offset:" type MediaItem struct { Image image.Image UploadedBy string UsedIn int Duplicate bool } type MediaCounts struct { All int Unused int Duplicates int } type MediaPage struct { Items []MediaItem NextCursor string Counts MediaCounts } type ListMediaUseCase struct { imageRepository image.Repository articleRepository article.Repository userRepository user.Repository } func NewListMediaUseCase(imageRepository image.Repository, articleRepository article.Repository, userRepository user.Repository) *ListMediaUseCase { return &ListMediaUseCase{imageRepository, articleRepository, userRepository} } func (l *ListMediaUseCase) List(query image.ListQuery) (MediaPage, error) { if err := query.Validate(); err != nil { return MediaPage{}, err } offset, err := decodeCursor(query.Cursor) if err != nil { return MediaPage{}, err } usage, err := l.usageCounter() if err != nil { return MediaPage{}, err } duplicateHashes, err := l.duplicateHashes() if err != nil { return MediaPage{}, err } counts, err := l.counts(usage) if err != nil { return MediaPage{}, err } images, err := l.imageRepository.List(image.SearchQuery{Search: strings.TrimSpace(query.Search), Hash: query.Hash}) if err != nil { return MediaPage{}, err } image.SortImages(images, query.Sort) matching := make([]image.Image, 0, len(images)) for _, img := range images { if matchesFilter(query.Filter, usage(img), duplicateHashes[img.SHA256] && img.SHA256 != "") { matching = append(matching, img) } } pageEnd := min(offset+query.Limit, len(matching)) pageStart := min(offset, pageEnd) names := newUploaderNames(l.userRepository) items := []MediaItem{} for _, img := range matching[pageStart:pageEnd] { uploadedBy, err := names.nameOf(img.UploadedByID) if err != nil { return MediaPage{}, err } items = append(items, MediaItem{ Image: img, UploadedBy: uploadedBy, UsedIn: usage(img), Duplicate: img.SHA256 != "" && duplicateHashes[img.SHA256], }) } page := MediaPage{Items: items, Counts: counts} if pageEnd < len(matching) { page.NextCursor = encodeCursor(pageEnd) } return page, nil } func (l *ListMediaUseCase) Describe(img image.Image) (MediaItem, error) { articles, err := l.articleRepository.FindByBodyContaining(image.StoredName(img.Path)) if err != nil { return MediaItem{}, err } uploadedBy, err := newUploaderNames(l.userRepository).nameOf(img.UploadedByID) if err != nil { return MediaItem{}, err } duplicate := false if img.SHA256 != "" { sameContent, err := l.imageRepository.FindByHash(img.SHA256) if err != nil { return MediaItem{}, err } duplicate = len(sameContent) > 1 } return MediaItem{Image: img, UploadedBy: uploadedBy, UsedIn: len(articles), Duplicate: duplicate}, nil } func (l *ListMediaUseCase) usageCounter() (func(image.Image) int, error) { articles, err := l.articleRepository.FindByFilters(article.Filters{}) if err != nil { return nil, err } return func(img image.Image) int { storedName := image.StoredName(img.Path) used := 0 for _, a := range articles { if strings.Contains(a.Body, storedName) { used++ } } return used }, nil } func (l *ListMediaUseCase) duplicateHashes() (map[string]bool, error) { duplicates, err := l.imageRepository.ListDuplicates() if err != nil { return nil, err } hashes := make(map[string]bool, len(duplicates)) for _, img := range duplicates { hashes[img.SHA256] = true } return hashes, nil } func (l *ListMediaUseCase) counts(usage func(image.Image) int) (MediaCounts, error) { libraryCounts, err := l.imageRepository.Counts() if err != nil { return MediaCounts{}, err } everything, err := l.imageRepository.List(image.SearchQuery{}) if err != nil { return MediaCounts{}, err } unused := 0 for _, img := range everything { if usage(img) == 0 { unused++ } } return MediaCounts{All: libraryCounts.All, Unused: unused, Duplicates: libraryCounts.DuplicateGroups}, nil } func matchesFilter(filter image.Filter, usedIn int, duplicate bool) bool { switch filter { case image.FilterUnused: return usedIn == 0 case image.FilterDuplicates: return duplicate default: return true } } func encodeCursor(offset int) string { return base64.RawURLEncoding.EncodeToString([]byte(cursorPrefix + strconv.Itoa(offset))) } func decodeCursor(cursor string) (int, error) { if cursor == "" { return 0, nil } raw, err := base64.RawURLEncoding.DecodeString(cursor) if err != nil || !strings.HasPrefix(string(raw), cursorPrefix) { return 0, image.ErrInvalidQuery } offset, err := strconv.Atoi(strings.TrimPrefix(string(raw), cursorPrefix)) if err != nil || offset < 0 { return 0, image.ErrInvalidQuery } return offset, nil }