Skip to content

Commit

Permalink
Hooked up bulk delete library
Browse files Browse the repository at this point in the history
  • Loading branch information
majora2007 committed Nov 27, 2024
1 parent 286a240 commit 1fb632b
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 16 deletions.
66 changes: 58 additions & 8 deletions API/Controllers/LibraryController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
using AutoMapper;
using EasyCaching.Core;
using Hangfire;
using Kavita.Common;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
Expand Down Expand Up @@ -447,12 +448,58 @@ public async Task<ActionResult> ScanFolder(ScanFolderDto dto)
return Ok();
}

/// <summary>
/// Deletes the library and all series within it.
/// </summary>
/// <remarks>This does not touch any files</remarks>
/// <param name="libraryId"></param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpDelete("delete")]
public async Task<ActionResult<bool>> DeleteLibrary(int libraryId)
{
_logger.LogInformation("Library {LibraryId} is being deleted by {UserName}", libraryId, User.GetUsername());

try
{
return Ok(await DeleteLibrary(libraryId, User.GetUserId()));
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}

/// <summary>
/// Deletes multiple libraries and all series within it.
/// </summary>
/// <remarks>This does not touch any files</remarks>
/// <param name="libraryIds"></param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpDelete("delete-multiple")]
public async Task<ActionResult<bool>> DeleteMultipleLibraries([FromQuery] List<int> libraryIds)
{
var username = User.GetUsername();
_logger.LogInformation("Library {LibraryId} is being deleted by {UserName}", libraryId, username);
_logger.LogInformation("Libraries {LibraryIds} are being deleted by {UserName}", libraryIds, username);

Check failure

Code scanning / CodeQL

Log entries created from user input High

This log entry depends on a
user-provided value
.

foreach (var libraryId in libraryIds)
{
try
{
await DeleteLibrary(libraryId, User.GetUserId());
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}

return Ok();
}

private async Task<bool> DeleteLibrary(int libraryId, int userId)
{
var series = await _unitOfWork.SeriesRepository.GetSeriesForLibraryIdAsync(libraryId);
var seriesIds = series.Select(x => x.Id).ToArray();
var chapterIds =
Expand All @@ -463,16 +510,19 @@ public async Task<ActionResult<bool>> DeleteLibrary(int libraryId)
if (TaskScheduler.HasScanTaskRunningForLibrary(libraryId))
{
_logger.LogInformation("User is attempting to delete a library while a scan is in progress");
return BadRequest(await _localizationService.Translate(User.GetUserId(), "delete-library-while-scan"));
throw new KavitaException(await _localizationService.Translate(userId, "delete-library-while-scan"));
}

var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId);
if (library == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "library-doesnt-exist"));
if (library == null)
{
throw new KavitaException(await _localizationService.Translate(userId, "library-doesnt-exist"));
}


// Due to a bad schema that I can't figure out how to fix, we need to erase all RelatedSeries before we delete the library
// Aka SeriesRelation has an invalid foreign key
foreach (var s in await _unitOfWork.SeriesRepository.GetSeriesForLibraryIdAsync(library.Id,
SeriesIncludes.Related))
foreach (var s in await _unitOfWork.SeriesRepository.GetSeriesForLibraryIdAsync(library.Id, SeriesIncludes.Related))
{
s.Relations = new List<SeriesRelation>();
_unitOfWork.SeriesRepository.Update(s);
Expand All @@ -489,7 +539,7 @@ public async Task<ActionResult<bool>> DeleteLibrary(int libraryId)

await _libraryCacheProvider.RemoveByPrefixAsync(CacheKey);
await _eventHub.SendMessageAsync(MessageFactory.SideNavUpdate,
MessageFactory.SideNavUpdateEvent(User.GetUserId()), false);
MessageFactory.SideNavUpdateEvent(userId), false);

if (chapterIds.Any())
{
Expand All @@ -508,13 +558,13 @@ await _eventHub.SendMessageAsync(MessageFactory.SeriesRemoved,

await _eventHub.SendMessageAsync(MessageFactory.LibraryModified,
MessageFactory.LibraryModifiedEvent(libraryId, "delete"), false);
return Ok(true);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "There was a critical issue. Please try again");
await _unitOfWork.RollbackAsync();
return Ok(false);
return false;
}
}

