search-fix
All checks were successful
Build and Push Docker Image / build (push) Successful in 56s

This commit is contained in:
Garret Patti
2026-04-21 14:55:28 -04:00
parent da3ad97d51
commit 2cf8bc6d7d
2 changed files with 31 additions and 25 deletions

View File

@@ -30,7 +30,7 @@ export async function GET(request: NextRequest) {
const root = resolveLibraryRoot(library) const root = resolveLibraryRoot(library)
const recursive = request.nextUrl.searchParams.get('recursive') === 'true' const recursive = request.nextUrl.searchParams.get('recursive') === 'true'
const listing = recursive const listing = recursive
? scanDirectoryRecursive(root, libraryId, subpath) ? await scanDirectoryRecursive(root, libraryId, subpath)
: scanDirectory(root, libraryId, subpath) : scanDirectory(root, libraryId, subpath)
// Annotate image files with hasExtractedText, and directories if any descendant has extracted text // Annotate image files with hasExtractedText, and directories if any descendant has extracted text

View File

@@ -74,12 +74,16 @@ export function scanDirectory(
* Recursively walks every subdirectory under `subpath` and returns a flat list * Recursively walks every subdirectory under `subpath` and returns a flat list
* of all files. Directory entries are omitted. Each FileEntry.name is the full * of all files. Directory entries are omitted. Each FileEntry.name is the full
* relative path from the library root (e.g. FolderA/SubFolder/video.mp4). * relative path from the library root (e.g. FolderA/SubFolder/video.mp4).
*
* Uses async I/O so the Node.js event loop is not blocked during large
* directory trees (blocking stalls streaming responses and causes
* "ReadableStream is already closed" errors on concurrent requests).
*/ */
export function scanDirectoryRecursive( export async function scanDirectoryRecursive(
libraryRoot: string, libraryRoot: string,
libraryId: string, libraryId: string,
subpath: string subpath: string
): DirectoryListing { ): Promise<DirectoryListing> {
let rootAbsPath: string let rootAbsPath: string
try { try {
rootAbsPath = subpath ? resolveAndJail(libraryRoot, subpath) : libraryRoot rootAbsPath = subpath ? resolveAndJail(libraryRoot, subpath) : libraryRoot
@@ -89,35 +93,37 @@ export function scanDirectoryRecursive(
const entries: FileEntry[] = [] const entries: FileEntry[] = []
function walk(absDir: string, relDir: string): void { async function walk(absDir: string, relDir: string): Promise<void> {
let dirents: fs.Dirent[] let dirents: fs.Dirent[]
try { try {
dirents = fs.readdirSync(absDir, { withFileTypes: true }) dirents = await fs.promises.readdir(absDir, { withFileTypes: true })
} catch { } catch {
return return
} }
for (const d of dirents) { await Promise.all(
if (HIDDEN_FILES.test(d.name)) continue dirents.map(async (d) => {
const relPath = relDir ? path.join(relDir, d.name) : d.name if (HIDDEN_FILES.test(d.name)) return
if (d.isDirectory()) { const relPath = relDir ? path.join(relDir, d.name) : d.name
walk(path.join(absDir, d.name), relPath) if (d.isDirectory()) {
} else { await walk(path.join(absDir, d.name), relPath)
const mediaType = getMediaType(d.name) } else {
const hasThumbnail = mediaType === 'image' || mediaType === 'video' const mediaType = getMediaType(d.name)
// name = full relative path from library root so media keys match const hasThumbnail = mediaType === 'image' || mediaType === 'video'
const fullRelPath = subpath ? path.join(subpath, relPath) : relPath // name = full relative path from library root so media keys match
entries.push({ const fullRelPath = subpath ? path.join(subpath, relPath) : relPath
name: fullRelPath, entries.push({
type: 'file', name: fullRelPath,
mediaType, type: 'file',
url: fileApiUrl(libraryId, fullRelPath), mediaType,
thumbnailUrl: hasThumbnail ? thumbnailApiUrl(libraryId, fullRelPath) : null, url: fileApiUrl(libraryId, fullRelPath),
}) thumbnailUrl: hasThumbnail ? thumbnailApiUrl(libraryId, fullRelPath) : null,
} })
} }
})
)
} }
walk(rootAbsPath, '') await walk(rootAbsPath, '')
entries.sort((a, b) => a.name.localeCompare(b.name)) entries.sort((a, b) => a.name.localeCompare(b.name))
return { path: subpath, entries } return { path: subpath, entries }
} }