mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
A symlink to a directory was listed as a file on SSH and managed hosts, so projects behind a linked path could not be reached. Follow links when classifying entries; broken links and loops stay files. Co-authored-by: Gideon <jjin6573@naver.com>
79 lines
2.5 KiB
TypeScript
79 lines
2.5 KiB
TypeScript
import { readdir, stat } from 'node:fs/promises'
|
|
import { homedir } from 'node:os'
|
|
import type { Dirent } from 'node:fs'
|
|
import { isAbsolute, join, resolve } from 'node:path'
|
|
import type { DirEntry, FilesystemPathFlavor } from '../../shared/filesystem-entry-types'
|
|
import { sortDirEntries } from '../../shared/file-name-sort'
|
|
import { probeGitAvailability } from '../git/git-availability'
|
|
import { gitExecFileAsync } from '../git/runner'
|
|
import { isServerDriveListRequest, listWindowsDrives } from './windows-drive-listing'
|
|
|
|
function resolveServerBrowsePath(pathValue: string): string {
|
|
const trimmed = pathValue.trim() || '~'
|
|
if (trimmed.includes('\0')) {
|
|
throw new Error('Path cannot contain null bytes')
|
|
}
|
|
if (trimmed === '~') {
|
|
return homedir()
|
|
}
|
|
if (/^~[\\/]/.test(trimmed)) {
|
|
return resolve(homedir(), trimmed.slice(2))
|
|
}
|
|
if (isAbsolute(trimmed)) {
|
|
return resolve(trimmed)
|
|
}
|
|
return resolve(homedir(), trimmed)
|
|
}
|
|
|
|
// Why: Dirent.isDirectory() is false for a symlink, so a linked folder would be unreachable; follow it, and treat broken links or loops (ENOENT/ELOOP) as files.
|
|
async function isBrowseEntryDirectory(dirPath: string, entry: Dirent): Promise<boolean> {
|
|
if (entry.isDirectory()) {
|
|
return true
|
|
}
|
|
if (!entry.isSymbolicLink()) {
|
|
return false
|
|
}
|
|
try {
|
|
return (await stat(join(dirPath, entry.name))).isDirectory()
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
export class RuntimeServerEnvironmentCommands {
|
|
async browseDirectory(pathValue: string): Promise<{
|
|
resolvedPath: string
|
|
entries: DirEntry[]
|
|
pathFlavor: FilesystemPathFlavor
|
|
}> {
|
|
if (isServerDriveListRequest(pathValue)) {
|
|
return listWindowsDrives()
|
|
}
|
|
const dirPath = resolveServerBrowsePath(pathValue)
|
|
const dirStat = await stat(dirPath)
|
|
if (!dirStat.isDirectory()) {
|
|
throw new Error(`${dirPath} is not a directory`)
|
|
}
|
|
const entries = await readdir(dirPath, { withFileTypes: true })
|
|
const mapped = await Promise.all(
|
|
entries
|
|
.filter((entry) => entry.name !== '.' && entry.name !== '..')
|
|
.map(async (entry) => ({
|
|
name: entry.name,
|
|
isDirectory: await isBrowseEntryDirectory(dirPath, entry),
|
|
isSymlink: entry.isSymbolicLink()
|
|
}))
|
|
)
|
|
sortDirEntries(mapped)
|
|
return {
|
|
resolvedPath: dirPath,
|
|
entries: mapped,
|
|
pathFlavor: process.platform === 'win32' ? 'win32' : 'posix'
|
|
}
|
|
}
|
|
|
|
async isGitAvailable(): Promise<boolean> {
|
|
return probeGitAvailability(gitExecFileAsync, { cwd: process.cwd(), timeout: 3000 })
|
|
}
|
|
}
|