Allow manual bookdrop folder refresh for non-local file systems

This commit is contained in:
aditya.chandel
2025-08-29 10:30:27 -06:00
committed by Aditya Chandel
parent a8102a3a75
commit addf7c0c17
9 changed files with 98 additions and 21 deletions

View File

@@ -6,6 +6,8 @@ import com.adityachandel.booklore.model.dto.request.BookdropFinalizeRequest;
import com.adityachandel.booklore.model.dto.request.BookdropSelectionRequest;
import com.adityachandel.booklore.model.dto.response.BookdropFinalizeResult;
import com.adityachandel.booklore.service.bookdrop.BookDropService;
import com.adityachandel.booklore.service.bookdrop.BookdropMonitoringService;
import com.adityachandel.booklore.service.monitoring.MonitoringService;
import lombok.AllArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
@@ -18,6 +20,7 @@ import org.springframework.web.bind.annotation.*;
public class BookdropFileController {
private final BookDropService bookDropService;
private final BookdropMonitoringService monitoringService;
@GetMapping("/notification")
public BookdropFileNotification getSummary() {
@@ -40,4 +43,10 @@ public class BookdropFileController {
BookdropFinalizeResult result = bookDropService.finalizeImport(request);
return ResponseEntity.ok(result);
}
@PostMapping("/rescan")
public ResponseEntity<Void> rescanBookdrop() {
monitoringService.rescanBookdropFolder();
return ResponseEntity.ok().build();
}
}

View File

@@ -179,6 +179,11 @@ public class BookdropMonitoringService {
}
}
public void rescanBookdropFolder() {
log.info("Manual rescan of Bookdrop folder triggered.");
scanExistingBookdropFiles();
}
private void scanExistingBookdropFiles() {
try (Stream<Path> files = Files.walk(bookdrop)) {
files.filter(Files::isRegularFile)

View File

@@ -1,12 +1,35 @@
<div class="flex flex-col h-[calc(100dvh-6.1rem)] rounded-xl bg-[var(--card-background)] space-y-4">
<div class="px-6 pt-6 pb-4">
<h2 class="text-xl font-semibold pb-1">Review Bookdrop Files</h2>
<p class="text-sm text-gray-400">
These files were uploaded to the
<strong class="text-[var(--primary-color)]">Bookdrop Folder</strong>.
Review their fetched metadata, assign a library and subpath, and finalize where they belong in your collection.
</p>
<div class="px-6 pt-6 pb-4 flex flex-col md:flex-row md:items-center md:justify-between">
<div>
<h2 class="text-xl font-semibold pb-1 flex items-center gap-2">
Review Bookdrop Files
<a href="https://booklore-app.github.io/booklore-docs/docs/bookdrop" target="_blank" rel="noopener noreferrer">
<i
class="pi pi-external-link text-sky-600 cursor-pointer"
style="font-size: 0.9rem"
pTooltip="View documentation"
tooltipPosition="top"></i>
</a>
</h2>
<p class="text-sm text-gray-400">
These files were uploaded to the
<strong class="text-[var(--primary-color)]">Bookdrop Folder</strong>.
Review their fetched metadata, assign a library and subpath, and finalize where they belong in your collection.
</p>
</div>
<div class="mt-4 md:mt-0">
<p-button
label="Rescan Bookdrop"
icon="pi pi-refresh"
severity="primary"
outlined
(click)="rescanBookdrop()"
pTooltip="Manually trigger a rescan of the Bookdrop folder"
tooltipPosition="top">
</p-button>
</div>
</div>
@if (loading) {

View File

@@ -1,8 +1,8 @@
import {Component, DestroyRef, inject, OnInit, QueryList, ViewChildren} from '@angular/core';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {filter, take} from 'rxjs/operators';
import {filter, startWith, take, tap} from 'rxjs/operators';
import {BookdropFile, BookdropFileTaskService, BookdropFinalizePayload, BookdropFinalizeResult} from '../bookdrop-file-task.service';
import {BookdropFile, BookdropService, BookdropFinalizePayload, BookdropFinalizeResult} from '../bookdrop.service';
import {LibraryService} from '../../book/service/library.service';
import {Library} from '../../book/model/library.model';
@@ -15,7 +15,7 @@ import {Divider} from 'primeng/divider';
import {ConfirmationService, MessageService} from 'primeng/api';
import {BookdropFileMetadataPickerComponent} from '../bookdrop-file-metadata-picker-component/bookdrop-file-metadata-picker.component';
import {Observable} from 'rxjs';
import {Observable, Subscription} from 'rxjs';
import {AppSettings} from '../../core/model/app-settings.model';
import {AppSettingsService} from '../../core/service/app-settings.service';
@@ -26,6 +26,7 @@ import {UrlHelperService} from '../../utilities/service/url-helper.service';
import {Checkbox} from 'primeng/checkbox';
import {NgClass, NgStyle} from '@angular/common';
import {Paginator} from 'primeng/paginator';
import {ActivatedRoute, NavigationEnd, Router} from '@angular/router';
export interface BookdropFileUI {
file: BookdropFile;
@@ -59,7 +60,7 @@ export interface BookdropFileUI {
],
})
export class BookdropFileReviewComponent implements OnInit {
private readonly bookdropFileService = inject(BookdropFileTaskService);
private readonly bookdropService = inject(BookdropService);
private readonly libraryService = inject(LibraryService);
private readonly confirmationService = inject(ConfirmationService);
private readonly destroyRef = inject(DestroyRef);
@@ -67,10 +68,12 @@ export class BookdropFileReviewComponent implements OnInit {
private readonly appSettingsService = inject(AppSettingsService);
private readonly messageService = inject(MessageService);
private readonly urlHelper = inject(UrlHelperService);
private readonly activatedRoute = inject(ActivatedRoute);
@ViewChildren('metadataPicker') metadataPickers!: QueryList<BookdropFileMetadataPickerComponent>;
appSettings$: Observable<AppSettings | null> = this.appSettingsService.appSettings$;
private routerSub!: Subscription;
uploadPattern = '';
_defaultLibraryId: string | null = null;
@@ -91,8 +94,12 @@ export class BookdropFileReviewComponent implements OnInit {
excludedFiles = new Set<number>();
ngOnInit(): void {
this.loading = true;
this.loadPage(0);
this.activatedRoute.queryParams
.pipe(startWith({}), tap(() => {
this.loading = true;
this.loadPage(0);
}))
.subscribe();
this.libraryService.libraryState$
.pipe(filter(state => !!state?.loaded), take(1))
@@ -162,7 +169,7 @@ export class BookdropFileReviewComponent implements OnInit {
}
loadPage(page: number): void {
this.bookdropFileService.getPendingFiles(page, this.pageSize)
this.bookdropService.getPendingFiles(page, this.pageSize)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
next: response => {
@@ -354,7 +361,7 @@ export class BookdropFileReviewComponent implements OnInit {
.map(file => file.file.id);
}
this.bookdropFileService.discardFiles(payload).subscribe({
this.bookdropService.discardFiles(payload).subscribe({
next: () => {
this.messageService.add({
severity: 'success',
@@ -443,7 +450,7 @@ export class BookdropFileReviewComponent implements OnInit {
files,
};
this.bookdropFileService.finalizeImport(payload).subscribe({
this.bookdropService.finalizeImport(payload).subscribe({
next: (result: BookdropFinalizeResult) => {
this.saving = false;
@@ -528,4 +535,24 @@ export class BookdropFileReviewComponent implements OnInit {
savedFields: {}
};
}
rescanBookdrop() {
this.bookdropService.rescan().subscribe({
next: () => {
this.messageService.add({
severity: 'success',
summary: 'Rescan Triggered',
detail: 'Bookdrop rescan has been started successfully.',
});
},
error: (err) => {
this.messageService.add({
severity: 'error',
summary: 'Rescan Failed',
detail: 'Unable to trigger bookdrop rescan. Please try again.',
});
console.error(err);
}
});
}
}

View File

@@ -36,7 +36,7 @@ export class BookdropFilesWidgetComponent implements OnInit, OnDestroy {
}
openReviewDialog(): void {
this.router.navigate(['/bookdrop']);
this.router.navigate(['/bookdrop'], {queryParams: {reload: Date.now()}});
}
ngOnDestroy(): void {

View File

@@ -1,6 +1,6 @@
import {Component, OnDestroy} from '@angular/core';
import {DatePipe, NgClass} from '@angular/common';
import {BookdropFinalizeResult} from '../bookdrop-file-task.service';
import {BookdropFinalizeResult} from '../bookdrop.service';
import {DynamicDialogConfig, DynamicDialogRef} from "primeng/dynamicdialog";
@Component({

View File

@@ -57,7 +57,7 @@ export interface Page<T> {
}
@Injectable({providedIn: 'root'})
export class BookdropFileTaskService {
export class BookdropService {
private readonly url = `${API_CONFIG.BASE_URL}/api/v1/bookdrop`;
private http = inject(HttpClient);
@@ -72,4 +72,8 @@ export class BookdropFileTaskService {
discardFiles(payload: { selectAll: boolean; excludedIds?: number[]; selectedIds?: number[] }): Observable<void> {
return this.http.post<void>(`${this.url}/files/discard`, payload);
}
rescan(): Observable<void> {
return this.http.post<void>(`${this.url}/rescan`, {});
}
}

View File

@@ -30,6 +30,13 @@
<ul class="topbar-items hidden md:flex items-center gap-3 ml-auto pl-4">
<div class="flex gap-6">
@if (userService.userState$ | async; as userState) {
<li>
@if (userState.user?.permissions?.canManipulateLibrary || userState.user?.permissions?.admin) {
<a class="topbar-item" (click)="navigateToBookdrop()" pTooltip="Bookdrop" tooltipPosition="bottom">
<i class="pi pi-inbox text-surface-100"></i>
</a>
}
</li>
<li>
@if (userState.user?.permissions?.canManipulateLibrary || userState.user?.permissions?.admin) {
<a class="topbar-item" (click)="openLibraryCreatorDialog()" pTooltip="Create New Library" tooltipPosition="bottom">
@@ -37,8 +44,6 @@
</a>
}
</li>
}
@if (userService.userState$ | async; as userState) {
<li>
@if (userState.user?.permissions?.canManipulateLibrary || userState.user?.permissions?.admin) {
<a class="topbar-item" (click)="openFileUploadDialog()" pTooltip="Upload Book" tooltipPosition="bottom">

View File

@@ -136,6 +136,10 @@ export class AppTopBarComponent implements OnDestroy {
this.router.navigate(['/settings']);
}
navigateToBookdrop() {
this.router.navigate(['/bookdrop'], {queryParams: {reload: Date.now()}});
}
logout() {
this.authService.logout();
}