diff --git a/API/Services/MetadataService.cs b/API/Services/MetadataService.cs index f7ac6ef5e..09161f42a 100644 --- a/API/Services/MetadataService.cs +++ b/API/Services/MetadataService.cs @@ -280,13 +280,13 @@ namespace API.Services "[MetadataService] Processed {SeriesStart} - {SeriesEnd} out of {TotalSeries} series in {ElapsedScanTime} milliseconds for {LibraryName}", chunk * chunkInfo.ChunkSize, (chunk * chunkInfo.ChunkSize) + nonLibrarySeries.Count, chunkInfo.TotalSize, stopwatch.ElapsedMilliseconds, library.Name); } - var progress = Math.Max(0F, Math.Min(100F, i * 1F / chunkInfo.TotalChunks)); + var progress = Math.Max(0F, Math.Min(1F, i * 1F / chunkInfo.TotalChunks)); await _messageHub.Clients.All.SendAsync(SignalREvents.RefreshMetadataProgress, MessageFactory.RefreshMetadataProgressEvent(library.Id, progress)); } await _messageHub.Clients.All.SendAsync(SignalREvents.RefreshMetadataProgress, - MessageFactory.RefreshMetadataProgressEvent(library.Id, 100F)); + MessageFactory.RefreshMetadataProgressEvent(library.Id, 1F)); _logger.LogInformation("[MetadataService] Updated metadata for {SeriesNumber} series in library {LibraryName} in {ElapsedMilliseconds} milliseconds total", chunkInfo.TotalSize, library.Name, totalTime); } diff --git a/API/Services/Tasks/BackupService.cs b/API/Services/Tasks/BackupService.cs index 5f268be5d..04cb279ec 100644 --- a/API/Services/Tasks/BackupService.cs +++ b/API/Services/Tasks/BackupService.cs @@ -8,7 +8,9 @@ using API.Entities.Enums; using API.Extensions; using API.Interfaces; using API.Interfaces.Services; +using API.SignalR; using Hangfire; +using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; @@ -19,14 +21,17 @@ namespace API.Services.Tasks private readonly IUnitOfWork _unitOfWork; private readonly ILogger _logger; private readonly IDirectoryService _directoryService; + private readonly IHubContext _messageHub; private readonly IList _backupFiles; - public BackupService(IUnitOfWork unitOfWork, ILogger logger, IDirectoryService directoryService, IConfiguration config) + public BackupService(IUnitOfWork unitOfWork, ILogger logger, + IDirectoryService directoryService, IConfiguration config, IHubContext messageHub) { _unitOfWork = unitOfWork; _logger = logger; _directoryService = directoryService; + _messageHub = messageHub; var maxRollingFiles = config.GetMaxRollingFiles(); var loggingSection = config.GetLoggingFileName(); @@ -76,6 +81,8 @@ namespace API.Services.Tasks return; } + await SendProgress(0F); + var dateString = $"{DateTime.Now.ToShortDateString()}_{DateTime.Now.ToLongTimeString()}".Replace("/", "_").Replace(":", "_"); var zipPath = Path.Join(backupDirectory, $"kavita_backup_{dateString}.zip"); @@ -92,8 +99,12 @@ namespace API.Services.Tasks _directoryService.CopyFilesToDirectory( _backupFiles.Select(file => Path.Join(DirectoryService.ConfigDirectory, file)).ToList(), tempDirectory); + await SendProgress(0.25F); + await CopyCoverImagesToBackupDirectory(tempDirectory); + await SendProgress(0.75F); + try { ZipFile.CreateFromDirectory(tempDirectory, zipPath); @@ -105,6 +116,7 @@ namespace API.Services.Tasks DirectoryService.ClearAndDeleteDirectory(tempDirectory); _logger.LogInformation("Database backup completed"); + await SendProgress(1F); } private async Task CopyCoverImagesToBackupDirectory(string tempDirectory) @@ -137,6 +149,12 @@ namespace API.Services.Tasks } } + private async Task SendProgress(float progress) + { + await _messageHub.Clients.All.SendAsync(SignalREvents.BackupDatabaseProgress, + MessageFactory.BackupDatabaseProgressEvent(progress)); + } + /// /// Removes Database backups older than 30 days. If all backups are older than 30 days, the latest is kept. /// diff --git a/API/Services/Tasks/CleanupService.cs b/API/Services/Tasks/CleanupService.cs index a3c63c30f..1ecc9cec5 100644 --- a/API/Services/Tasks/CleanupService.cs +++ b/API/Services/Tasks/CleanupService.cs @@ -2,7 +2,9 @@ using System.Threading.Tasks; using API.Interfaces; using API.Interfaces.Services; +using API.SignalR; using Hangfire; +using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging; namespace API.Services.Tasks @@ -16,14 +18,16 @@ namespace API.Services.Tasks private readonly ILogger _logger; private readonly IBackupService _backupService; private readonly IUnitOfWork _unitOfWork; + private readonly IHubContext _messageHub; public CleanupService(ICacheService cacheService, ILogger logger, - IBackupService backupService, IUnitOfWork unitOfWork) + IBackupService backupService, IUnitOfWork unitOfWork, IHubContext messageHub) { _cacheService = cacheService; _logger = logger; _backupService = backupService; _unitOfWork = unitOfWork; + _messageHub = messageHub; } public void CleanupCacheDirectory() @@ -39,19 +43,31 @@ namespace API.Services.Tasks public async Task Cleanup() { _logger.LogInformation("Starting Cleanup"); + await SendProgress(0F); _logger.LogInformation("Cleaning temp directory"); - var tempDirectory = DirectoryService.TempDirectory; - DirectoryService.ClearDirectory(tempDirectory); + DirectoryService.ClearDirectory(DirectoryService.TempDirectory); + await SendProgress(0.1F); CleanupCacheDirectory(); + await SendProgress(0.25F); _logger.LogInformation("Cleaning old database backups"); _backupService.CleanupBackups(); + await SendProgress(0.50F); _logger.LogInformation("Cleaning deleted cover images"); await DeleteSeriesCoverImages(); + await SendProgress(0.6F); await DeleteChapterCoverImages(); + await SendProgress(0.7F); await DeleteTagCoverImages(); + await SendProgress(1F); _logger.LogInformation("Cleanup finished"); } + private async Task SendProgress(float progress) + { + await _messageHub.Clients.All.SendAsync(SignalREvents.CleanupProgress, + MessageFactory.CleanupProgressEvent(progress)); + } + private async Task DeleteSeriesCoverImages() { var images = await _unitOfWork.SeriesRepository.GetAllCoverImagesAsync(); diff --git a/API/Services/Tasks/ScannerService.cs b/API/Services/Tasks/ScannerService.cs index 250045a47..7ba02d041 100644 --- a/API/Services/Tasks/ScannerService.cs +++ b/API/Services/Tasks/ScannerService.cs @@ -244,7 +244,7 @@ namespace API.Services.Tasks BackgroundJob.Enqueue(() => _metadataService.RefreshMetadata(libraryId, false)); await _messageHub.Clients.All.SendAsync(SignalREvents.ScanLibraryProgress, - MessageFactory.ScanLibraryProgressEvent(libraryId, 100)); + MessageFactory.ScanLibraryProgressEvent(libraryId, 1F)); } /// @@ -342,7 +342,7 @@ namespace API.Services.Tasks await _messageHub.Clients.All.SendAsync(SignalREvents.SeriesRemoved, MessageFactory.SeriesRemovedEvent(missing.Id, missing.Name, library.Id)); } - var progress = Math.Max(0, Math.Min(100, ((chunk + 1F) * chunkInfo.ChunkSize) / chunkInfo.TotalSize)); + var progress = Math.Max(0, Math.Min(1, ((chunk + 1F) * chunkInfo.ChunkSize) / chunkInfo.TotalSize)); await _messageHub.Clients.All.SendAsync(SignalREvents.ScanLibraryProgress, MessageFactory.ScanLibraryProgressEvent(library.Id, progress)); } @@ -405,7 +405,7 @@ namespace API.Services.Tasks series.Name, $"{series.Name}_{series.NormalizedName}_{series.LocalizedName}_{series.LibraryId}_{series.Format}"); } - var progress = Math.Max(0F, Math.Min(100F, i * 1F / newSeries.Count)); + var progress = Math.Max(0F, Math.Min(1F, i * 1F / newSeries.Count)); await _messageHub.Clients.All.SendAsync(SignalREvents.ScanLibraryProgress, MessageFactory.ScanLibraryProgressEvent(library.Id, progress)); i++; diff --git a/API/SignalR/MessageFactory.cs b/API/SignalR/MessageFactory.cs index c0f069f18..3ab6c646c 100644 --- a/API/SignalR/MessageFactory.cs +++ b/API/SignalR/MessageFactory.cs @@ -89,6 +89,31 @@ namespace API.SignalR }; } + public static SignalRMessage BackupDatabaseProgressEvent(float progress) + { + return new SignalRMessage() + { + Name = SignalREvents.BackupDatabaseProgress, + Body = new + { + Progress = progress + } + }; + } + public static SignalRMessage CleanupProgressEvent(float progress) + { + return new SignalRMessage() + { + Name = SignalREvents.CleanupProgress, + Body = new + { + Progress = progress + } + }; + } + + + public static SignalRMessage UpdateVersionEvent(UpdateNotificationDto update) { return new SignalRMessage diff --git a/API/SignalR/SignalREvents.cs b/API/SignalR/SignalREvents.cs index 09a6e8f24..06908c2be 100644 --- a/API/SignalR/SignalREvents.cs +++ b/API/SignalR/SignalREvents.cs @@ -19,5 +19,13 @@ public const string OnlineUsers = "OnlineUsers"; public const string SeriesAddedToCollection = "SeriesAddedToCollection"; public const string ScanLibraryError = "ScanLibraryError"; + /// + /// Event sent out during backing up the database + /// + public const string BackupDatabaseProgress = "BackupDatabaseProgress"; + /// + /// Event sent out during cleaning up temp and cache folders + /// + public const string CleanupProgress = "CleanupProgress"; } } diff --git a/UI/Web/src/app/_models/events/scan-library-progress-event.ts b/UI/Web/src/app/_models/events/scan-library-progress-event.ts index e8460fde4..7b4d9c2d0 100644 --- a/UI/Web/src/app/_models/events/scan-library-progress-event.ts +++ b/UI/Web/src/app/_models/events/scan-library-progress-event.ts @@ -1,4 +1,4 @@ -export interface ScanLibraryProgressEvent { +export interface ProgressEvent { libraryId: number; progress: number; eventTime: string; diff --git a/UI/Web/src/app/_services/message-hub.service.ts b/UI/Web/src/app/_services/message-hub.service.ts index 1a1c4d8f7..85a543322 100644 --- a/UI/Web/src/app/_services/message-hub.service.ts +++ b/UI/Web/src/app/_services/message-hub.service.ts @@ -7,7 +7,7 @@ import { BehaviorSubject, ReplaySubject } from 'rxjs'; import { environment } from 'src/environments/environment'; import { UpdateNotificationModalComponent } from '../shared/update-notification/update-notification-modal.component'; import { RefreshMetadataEvent } from '../_models/events/refresh-metadata-event'; -import { ScanLibraryProgressEvent } from '../_models/events/scan-library-progress-event'; +import { ProgressEvent } from '../_models/events/scan-library-progress-event'; import { ScanSeriesEvent } from '../_models/events/scan-series-event'; import { SeriesAddedEvent } from '../_models/events/series-added-event'; import { User } from '../_models/user'; @@ -22,7 +22,9 @@ export enum EVENTS { ScanLibraryProgress = 'ScanLibraryProgress', OnlineUsers = 'OnlineUsers', SeriesAddedToCollection = 'SeriesAddedToCollection', - ScanLibraryError = 'ScanLibraryError' + ScanLibraryError = 'ScanLibraryError', + BackupDatabaseProgress = 'BackupDatabaseProgress', + CleanupProgress = 'CleanupProgress' } export interface Message { @@ -45,7 +47,7 @@ export class MessageHubService { onlineUsers$ = this.onlineUsersSource.asObservable(); public scanSeries: EventEmitter = new EventEmitter(); - public scanLibrary: EventEmitter = new EventEmitter(); + public scanLibrary: EventEmitter = new EventEmitter(); // TODO: Refactor this name to be generic public seriesAdded: EventEmitter = new EventEmitter(); public refreshMetadata: EventEmitter = new EventEmitter(); @@ -90,6 +92,20 @@ export class MessageHubService { this.scanLibrary.emit(resp.body); }); + this.hubConnection.on(EVENTS.BackupDatabaseProgress, resp => { + this.messagesSource.next({ + event: EVENTS.BackupDatabaseProgress, + payload: resp.body + }); + }); + + this.hubConnection.on(EVENTS.CleanupProgress, resp => { + this.messagesSource.next({ + event: EVENTS.CleanupProgress, + payload: resp.body + }); + }); + this.hubConnection.on(EVENTS.RefreshMetadataProgress, resp => { this.messagesSource.next({ event: EVENTS.RefreshMetadataProgress, diff --git a/UI/Web/src/app/admin/manage-library/manage-library.component.ts b/UI/Web/src/app/admin/manage-library/manage-library.component.ts index 3caf6c7f4..d7a694ac1 100644 --- a/UI/Web/src/app/admin/manage-library/manage-library.component.ts +++ b/UI/Web/src/app/admin/manage-library/manage-library.component.ts @@ -4,7 +4,7 @@ import { ToastrService } from 'ngx-toastr'; import { Subject } from 'rxjs'; import { take, takeUntil } from 'rxjs/operators'; import { ConfirmService } from 'src/app/shared/confirm.service'; -import { ScanLibraryProgressEvent } from 'src/app/_models/events/scan-library-progress-event'; +import { ProgressEvent } from 'src/app/_models/events/scan-library-progress-event'; import { Library, LibraryType } from 'src/app/_models/library'; import { LibraryService } from 'src/app/_services/library.service'; import { EVENTS, MessageHubService } from 'src/app/_services/message-hub.service'; @@ -40,7 +40,7 @@ export class ManageLibraryComponent implements OnInit, OnDestroy { this.hubService.messages$.pipe(takeUntil(this.onDestroy)).subscribe((event) => { if (event.event !== EVENTS.ScanLibraryProgress) return; - const scanEvent = event.payload as ScanLibraryProgressEvent; + const scanEvent = event.payload as ProgressEvent; this.scanInProgress[scanEvent.libraryId] = {progress: scanEvent.progress !== 100}; if (scanEvent.progress === 0) { this.scanInProgress[scanEvent.libraryId].timestamp = scanEvent.eventTime; diff --git a/UI/Web/src/app/app.module.ts b/UI/Web/src/app/app.module.ts index 2dacb097b..a9cb82151 100644 --- a/UI/Web/src/app/app.module.ts +++ b/UI/Web/src/app/app.module.ts @@ -7,7 +7,7 @@ import { AppComponent } from './app.component'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; -import { NgbCollapseModule, NgbDropdownModule, NgbNavModule, NgbPaginationModule, NgbRatingModule } from '@ng-bootstrap/ng-bootstrap'; +import { NgbCollapseModule, NgbDropdownModule, NgbNavModule, NgbPaginationModule, NgbPopoverModule, NgbRatingModule } from '@ng-bootstrap/ng-bootstrap'; import { NavHeaderComponent } from './nav-header/nav-header.component'; import { JwtInterceptor } from './_interceptors/jwt.interceptor'; import { UserLoginComponent } from './user-login/user-login.component'; @@ -32,6 +32,7 @@ import { CollectionsModule } from './collections/collections.module'; import { ReadingListModule } from './reading-list/reading-list.module'; import { SAVER, getSaver } from './shared/_providers/saver.provider'; import { ConfigData } from './_models/config-data'; +import { NavEventsToggleComponent } from './nav-events-toggle/nav-events-toggle.component'; @NgModule({ @@ -48,6 +49,7 @@ import { ConfigData } from './_models/config-data'; RecentlyAddedComponent, OnDeckComponent, DashboardComponent, + NavEventsToggleComponent, ], imports: [ HttpClientModule, @@ -59,6 +61,7 @@ import { ConfigData } from './_models/config-data'; NgbDropdownModule, // Nav AutocompleteLibModule, // Nav + NgbPopoverModule, // Nav Events toggle NgbRatingModule, // Series Detail NgbNavModule, NgbPaginationModule, diff --git a/UI/Web/src/app/nav-events-toggle/nav-events-toggle.component.html b/UI/Web/src/app/nav-events-toggle/nav-events-toggle.component.html new file mode 100644 index 000000000..36da7d079 --- /dev/null +++ b/UI/Web/src/app/nav-events-toggle/nav-events-toggle.component.html @@ -0,0 +1,22 @@ + + + + + +
    +
  • +
    + Scan for {{event.libraryName}} in progress +
    + {{prettyPrintProgress(event.progress)}}% + {{prettyPrintEvent(event.eventType)}} {{event.libraryName}} +
  • +
  • Not much going on here
  • +
+
+
\ No newline at end of file diff --git a/UI/Web/src/app/nav-events-toggle/nav-events-toggle.component.scss b/UI/Web/src/app/nav-events-toggle/nav-events-toggle.component.scss new file mode 100644 index 000000000..49fa7ed63 --- /dev/null +++ b/UI/Web/src/app/nav-events-toggle/nav-events-toggle.component.scss @@ -0,0 +1,23 @@ +@import "../../theme/colors"; + +.small-spinner { + width: 1rem; + height: 1rem; +} + +.nav-events { + background-color: white; +} + +.nav-events .popover-body { + padding: 0px; +} + +.btn-icon { + color: white; +} + +.colored { + background-color: $primary-color; + border-radius: 60px; +} \ No newline at end of file diff --git a/UI/Web/src/app/nav-events-toggle/nav-events-toggle.component.ts b/UI/Web/src/app/nav-events-toggle/nav-events-toggle.component.ts new file mode 100644 index 000000000..e1c9b0064 --- /dev/null +++ b/UI/Web/src/app/nav-events-toggle/nav-events-toggle.component.ts @@ -0,0 +1,89 @@ +import { Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { BehaviorSubject, Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { ProgressEvent } from '../_models/events/scan-library-progress-event'; +import { User } from '../_models/user'; +import { LibraryService } from '../_services/library.service'; +import { EVENTS, Message, MessageHubService } from '../_services/message-hub.service'; + +interface ProcessedEvent { + eventType: string; + timestamp?: string; + progress: number; + libraryId: number; + libraryName: string; +} + +type ProgressType = EVENTS.ScanLibraryProgress | EVENTS.RefreshMetadataProgress | EVENTS.BackupDatabaseProgress | EVENTS.CleanupProgress; + +@Component({ + selector: 'app-nav-events-toggle', + templateUrl: './nav-events-toggle.component.html', + styleUrls: ['./nav-events-toggle.component.scss'] +}) +export class NavEventsToggleComponent implements OnInit, OnDestroy { + + @Input() user!: User; + + private readonly onDestroy = new Subject(); + + /** + * Events that come through and are merged (ie progress event gets merged into a progress event) + */ + progressEventsSource = new BehaviorSubject([]); + progressEvents$ = this.progressEventsSource.asObservable(); + + constructor(private messageHub: MessageHubService, private libraryService: LibraryService) { } + + ngOnDestroy(): void { + this.onDestroy.next(); + this.onDestroy.complete(); + } + + ngOnInit(): void { + this.messageHub.messages$.pipe(takeUntil(this.onDestroy)).subscribe(event => { + if (event.event === EVENTS.ScanLibraryProgress || event.event === EVENTS.RefreshMetadataProgress || event.event === EVENTS.BackupDatabaseProgress || event.event === EVENTS.CleanupProgress) { + this.processProgressEvent(event, event.event); + } + }); + } + + + processProgressEvent(event: Message, eventType: string) { + const scanEvent = event.payload as ProgressEvent; + console.log(event.event, event.payload); + + + this.libraryService.getLibraryNames().subscribe(names => { + const data = this.progressEventsSource.getValue(); + const index = data.findIndex(item => item.eventType === eventType && item.libraryId === event.payload.libraryId); + if (index >= 0) { + data.splice(index, 1); + } + + if (scanEvent.progress !== 1) { + const libraryName = names[scanEvent.libraryId] || ''; + const newEvent = {eventType: eventType, timestamp: scanEvent.eventTime, progress: scanEvent.progress, libraryId: scanEvent.libraryId, libraryName}; + data.push(newEvent); + } + + + this.progressEventsSource.next(data); + }); + } + + prettyPrintProgress(progress: number) { + return Math.trunc(progress * 100); + } + + prettyPrintEvent(eventType: string) { + switch(eventType) { + case (EVENTS.ScanLibraryProgress): return 'Scanning '; + case (EVENTS.RefreshMetadataProgress): return 'Refreshing '; + case (EVENTS.CleanupProgress): return 'Clearing Cache'; + case (EVENTS.BackupDatabaseProgress): return 'Backing up Database'; + default: return eventType; + } + } + +} diff --git a/UI/Web/src/app/nav-header/nav-header.component.html b/UI/Web/src/app/nav-header/nav-header.component.html index b96bd73f6..d2e99de03 100644 --- a/UI/Web/src/app/nav-header/nav-header.component.html +++ b/UI/Web/src/app/nav-header/nav-header.component.html @@ -62,6 +62,10 @@ + +