feat(API): media library
This commit is contained in:
1 parent
95ae043d3e
commit
0d99544579
65 files changed
+3992
-1530
No files matched your search
@@ -0,0 +1,199 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user