98 lines
3.0 KiB
TypeScript
98 lines
3.0 KiB
TypeScript
|
|
import express, { type Request, type Response } from 'express'
|
||
|
|
import fs from 'fs/promises'
|
||
|
|
import path from 'path'
|
||
|
|
import { asyncHandler } from '../../utils/asyncHandler.js'
|
||
|
|
import { successResponse } from '../../utils/response.js'
|
||
|
|
import { resolveNotebookPath } from '../../utils/pathSafety.js'
|
||
|
|
import type { FileItemDTO } from '../../../shared/types.js'
|
||
|
|
import { toPosixPath } from '../../../shared/utils/path.js'
|
||
|
|
|
||
|
|
const router = express.Router()
|
||
|
|
|
||
|
|
router.post(
|
||
|
|
'/',
|
||
|
|
asyncHandler(async (req: Request, res: Response) => {
|
||
|
|
const { keywords } = req.body as { keywords?: string[] }
|
||
|
|
if (!keywords || !Array.isArray(keywords) || keywords.length === 0) {
|
||
|
|
successResponse(res, { items: [] })
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
const searchTerms = keywords.map(k => k.trim().toLowerCase()).filter(k => k.length > 0)
|
||
|
|
|
||
|
|
if (searchTerms.length === 0) {
|
||
|
|
successResponse(res, { items: [] })
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
const { fullPath: rootPath } = resolveNotebookPath('')
|
||
|
|
const results: FileItemDTO[] = []
|
||
|
|
const maxResults = 100
|
||
|
|
|
||
|
|
const searchDir = async (dir: string, relativeDir: string) => {
|
||
|
|
if (results.length >= maxResults) return
|
||
|
|
|
||
|
|
const entries = await fs.readdir(dir, { withFileTypes: true })
|
||
|
|
|
||
|
|
for (const entry of entries) {
|
||
|
|
if (results.length >= maxResults) break
|
||
|
|
|
||
|
|
const entryPath = path.join(dir, entry.name)
|
||
|
|
const entryRelativePath = path.join(relativeDir, entry.name)
|
||
|
|
|
||
|
|
if (entry.name.startsWith('.') || entry.name === 'RB' || entry.name === 'node_modules') continue
|
||
|
|
|
||
|
|
if (entry.isDirectory()) {
|
||
|
|
await searchDir(entryPath, entryRelativePath)
|
||
|
|
} else if (entry.isFile()) {
|
||
|
|
const fileNameLower = entry.name.toLowerCase()
|
||
|
|
let contentLower = ''
|
||
|
|
let contentLoaded = false
|
||
|
|
|
||
|
|
const checkKeyword = async (term: string) => {
|
||
|
|
if (fileNameLower.includes(term)) return true
|
||
|
|
|
||
|
|
if (entry.name.toLowerCase().endsWith('.md')) {
|
||
|
|
if (!contentLoaded) {
|
||
|
|
try {
|
||
|
|
const content = await fs.readFile(entryPath, 'utf-8')
|
||
|
|
contentLower = content.toLowerCase()
|
||
|
|
contentLoaded = true
|
||
|
|
} catch {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return contentLower.includes(term)
|
||
|
|
}
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
let allMatched = true
|
||
|
|
for (const term of searchTerms) {
|
||
|
|
const matched = await checkKeyword(term)
|
||
|
|
if (!matched) {
|
||
|
|
allMatched = false
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (allMatched) {
|
||
|
|
results.push({
|
||
|
|
name: entry.name,
|
||
|
|
path: toPosixPath(entryRelativePath),
|
||
|
|
type: 'file',
|
||
|
|
size: 0,
|
||
|
|
modified: new Date().toISOString(),
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
await searchDir(rootPath, '')
|
||
|
|
successResponse(res, { items: results, limited: results.length >= maxResults })
|
||
|
|
}),
|
||
|
|
)
|
||
|
|
|
||
|
|
export default router
|