Files
MilkyShots/Lactose/Repositories/AlbumRepository.cs
T

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();
}