Merge branch 'develop' into feature/rate-limit
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
namespace Butter.Dtos.Album;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request to merge multiple albums into a single destination album.
|
||||
/// </summary>
|
||||
public class AlbumMergeDto {
|
||||
/// <summary>
|
||||
/// Gets or sets the ID of the destination album that will receive all assets.
|
||||
/// Must not be present in <see cref="SourceIds"/>.
|
||||
/// </summary>
|
||||
public required Guid DestinationId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of source album IDs whose assets will be moved to the destination.
|
||||
/// Source albums will be hard-deleted after the merge.
|
||||
/// </summary>
|
||||
public required List<Guid> SourceIds { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace Butter.Dtos.Person;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request to merge multiple people into a single destination person.
|
||||
/// </summary>
|
||||
public class PersonMergeDto {
|
||||
/// <summary>
|
||||
/// Gets or sets the ID of the destination person that will receive all associated data.
|
||||
/// Must not be present in <see cref="SourceIds"/>.
|
||||
/// </summary>
|
||||
public required Guid DestinationId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of source person IDs whose albums, faces, and maintainers
|
||||
/// will be reassigned to the destination. Source people will be hard-deleted.
|
||||
/// </summary>
|
||||
public required List<Guid> SourceIds { get; set; } = [];
|
||||
}
|
||||
@@ -186,6 +186,26 @@ public class AlbumController(
|
||||
return Ok(album.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merges multiple source albums into a destination album.
|
||||
/// All assets from source albums are moved to the destination, then sources are hard-deleted.
|
||||
/// </summary>
|
||||
/// <param name="dto">The merge request with destination and source IDs.</param>
|
||||
/// <returns>200 on success, 400 if destination is in sources or not found.</returns>
|
||||
[HttpPost("merge")]
|
||||
[Authorize(Roles = "Curator,Admin")]
|
||||
public ActionResult Merge([FromBody] AlbumMergeDto dto) {
|
||||
if (dto.SourceIds.Contains(dto.DestinationId))
|
||||
return BadRequest("Destination album ID must not appear in source album IDs.");
|
||||
|
||||
if (albumRepository.Find(dto.DestinationId) == null)
|
||||
return NotFound("Destination album not found.");
|
||||
|
||||
albumRepository.MergeAlbums(dto.DestinationId, dto.SourceIds);
|
||||
albumRepository.Save();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes multiple albums (soft delete).
|
||||
/// </summary>
|
||||
|
||||
@@ -112,6 +112,7 @@ public class PersonController(
|
||||
[HttpPost]
|
||||
[Authorize(Roles = "Admin, Curator")]
|
||||
public IActionResult BulkUpdate([FromBody] BulkDto<PersonUpdateDto> personBulkDto) {
|
||||
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
||||
var people = personRepository.FindBulk(personBulkDto.Ids).ToList();
|
||||
|
||||
foreach (var person in people) {
|
||||
@@ -120,7 +121,11 @@ public class PersonController(
|
||||
person.ProfileCropX = personBulkDto.Data.ProfileCropX;
|
||||
person.ProfileCropY = personBulkDto.Data.ProfileCropY;
|
||||
person.ProfileCropZoom = personBulkDto.Data.ProfileCropZoom;
|
||||
person.Visibility = personBulkDto.Data.Visibility ?? person.Visibility;
|
||||
person.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
if (personBulkDto.Data.MaintainerUserIds != null && accessLevel >= EAccessLevel.Curator)
|
||||
personRepository.SetMaintainers(person.Id, personBulkDto.Data.MaintainerUserIds);
|
||||
}
|
||||
|
||||
personRepository.Save();
|
||||
@@ -180,6 +185,26 @@ public class PersonController(
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merges multiple source people into a destination person.
|
||||
/// Albums, faces, and maintainers are reassigned to the destination, then source people are hard-deleted.
|
||||
/// </summary>
|
||||
/// <param name="dto">The merge request with destination and source IDs.</param>
|
||||
/// <returns>200 on success, 400 if destination is in sources or not found.</returns>
|
||||
[HttpPost("merge")]
|
||||
[Authorize(Roles = "Admin, Curator")]
|
||||
public IActionResult Merge([FromBody] PersonMergeDto dto) {
|
||||
if (dto.SourceIds.Contains(dto.DestinationId))
|
||||
return BadRequest("Destination person ID must not appear in source person IDs.");
|
||||
|
||||
if (personRepository.Find(dto.DestinationId) == null)
|
||||
return NotFound("Destination person not found.");
|
||||
|
||||
personRepository.MergePeople(dto.DestinationId, dto.SourceIds);
|
||||
personRepository.Save();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new person.
|
||||
/// </summary>
|
||||
|
||||
@@ -176,6 +176,33 @@ public class AlbumRepository(LactoseDbContext context) : IAlbumRepository {
|
||||
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);
|
||||
|
||||
|
||||
@@ -70,6 +70,14 @@ public interface IAlbumRepository : IDisposable {
|
||||
/// <returns>A collection of albums created or updated within the specified date range.</returns>
|
||||
public IEnumerable<Album> FindByDateRange(DateTime from, DateTime to);
|
||||
|
||||
/// <summary>
|
||||
/// Merges multiple source albums into a destination album.
|
||||
/// All assets from source albums are moved to the destination, then source albums are hard-deleted.
|
||||
/// </summary>
|
||||
/// <param name="destId">The ID of the destination album.</param>
|
||||
/// <param name="sourceIds">The IDs of the source albums to merge.</param>
|
||||
public void MergeAlbums(Guid destId, List<Guid> sourceIds);
|
||||
|
||||
/// <summary>
|
||||
/// Removes an album from the database.
|
||||
/// </summary>
|
||||
|
||||
@@ -89,6 +89,14 @@ public interface IPersonRepository : IDisposable {
|
||||
/// <returns>A collection of matching people.</returns>
|
||||
IEnumerable<Person> FindBulk(IEnumerable<Guid> ids);
|
||||
|
||||
/// <summary>
|
||||
/// Merges multiple source people into a destination person.
|
||||
/// Albums, faces, and maintainers are reassigned to the destination, then source people are hard-deleted.
|
||||
/// </summary>
|
||||
/// <param name="destId">The ID of the destination person.</param>
|
||||
/// <param name="sourceIds">The IDs of the source people to merge.</param>
|
||||
void MergePeople(Guid destId, List<Guid> sourceIds);
|
||||
|
||||
/// <summary>
|
||||
/// Removes a person from the database.
|
||||
/// </summary>
|
||||
|
||||
@@ -227,6 +227,39 @@ public class PersonRepository(LactoseDbContext context) : IPersonRepository {
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void MergePeople(Guid destId, List<Guid> sourceIds) {
|
||||
var dest = context.People
|
||||
.AsSplitQuery()
|
||||
.Include(p => p.Maintainers)
|
||||
.FirstOrDefault(p => p.Id == destId);
|
||||
if (dest == null) return;
|
||||
|
||||
// Reassign albums
|
||||
var albums = context.Albums.Where(a => a.PersonOwnerId != null && sourceIds.Contains(a.PersonOwnerId.Value)).ToList();
|
||||
foreach (var album in albums)
|
||||
album.PersonOwnerId = destId;
|
||||
|
||||
// Reassign faces
|
||||
var faces = context.Faces.Where(f => f.PersonId.HasValue && sourceIds.Contains(f.PersonId.Value)).ToList();
|
||||
foreach (var face in faces)
|
||||
face.PersonId = destId;
|
||||
|
||||
// Reassign maintainers
|
||||
var existingMaintainerIds = dest.Maintainers?.Select(m => m.Id).ToHashSet() ?? [];
|
||||
var sourceMaintainers = context.PersonMaintainers
|
||||
.Where(pm => sourceIds.Contains(pm.PersonId))
|
||||
.ToList();
|
||||
foreach (var pm in sourceMaintainers) {
|
||||
if (!existingMaintainerIds.Contains(pm.UserId))
|
||||
context.PersonMaintainers.Add(new PersonMaintainer { PersonId = destId, UserId = pm.UserId });
|
||||
}
|
||||
|
||||
// Hard-delete source people
|
||||
var people = context.People.Where(p => sourceIds.Contains(p.Id)).ToList();
|
||||
context.People.RemoveRange(people);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose() => context.Dispose();
|
||||
}
|
||||
|
||||
+475
-115
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user