Files
orca/src/main/runtime/runtime-server-environment-commands.ts
T
NeilandGideon c8ad8ea313 fix(projects): list symlinked folders as folders in the remote Add Project browser (#29658)
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>
2026-10-16 05:21:44 -07:00

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 })
}
}