213 lines
8.5 KiB
C#
213 lines
8.5 KiB
C#
using Butter.Dtos;
|
|
using Butter.Dtos.Album;
|
|
using Butter.Types;
|
|
using Lactose.Context;
|
|
using Lactose.Mapper;
|
|
using Lactose.Models;
|
|
|
|
namespace Lactose.Repositories;
|
|
|
|
/// <inheritdoc />
|
|
public class AlbumRepository(LactoseDbContext context) : IAlbumRepository {
|
|
/// <inheritdoc />
|
|
public void Insert(Album album) => context.Albums.Add(album);
|
|
|
|
/// <inheritdoc />
|
|
public void Update(Album album) {
|
|
album.UpdatedAt = DateTime.UtcNow;
|
|
context.Albums.Update(album);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void UpdateBulk(IEnumerable<Album> albums) {
|
|
foreach (var album in albums) {
|
|
album.UpdatedAt = DateTime.UtcNow;
|
|
context.Albums.Update(album);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public IEnumerable<AlbumPreviewDto> SearchQuery(string query, int page = 0, int pageSize = PagedParametersDto.MaxPageSize, string? sortBy = null, bool sortAsc = false, bool unassigned = false, Guid userId = default, EAccessLevel accessLevel = EAccessLevel.User, Guid? uploadedBy = null) {
|
|
IQueryable<Album> albumsQuery = context.Albums;
|
|
|
|
if (!string.IsNullOrEmpty(query))
|
|
albumsQuery = albumsQuery.Where(x => EF.Functions.ILike(x.Title, $"%{query}%"));
|
|
|
|
if (unassigned)
|
|
albumsQuery = albumsQuery.Where(a => a.PersonOwnerId == null);
|
|
|
|
if (uploadedBy.HasValue)
|
|
albumsQuery = albumsQuery.Where(a => a.Assets!.Any(asset => asset.UploadedBy == uploadedBy.Value));
|
|
|
|
// Apply visibility filter scoped to access level
|
|
albumsQuery = accessLevel switch {
|
|
< EAccessLevel.Maintainer => albumsQuery.Where(a => a.Visibility <= EVisibility.Protected),
|
|
EAccessLevel.Maintainer => albumsQuery.Where(a => a.Visibility <= EVisibility.Protected
|
|
|| context.PersonMaintainers.Any(pm => pm.UserId == userId && pm.PersonId == a.PersonOwnerId)),
|
|
_ => albumsQuery
|
|
};
|
|
|
|
IOrderedQueryable<Album> ordered = sortBy?.ToLower() switch
|
|
{
|
|
"name" => sortAsc ? albumsQuery.OrderBy(a => a.Title) : albumsQuery.OrderByDescending(a => a.Title),
|
|
"updated" => sortAsc ? albumsQuery.OrderBy(a => a.UpdatedAt) : albumsQuery.OrderByDescending(a => a.UpdatedAt),
|
|
"assets" => sortAsc
|
|
? albumsQuery.OrderBy(a => a.Assets!.Count)
|
|
: albumsQuery.OrderByDescending(a => a.Assets!.Count),
|
|
"person" => sortAsc ? albumsQuery.OrderBy(a => a.PersonOwner!.Name) : albumsQuery.OrderByDescending(a => a.PersonOwner!.Name),
|
|
_ => sortAsc ? albumsQuery.OrderBy(a => a.CreatedAt) : albumsQuery.OrderByDescending(a => a.CreatedAt),
|
|
};
|
|
|
|
var pageOffset = page * pageSize;
|
|
var albumIds = ordered
|
|
.Skip(pageOffset)
|
|
.Take(pageSize)
|
|
.Select(a => a.Id)
|
|
.ToList();
|
|
|
|
if (albumIds.Count == 0)
|
|
return [];
|
|
|
|
var pagedAlbums = context.Albums
|
|
.Include(a => a.PersonOwner)
|
|
.Include(a => a.CoverAsset)
|
|
.Where(a => albumIds.Contains(a.Id))
|
|
.ToList();
|
|
|
|
// Compute visible asset count per album at the DB level
|
|
Dictionary<Guid, int> assetCounts;
|
|
if (accessLevel >= EAccessLevel.Curator) {
|
|
assetCounts = context.Albums
|
|
.Where(a => albumIds.Contains(a.Id))
|
|
.Select(a => new { a.Id, Count = a.Assets!.Count() })
|
|
.ToDictionary(x => x.Id, x => x.Count);
|
|
} else {
|
|
assetCounts = context.Albums
|
|
.Where(a => albumIds.Contains(a.Id))
|
|
.Select(a => new {
|
|
a.Id,
|
|
Count = a.Assets!.Count(asset => asset.DeletedAt == null && (
|
|
asset.Visibility == EVisibility.Public ||
|
|
(asset.Visibility == EVisibility.Protected && userId != default) ||
|
|
(asset.Visibility == EVisibility.Private && asset.UploadedBy == userId)
|
|
))
|
|
})
|
|
.ToDictionary(x => x.Id, x => x.Count);
|
|
}
|
|
|
|
var dtos = pagedAlbums.Select(a => {
|
|
var dto = a.ToAlbumPreviewDto(accessLevel, userId);
|
|
dto.AssetCount = assetCounts.GetValueOrDefault(a.Id, 0);
|
|
return dto;
|
|
}).ToList();
|
|
|
|
var orderMap = albumIds.Select((id, i) => (id, i)).ToDictionary(x => x.id, x => x.i);
|
|
return [.. dtos.OrderBy(d => orderMap.GetValueOrDefault(d.Id))];
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void Save() => context.SaveChanges();
|
|
|
|
/// <inheritdoc />
|
|
public Album? Find(Guid id) => context.Albums
|
|
.AsSplitQuery()
|
|
.Include(a => a.CoverAsset)
|
|
.Include(a => a.PersonOwner)
|
|
.Include(a => a.Assets)
|
|
.FirstOrDefault(a => a.Id == id);
|
|
|
|
/// <inheritdoc />
|
|
public Album? FindVisible(Guid id, Guid? userId, EAccessLevel accessLevel) {
|
|
var album = context.Albums
|
|
.AsSplitQuery()
|
|
.Include(a => a.CoverAsset)
|
|
.Include(a => a.PersonOwner)
|
|
.Include(a => a.Assets)
|
|
.FirstOrDefault(a => a.Id == id);
|
|
|
|
if (album == null) return null;
|
|
|
|
// R3: Gate album visibility
|
|
bool canSeeAlbum = accessLevel switch {
|
|
< EAccessLevel.Maintainer => album.Visibility <= EVisibility.Protected,
|
|
EAccessLevel.Maintainer when userId.HasValue => album.Visibility <= EVisibility.Protected
|
|
|| (album.PersonOwnerId.HasValue && context.PersonMaintainers.Any(pm =>
|
|
pm.UserId == userId.Value && pm.PersonId == album.PersonOwnerId.Value)),
|
|
EAccessLevel.Maintainer => album.Visibility <= EVisibility.Protected,
|
|
_ => true
|
|
};
|
|
|
|
if (!canSeeAlbum) return null;
|
|
|
|
if (album.Assets == null) return album;
|
|
|
|
album.Assets = accessLevel switch {
|
|
EAccessLevel.Admin => [.. album.Assets.OrderBy(a => a.OriginalFilename)],
|
|
EAccessLevel.Curator => album.Assets.Where(a => a.DeletedAt == null || a.UploadedBy == userId).OrderBy(a => a.OriginalFilename).ToList(),
|
|
EAccessLevel.Maintainer when userId.HasValue => [.. FilterAssetsForMaintainer(album, userId.Value).OrderBy(a => a.OriginalFilename)],
|
|
_ => album.Assets.Where(a => a.DeletedAt == null && (
|
|
a.Visibility == EVisibility.Public ||
|
|
(a.Visibility == EVisibility.Protected && userId.HasValue) ||
|
|
(a.Visibility == EVisibility.Private && a.UploadedBy == userId)
|
|
)).OrderBy(a => a.OriginalFilename).ToList()
|
|
};
|
|
|
|
return album;
|
|
}
|
|
|
|
private List<Asset> FilterAssetsForMaintainer(Album album, Guid userId) {
|
|
if (album.PersonOwnerId.HasValue && context.PersonMaintainers.Any(pm =>
|
|
pm.UserId == userId && pm.PersonId == album.PersonOwnerId.Value))
|
|
return album.Assets!.Where(a => a.DeletedAt == null).ToList();
|
|
|
|
return album.Assets!.Where(a => a.DeletedAt == null && (
|
|
a.Visibility == EVisibility.Public ||
|
|
a.Visibility == EVisibility.Protected ||
|
|
a.UploadedBy == userId
|
|
)).ToList();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public IEnumerable<Album> FindByPerson(Guid personId) => context.Albums.Where(a => a.PersonOwnerId == personId);
|
|
|
|
/// <inheritdoc />
|
|
public IEnumerable<Album> FindBulk(IEnumerable<Guid> ids) => context.Albums.Where(a => ids.Contains(a.Id));
|
|
|
|
/// <inheritdoc />
|
|
public IEnumerable<Album> FindByDateRange(DateTime from, DateTime to)
|
|
=> context.Albums.Where(x => x.CreatedAt >= from && x.UpdatedAt <= to);
|
|
|
|
/// <inheritdoc />
|
|
public void MergeAlbums(Guid destId, List<Guid> sourceIds) {
|
|
var dest = context.Albums
|
|
.AsSplitQuery()
|
|
.Include(a => a.Assets)
|
|
.FirstOrDefault(a => a.Id == destId);
|
|
if (dest == null) return;
|
|
|
|
var sources = context.Albums
|
|
.AsSplitQuery()
|
|
.Include(a => a.Assets)
|
|
.Where(a => sourceIds.Contains(a.Id))
|
|
.ToList();
|
|
|
|
var existingAssetIds = dest.Assets?.Select(a => a.Id).ToHashSet() ?? [];
|
|
|
|
foreach (var source in sources) {
|
|
if (source.Assets == null) continue;
|
|
foreach (var asset in source.Assets) {
|
|
if (existingAssetIds.Add(asset.Id))
|
|
dest.Assets?.Add(asset);
|
|
}
|
|
}
|
|
|
|
context.Albums.RemoveRange(sources);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void Remove(Album album) => context.Albums.Remove(album);
|
|
|
|
/// <inheritdoc />
|
|
public void Dispose() => context.Dispose();
|
|
}
|