mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
feat: Add directory tree tab to session files
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { mkdir, rm, symlink, writeFile } from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
import { tmpdir } from 'os'
|
||||
import { RpcHandlerManager } from '../../../api/rpc/RpcHandlerManager'
|
||||
import { registerDirectoryHandlers } from './directories'
|
||||
|
||||
async function createTempDir(prefix: string): Promise<string> {
|
||||
const base = tmpdir()
|
||||
const path = join(base, `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}`)
|
||||
await mkdir(path, { recursive: true })
|
||||
return path
|
||||
}
|
||||
|
||||
describe('directory RPC handlers', () => {
|
||||
let rootDir: string
|
||||
let rpc: RpcHandlerManager
|
||||
|
||||
beforeEach(async () => {
|
||||
if (rootDir) {
|
||||
await rm(rootDir, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
rootDir = await createTempDir('hapi-dir-handler')
|
||||
await mkdir(join(rootDir, 'src'), { recursive: true })
|
||||
await writeFile(join(rootDir, 'src', 'index.ts'), 'console.log("ok")')
|
||||
await writeFile(join(rootDir, 'README.md'), '# test')
|
||||
|
||||
rpc = new RpcHandlerManager({ scopePrefix: 'session-test' })
|
||||
registerDirectoryHandlers(rpc, rootDir)
|
||||
})
|
||||
|
||||
it('lists root directory via empty path', async () => {
|
||||
const response = await rpc.handleRequest({
|
||||
method: 'session-test:listDirectory',
|
||||
params: JSON.stringify({ path: '' })
|
||||
})
|
||||
|
||||
const parsed = JSON.parse(response) as { success: boolean; entries?: Array<{ name: string; type: string }> }
|
||||
expect(parsed.success).toBe(true)
|
||||
|
||||
const names = (parsed.entries ?? []).map((entry) => entry.name)
|
||||
expect(names).toContain('src')
|
||||
expect(names).toContain('README.md')
|
||||
})
|
||||
|
||||
it('skips symlink stat in listDirectory', async () => {
|
||||
try {
|
||||
await symlink('/definitely-not-a-real-path', join(rootDir, 'bad-link'))
|
||||
} catch {
|
||||
// symlink may be disallowed on some systems; skip the test
|
||||
return
|
||||
}
|
||||
|
||||
const response = await rpc.handleRequest({
|
||||
method: 'session-test:listDirectory',
|
||||
params: JSON.stringify({ path: '' })
|
||||
})
|
||||
const parsed = JSON.parse(response) as { success: boolean; entries?: Array<{ name: string; type: string; size?: number }> }
|
||||
expect(parsed.success).toBe(true)
|
||||
const link = (parsed.entries ?? []).find((entry) => entry.name === 'bad-link')
|
||||
expect(link).toBeTruthy()
|
||||
expect(link?.type).toBe('other')
|
||||
expect(link?.size).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
import { logger } from '@/ui/logger'
|
||||
import { readdir, stat } from 'fs/promises'
|
||||
import { basename, join } from 'path'
|
||||
import { basename, join, resolve } from 'path'
|
||||
import type { RpcHandlerManager } from '@/api/rpc/RpcHandlerManager'
|
||||
import { validatePath } from '../pathSecurity'
|
||||
import { getErrorMessage, rpcError } from '../rpcResponses'
|
||||
@@ -46,17 +46,20 @@ export function registerDirectoryHandlers(rpcHandlerManager: RpcHandlerManager,
|
||||
rpcHandlerManager.registerHandler<ListDirectoryRequest, ListDirectoryResponse>('listDirectory', async (data) => {
|
||||
logger.debug('List directory request:', data.path)
|
||||
|
||||
const validation = validatePath(data.path, workingDirectory)
|
||||
const targetPath = data.path || '.'
|
||||
|
||||
const validation = validatePath(targetPath, workingDirectory)
|
||||
if (!validation.valid) {
|
||||
return rpcError(validation.error ?? 'Invalid directory path')
|
||||
}
|
||||
|
||||
try {
|
||||
const entries = await readdir(data.path, { withFileTypes: true })
|
||||
const resolvedPath = resolve(workingDirectory, targetPath)
|
||||
const entries = await readdir(resolvedPath, { withFileTypes: true })
|
||||
|
||||
const directoryEntries: DirectoryEntry[] = await Promise.all(
|
||||
entries.map(async (entry) => {
|
||||
const fullPath = join(data.path, entry.name)
|
||||
const fullPath = join(resolvedPath, entry.name)
|
||||
let type: 'file' | 'directory' | 'other' = 'other'
|
||||
let size: number | undefined
|
||||
let modified: number | undefined
|
||||
@@ -65,14 +68,18 @@ export function registerDirectoryHandlers(rpcHandlerManager: RpcHandlerManager,
|
||||
type = 'directory'
|
||||
} else if (entry.isFile()) {
|
||||
type = 'file'
|
||||
} else if (entry.isSymbolicLink()) {
|
||||
type = 'other'
|
||||
}
|
||||
|
||||
try {
|
||||
const stats = await stat(fullPath)
|
||||
size = stats.size
|
||||
modified = stats.mtime.getTime()
|
||||
} catch (error) {
|
||||
logger.debug(`Failed to stat ${fullPath}:`, error)
|
||||
if (!entry.isSymbolicLink()) {
|
||||
try {
|
||||
const stats = await stat(fullPath)
|
||||
size = stats.size
|
||||
modified = stats.mtime.getTime()
|
||||
} catch (error) {
|
||||
logger.debug(`Failed to stat ${fullPath}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -100,11 +107,15 @@ export function registerDirectoryHandlers(rpcHandlerManager: RpcHandlerManager,
|
||||
rpcHandlerManager.registerHandler<GetDirectoryTreeRequest, GetDirectoryTreeResponse>('getDirectoryTree', async (data) => {
|
||||
logger.debug('Get directory tree request:', data.path, 'maxDepth:', data.maxDepth)
|
||||
|
||||
const validation = validatePath(data.path, workingDirectory)
|
||||
const targetPath = data.path || '.'
|
||||
|
||||
const validation = validatePath(targetPath, workingDirectory)
|
||||
if (!validation.valid) {
|
||||
return rpcError(validation.error ?? 'Invalid directory path')
|
||||
}
|
||||
|
||||
const resolvedRoot = resolve(workingDirectory, targetPath)
|
||||
|
||||
async function buildTree(path: string, name: string, currentDepth: number): Promise<TreeNode | null> {
|
||||
try {
|
||||
const stats = await stat(path)
|
||||
@@ -157,8 +168,8 @@ export function registerDirectoryHandlers(rpcHandlerManager: RpcHandlerManager,
|
||||
return rpcError('maxDepth must be non-negative')
|
||||
}
|
||||
|
||||
const baseName = data.path === '/' ? '/' : basename(data.path) || data.path
|
||||
const tree = await buildTree(data.path, baseName, 0)
|
||||
const baseName = resolvedRoot === '/' ? '/' : basename(resolvedRoot) || resolvedRoot
|
||||
const tree = await buildTree(resolvedRoot, baseName, 0)
|
||||
|
||||
if (!tree) {
|
||||
return rpcError('Failed to access the specified path')
|
||||
|
||||
Reference in New Issue
Block a user