How the pi-web Directory Browser Works With File Upload and Directory Picker
The pi-web directory browser combines a platform-agnostic library (lib/directory-browser.ts), a Next.js API route (app/api/cwd/browse/route.ts), and a React picker component (DirectoryPicker.tsx) to let users navigate the filesystem, select a target directory, and upload files through a validated streaming endpoint.
The pi-web repository implements a complete file-management workflow that bridges server-side directory operations with a browser-based UI. At its core, three layers cooperate: a reusable directory-browsing library, REST endpoints that expose those capabilities, and React components that render the interface and handle uploads. This article breaks down exactly how these pieces fit together, with specific file paths and implementation details from the source code.
Directory Browser Architecture: The Three Core Layers
Every directory selection and file upload in pi-web flows through the same pipeline. Understanding the boundaries between these layers helps diagnose issues or extend the system.
| Layer | Responsibility | Primary File(s) |
|---|---|---|
| Library | Platform-agnostic filesystem operations (resolve, list, navigate) | lib/directory-browser.ts |
| API | HTTP interface between library and browser | app/api/cwd/browse/route.ts |
| UI | Interactive picker and upload components | components/DirectoryPicker.tsx, components/FileExplorer.tsx |
The Library Layer: lib/directory-browser.ts
The lib/directory-browser.ts module provides the foundational operations used throughout the application. It exports several key functions:
listDirectories(directory: string)— Returns subdirectories of a given path, including symlink resolutionlistWindowsDrives()— Enumerates available drives on Windows systemsresolveDirectory(path?: string)— Normalizes and validates a directory path, falling back to the user's home directorygetParentDirectory(directory: string)— Computes the parent path with Windows drive-root detection
The listDirectories implementation is particularly robust. It filters for actual directories, resolves symbolic links that point to directories, and returns sorted results:
export async function listDirectories(directory: string): Promise<BrowsableDirectory[]> {
const entries = await readdir(directory, { withFileTypes: true });
const candidates = await Promise.all(
entries.map(async (entry) => {
if (entry.isDirectory()) {
return { name: entry.name, path: path.join(directory, entry.name) };
}
if (!entry.isSymbolicLink()) return null;
// Resolve symlinks that point at directories
try {
const real = await realpath(path.join(directory, entry.name));
const stat = await stat(real);
return stat.isDirectory() ? { name: entry.name, path: path.join(directory, entry.name) } : null;
} catch {
return null;
}
})
);
return candidates.filter((e): e is BrowsableDirectory => e !== null)
.sort((a, b) => a.name.localeCompare(b.name));
}
Windows receive special handling through shouldShowWindowsDrivePicker and listWindowsDrives, ensuring the picker shows C:\, D:\, etc. when appropriate.
The Browse API: app/api/cwd/browse/route.ts
The browse API translates library calls into JSON responses consumable by the React frontend. The GET handler implements conditional logic based on the request context:
- No explicit path on Windows → Return drive list via
listWindowsDrives() - Explicit or home directory → Resolve with
resolveDirectory()and list children vialistDirectories()
The response shape (BrowseResponse) includes:
{
path: string; // Current absolute path
parentPath: string; // Parent directory or null at drive root
directories: BrowsableDirectory[]; // Subdirectories for display
drives?: string[]; // Windows drives (when applicable)
}
This single endpoint powers the entire directory navigation experience. The picker calls it repeatedly as users drill deeper into the filesystem.
DirectoryPicker Component: Interactive Directory Selection
The DirectoryPicker.tsx component manages UI state and coordinates with the browse API. Its key behaviors include:
- Initial load — Calls
loadDirectories()which fetches/api/cwd/browsewithout a path parameter - Navigation — Clicking a folder triggers
navigateTo(entry.path), refreshing the directory list - Parent traversal — Uses
parentPathfrom the API response, with special handling for Windows drive roots - Selection confirmation — Calls
onSelect(currentPath)when the user confirms their choice
The component renders folder entries with FolderIcon and drive entries with DriveIcon, giving visual distinction to the navigation hierarchy.
File Upload Integration: From Picker to Server
Once a directory is selected, the upload workflow takes over. The FileExplorer.tsx component manages this through several coordinated steps.
Upload Trigger and API Call
The uploadFiles function (defined in FileExplorer.tsx) constructs a POST request to:
/api/files/${encodeFilePathForApi(cwd)}?type=upload&conflict=${strategy}
The strategy parameter controls conflict handling ("overwrite", "skip", or "rename"). The function accepts a progress callback for UI updates during multi-file uploads.
Server-Side Upload Handler: app/api/files/[...path]/route.ts
The upload route performs several critical validations before accepting file data:
- Directory extraction —
getUploadDirectoryresolves the target from URL parameters - Name validation —
validateUploadNamesfromlib/file-upload.tsensures safety - Size enforcement — 25 MiB per-file limit
- Streaming — File data streams directly to disk without buffering entire files in memory
Upload Validation: lib/file-upload.ts
The validation layer prevents path traversal attacks and duplicate filename collisions:
export function validateUploadNames(fileNames: string[]): string | null {
const seen = new Set<string>();
for (const name of fileNames) {
if (name.includes("/") || name.includes("\\")) return `Invalid file name: ${name}`;
if (seen.has(name)) return `Duplicate file name in upload: ${name}`;
seen.add(name);
}
return null;
}
This strict validation ensures uploaded files land exactly in the selected directory without escaping to parent paths.
Complete Upload Flow Example
Here's a practical implementation combining directory selection with file upload:
import { DirectoryPicker } from "@/components/DirectoryPicker";
import { uploadFiles } from "@/components/FileExplorer";
function UploadDialog({ onClose }: { onClose: () => void }) {
const startUpload = async (targetPath: string, files: File[]) => {
// Simple "overwrite" strategy; you could expose a UI for conflict handling.
const { status, data } = await uploadFiles(
targetPath,
files,
"overwrite",
() => {} // progress callback
);
if (status === 200) {
console.log("Uploaded:", data.uploaded);
} else {
console.error("Upload failed:", data);
}
onClose();
};
return (
<DirectoryPicker
onCancel={onClose}
onSelect={(path) => {
// Suppose we have a FileList from an <input>.
const fileInput = document.querySelector<HTMLInputElement>("#fileInput");
const files = Array.from(fileInput!.files!);
void startUpload(path, files);
}}
/>
);
}
The data response includes three arrays for result handling:
uploaded— Successfully written filesskipped— Files skipped due to conflict strategyerrors— Files that failed validation or write operations
Key Implementation Files
| File | Purpose |
|---|---|
lib/directory-browser.ts |
Core filesystem abstraction with Windows drive support |
app/api/cwd/browse/route.ts |
Next.js route exposing directory listings as JSON |
components/DirectoryPicker.tsx |
React component for interactive directory selection |
components/FileExplorer.tsx |
File tree UI and upload orchestration |
lib/file-upload.ts |
Filename validation and upload constraints |
app/api/files/[...path]/route.ts |
Streaming upload handler with result summarization |
Summary
- pi-web's directory browser centralizes filesystem logic in
lib/directory-browser.ts, ensuring consistent behavior across platforms including Windows drive enumeration. - The browse API (
/api/cwd/browse) returns structured JSON with path, parent, children, and optional drive lists that the picker consumes directly. - DirectoryPicker manages navigation state locally, refreshing directory listings through repeated API calls as users explore the filesystem.
- File uploads validate names strictly through
validateUploadNamesto prevent path traversal, stream content directly to disk, and return detailed result summaries. - The same conflict strategy (
overwrite,skip,rename) applies across all uploads, controlled via query parameter to the files API.
Frequently Asked Questions
How does pi-web handle Windows drive selection differently from Unix paths?
On Windows, when no explicit path is provided, app/api/cwd/browse/route.ts detects this via shouldShowWindowsDrivePicker and returns the list from listWindowsDrives() instead of attempting to list a directory. The picker renders these as drive buttons. Once a drive is selected, normal directory navigation resumes. This behavior is transparent on Unix systems where listWindowsDrives is never invoked.
What prevents uploaded files from escaping the selected directory?
Two mechanisms enforce this: validateUploadNames in lib/file-upload.ts rejects any filename containing forward or backward slashes, and the upload handler in app/api/files/[...path]/route.ts resolves the target directory through getUploadDirectory before any file operations occur. These layers ensure uploaded files are written with simple basenames within the explicitly selected path.
Can the directory picker start at a specific path rather than the home directory?
Yes. The DirectoryPicker component accepts an optional initialPath prop. When provided, navigateTo(initialPath) is called on mount instead of the parameterless call that triggers home-directory resolution. This allows workflows that need contextual starting points, such as reopening the last-used directory or starting from a project root.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →