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);
|
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>
|
/// <summary>
|
||||||
/// Removes multiple albums (soft delete).
|
/// Removes multiple albums (soft delete).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ public class PersonController(
|
|||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Authorize(Roles = "Admin, Curator")]
|
[Authorize(Roles = "Admin, Curator")]
|
||||||
public IActionResult BulkUpdate([FromBody] BulkDto<PersonUpdateDto> personBulkDto) {
|
public IActionResult BulkUpdate([FromBody] BulkDto<PersonUpdateDto> personBulkDto) {
|
||||||
|
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
||||||
var people = personRepository.FindBulk(personBulkDto.Ids).ToList();
|
var people = personRepository.FindBulk(personBulkDto.Ids).ToList();
|
||||||
|
|
||||||
foreach (var person in people) {
|
foreach (var person in people) {
|
||||||
@@ -120,7 +121,11 @@ public class PersonController(
|
|||||||
person.ProfileCropX = personBulkDto.Data.ProfileCropX;
|
person.ProfileCropX = personBulkDto.Data.ProfileCropX;
|
||||||
person.ProfileCropY = personBulkDto.Data.ProfileCropY;
|
person.ProfileCropY = personBulkDto.Data.ProfileCropY;
|
||||||
person.ProfileCropZoom = personBulkDto.Data.ProfileCropZoom;
|
person.ProfileCropZoom = personBulkDto.Data.ProfileCropZoom;
|
||||||
|
person.Visibility = personBulkDto.Data.Visibility ?? person.Visibility;
|
||||||
person.UpdatedAt = DateTime.UtcNow;
|
person.UpdatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
if (personBulkDto.Data.MaintainerUserIds != null && accessLevel >= EAccessLevel.Curator)
|
||||||
|
personRepository.SetMaintainers(person.Id, personBulkDto.Data.MaintainerUserIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
personRepository.Save();
|
personRepository.Save();
|
||||||
@@ -180,6 +185,26 @@ public class PersonController(
|
|||||||
return Ok();
|
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>
|
/// <summary>
|
||||||
/// Creates a new person.
|
/// Creates a new person.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -176,6 +176,33 @@ public class AlbumRepository(LactoseDbContext context) : IAlbumRepository {
|
|||||||
public IEnumerable<Album> FindByDateRange(DateTime from, DateTime to)
|
public IEnumerable<Album> FindByDateRange(DateTime from, DateTime to)
|
||||||
=> context.Albums.Where(x => x.CreatedAt >= from && x.UpdatedAt <= 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 />
|
/// <inheritdoc />
|
||||||
public void Remove(Album album) => context.Albums.Remove(album);
|
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>
|
/// <returns>A collection of albums created or updated within the specified date range.</returns>
|
||||||
public IEnumerable<Album> FindByDateRange(DateTime from, DateTime to);
|
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>
|
/// <summary>
|
||||||
/// Removes an album from the database.
|
/// Removes an album from the database.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -89,6 +89,14 @@ public interface IPersonRepository : IDisposable {
|
|||||||
/// <returns>A collection of matching people.</returns>
|
/// <returns>A collection of matching people.</returns>
|
||||||
IEnumerable<Person> FindBulk(IEnumerable<Guid> ids);
|
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>
|
/// <summary>
|
||||||
/// Removes a person from the database.
|
/// Removes a person from the database.
|
||||||
/// </summary>
|
/// </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 />
|
/// <inheritdoc />
|
||||||
public void Dispose() => context.Dispose();
|
public void Dispose() => context.Dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
+475
-115
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user