mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
fix(library): ignore current library during folder prescan
Prevent an edited library from treating its own persisted folders as conflicts while preserving overlap warnings for other libraries. Fixes #1340
This commit is contained in:
@@ -69,6 +69,24 @@ describe('useLibraryCreator', () => {
|
||||
expect(creator.prescanResult.value).toEqual(result)
|
||||
})
|
||||
|
||||
it('includes the current library ID when prescanning an edit', async () => {
|
||||
const { useLibraryCreator } = await import('../useLibraryCreator')
|
||||
const creator = useLibraryCreator()
|
||||
const result: PrescanResult = { paths: [{ path: '/books', accessible: true, fileCount: 2 }], totalFiles: 2 }
|
||||
apiMock.mockResolvedValue(jsonResponse(result))
|
||||
creator.initEdit(makeLibrary({ id: 12 }))
|
||||
|
||||
await creator.runPrescan()
|
||||
|
||||
expect(apiMock).toHaveBeenCalledWith(
|
||||
'/api/v1/libraries/prescan',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ paths: ['/books'], libraryId: 12 }),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('surfaces prescan connection failures', async () => {
|
||||
const { useLibraryCreator } = await import('../useLibraryCreator')
|
||||
const creator = useLibraryCreator()
|
||||
|
||||
@@ -141,10 +141,14 @@ export function useLibraryCreator() {
|
||||
prescanResult.value = null
|
||||
error.value = null
|
||||
try {
|
||||
const payload = {
|
||||
paths: form.folders,
|
||||
...(editingLibraryId.value === null ? {} : { libraryId: editingLibraryId.value }),
|
||||
}
|
||||
const res = await api('/api/v1/libraries/prescan', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ paths: form.folders }),
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
if (res.ok) {
|
||||
prescanResult.value = await res.json()
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { ArrayMinSize, IsArray, IsNotEmpty, IsString, MaxLength } from 'class-validator';
|
||||
import { ArrayMinSize, IsArray, IsInt, IsNotEmpty, IsOptional, IsString, MaxLength, Min } from 'class-validator';
|
||||
|
||||
export class PrescanLibraryDto {
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
libraryId?: number;
|
||||
|
||||
@IsArray()
|
||||
@ArrayMinSize(1)
|
||||
@IsString({ each: true })
|
||||
|
||||
@@ -61,9 +61,13 @@ describe('Library DTO validation', () => {
|
||||
expect(await hasErrors(plainToInstance(UpdateLibraryAccessDto, { accessLevel: 'owner' }))).toBe(false);
|
||||
});
|
||||
|
||||
it('PrescanLibraryDto requires at least one non-empty path', async () => {
|
||||
it('PrescanLibraryDto requires at least one non-empty path and validates an optional library ID', async () => {
|
||||
expect(await hasErrors(plainToInstance(PrescanLibraryDto, { paths: [''] }))).toBe(true);
|
||||
expect(await hasErrors(plainToInstance(PrescanLibraryDto, { paths: ['/books'] }))).toBe(false);
|
||||
expect(await hasErrors(plainToInstance(PrescanLibraryDto, { paths: ['/books'], libraryId: 12 }))).toBe(false);
|
||||
expect(await hasErrors(plainToInstance(PrescanLibraryDto, { paths: ['/books'], libraryId: 0 }))).toBe(true);
|
||||
expect(await hasErrors(plainToInstance(PrescanLibraryDto, { paths: ['/books'], libraryId: 1.5 }))).toBe(true);
|
||||
expect(await hasErrors(plainToInstance(PrescanLibraryDto, { paths: ['/books'], libraryId: '12' }))).toBe(true);
|
||||
});
|
||||
|
||||
it('ReorderLibrariesDto validates nested order items', async () => {
|
||||
|
||||
@@ -462,7 +462,7 @@ describe('LibraryService', () => {
|
||||
});
|
||||
|
||||
it('prescan counts primary files recursively and flags overlapping paths', async () => {
|
||||
libraryRepo.findAllFolderPaths.mockResolvedValue([{ path: '/books/existing', libraryName: 'Existing Library' }]);
|
||||
libraryRepo.findAllFolderPaths.mockResolvedValue([{ libraryId: 1, path: '/books/existing', libraryName: 'Existing Library' }]);
|
||||
|
||||
mockReaddir.mockImplementation((path: Parameters<typeof readdir>[0]) => {
|
||||
if (path === '/books/new') {
|
||||
@@ -483,6 +483,34 @@ describe('LibraryService', () => {
|
||||
expect(result.paths[1]).toEqual(expect.objectContaining({ overlapLibrary: 'Existing Library' }));
|
||||
});
|
||||
|
||||
it('prescan ignores persisted folders from the library being edited', async () => {
|
||||
libraryRepo.findAllFolderPaths.mockResolvedValue([
|
||||
{ libraryId: 10, path: '/books/audiobooks', libraryName: 'Audiobooks' },
|
||||
{ libraryId: 20, path: '/books/ebooks', libraryName: 'eBooks' },
|
||||
]);
|
||||
|
||||
const result = await service.prescan({ paths: ['/books/audiobooks'], libraryId: 10 });
|
||||
|
||||
expect(result.paths[0]).toEqual({
|
||||
path: '/books/audiobooks',
|
||||
accessible: true,
|
||||
fileCount: 0,
|
||||
overlapLibrary: undefined,
|
||||
error: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('prescan still reports a different overlapping library after ignoring the edited library', async () => {
|
||||
libraryRepo.findAllFolderPaths.mockResolvedValue([
|
||||
{ libraryId: 10, path: '/books/audiobooks', libraryName: 'Audiobooks' },
|
||||
{ libraryId: 20, path: '/books', libraryName: 'All Books' },
|
||||
]);
|
||||
|
||||
const result = await service.prescan({ paths: ['/books/audiobooks'], libraryId: 10 });
|
||||
|
||||
expect(result.paths[0]).toEqual(expect.objectContaining({ overlapLibrary: 'All Books' }));
|
||||
});
|
||||
|
||||
it('prescan reports paths outside the configured browse root without touching the filesystem', async () => {
|
||||
libraryRepo.findAllFolderPaths.mockResolvedValue([]);
|
||||
pathPolicy.resolveBrowsePath.mockRejectedValue(new ForbiddenException('outside root'));
|
||||
|
||||
@@ -292,6 +292,7 @@ export class LibraryService {
|
||||
}
|
||||
|
||||
for (const existing of allFolderPaths) {
|
||||
if (dto.libraryId !== undefined && existing.libraryId === dto.libraryId) continue;
|
||||
if (pathsOverlap(resolvedInputPath, existing.path)) {
|
||||
overlapLibrary = existing.libraryName;
|
||||
break;
|
||||
|
||||
@@ -308,6 +308,25 @@ describe('Library admin workflows (e2e)', { timeout: SCENARIO_TIMEOUT_MS }, () =
|
||||
},
|
||||
]);
|
||||
|
||||
const editPrescanResponse = await ctx.app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/v1/libraries/prescan',
|
||||
headers: authHeader(manager.accessToken),
|
||||
payload: { paths: [overlapLibrary.folderPath], libraryId: overlapLibrary.libraryId },
|
||||
});
|
||||
|
||||
expect(editPrescanResponse.statusCode).toBe(201);
|
||||
expect(editPrescanResponse.json()).toEqual({
|
||||
totalFiles: 0,
|
||||
paths: [
|
||||
{
|
||||
path: overlapLibrary.folderPath,
|
||||
accessible: true,
|
||||
fileCount: 0,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const forbiddenPrescanResponse = await ctx.app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/v1/libraries/prescan',
|
||||
|
||||
Reference in New Issue
Block a user