Expand Down
7 changes: 7 additions & 0 deletions UI/Web/src/app/_services/library.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,13 @@ export class LibraryService {
return this.httpClient.delete(this.baseUrl + 'library/delete?libraryId=' + libraryId, {});
}

deleteMultiple(libraryIds: Array<number>) {
if (libraryIds.length === 0) {
return of();
}
return this.httpClient.delete(this.baseUrl + 'library/delete-multiple?libraryIds=' + libraryIds.join(','), {});
}

update(model: {name: string, folders: string[], id: number}) {
return this.httpClient.post(this.baseUrl + 'library/update', model);
}
Expand Down
25 changes: 18 additions & 7 deletions UI/Web/src/app/admin/manage-library/manage-library.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,13 @@ import {LibraryTypePipe} from '../../_pipes/library-type.pipe';
import {RouterLink} from '@angular/router';
import {translate, TranslocoModule} from "@jsverse/transloco";
import {DefaultDatePipe} from "../../_pipes/default-date.pipe";
import {AsyncPipe, NgTemplateOutlet, TitleCasePipe} from "@angular/common";
import {DefaultValuePipe} from "../../_pipes/default-value.pipe";
import {AsyncPipe, NgTemplateOutlet} from "@angular/common";
import {LoadingComponent} from "../../shared/loading/loading.component";
import {TagBadgeComponent} from "../../shared/tag-badge/tag-badge.component";
import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe";
import {Breakpoint, UtilityService} from "../../shared/_services/utility.service";
import {Action, ActionFactoryService, ActionItem} from "../../_services/action-factory.service";
import {ActionService} from "../../_services/action.service";
import {CardActionablesComponent} from "../../_single-module/card-actionables/card-actionables.component";
import {BehaviorSubject, Observable} from "rxjs";
import {BehaviorSubject, catchError, Observable} from "rxjs";
import {Select2Module} from "ng-select2-component";
import {SelectionModel} from "../../typeahead/_models/selection-model";
import {
Expand All @@ -50,8 +47,7 @@ import {FormControl, FormGroup} from "@angular/forms";
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [RouterLink, NgbTooltip, LibraryTypePipe, TimeAgoPipe, SentenceCasePipe, TranslocoModule, DefaultDatePipe,
AsyncPipe, DefaultValuePipe, LoadingComponent, TagBadgeComponent, TitleCasePipe, UtcToLocalTimePipe,
CardActionablesComponent, Select2Module, NgTemplateOutlet]
AsyncPipe, LoadingComponent, CardActionablesComponent, Select2Module, NgTemplateOutlet]
})
export class ManageLibraryComponent implements OnInit {

Expand Down Expand Up @@ -243,6 +239,21 @@ export class ManageLibraryComponent implements OnInit {
this.resetBulkMode();
});
break;
case Action.Delete:
this.bulkMode = true;
this.cdRef.markForCheck();
const libIds = selected.map(l => l.id);
if (!await this.confirmService.confirm(translate('toasts.bulk-delete-libraries', {count: libIds.length}))) return;
this.libraryService.deleteMultiple(libIds)
.pipe(catchError((_, obs) => {
this.resetBulkMode();
return obs;
}))
.subscribe(() => {
this.getLibraries();
this.resetBulkMode();
})
break;
case Action.CopySettings:
// Remove the source library from the list
if (selected.length === 1 && selected[0].id === this.sourceCopyToLibrary!.id) {
Expand Down
3 changes: 2 additions & 1 deletion UI/Web/src/assets/langs/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2433,7 +2433,8 @@
"must-select-library": "At least one library must be selected",
"bulk-scan": "Scanning multiple libraries will be done linearly. This may take a long time and not complete depending on library size.",
"bulk-covers": "Refreshing covers on multiple libraries is intensive and can take a long time. Are you sure you want to continue?",
"person-image-downloaded": "Person cover was downloaded and applied."
"person-image-downloaded": "Person cover was downloaded and applied.",
"bulk-delete-libraries": "Are you sure you want to delete {{count}} libraries?"
},

"read-time-pipe": {
Expand Down

0 comments on commit 1fb632b

Please sign in to comment.