# How the pi-web Directory Browser Works With File Upload and Directory Picker

> Discover how the pi-web directory browser facilitates filesystem navigation file uploads and directory selection using a platform-agnostic library and React picker component Learn more today

- Repository: [Alex Yang/pi-web](https://github.com/agegr/pi-web)
- Tags: internals
- Published: 2026-08-16

---

**The pi-web directory browser combines a platform-agnostic library ([`lib/directory-browser.ts`](https://github.com/agegr/pi-web/blob/main/lib/directory-browser.ts)), a Next.js API route ([`app/api/cwd/browse/route.ts`](https://github.com/agegr/pi-web/blob/main/app/api/cwd/browse/route.ts)), and a React picker component ([`DirectoryPicker.tsx`](https://github.com/agegr/pi-web/blob/main/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`](https://github.com/agegr/pi-web/blob/main/lib/directory-browser.ts) |
| **API** | HTTP interface between library and browser | [`app/api/cwd/browse/route.ts`](https://github.com/agegr/pi-web/blob/main/app/api/cwd/browse/route.ts) |
| **UI** | Interactive picker and upload components | [`components/DirectoryPicker.tsx`](https://github.com/agegr/pi-web/blob/main/components/DirectoryPicker.tsx), [`components/FileExplorer.tsx`](https://github.com/agegr/pi-web/blob/main/components/FileExplorer.tsx) |

### The Library Layer: lib/directory-browser.ts

The [`lib/directory-browser.ts`](https://github.com/agegr/pi-web/blob/main/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 resolution
- **`listWindowsDrives()`** — Enumerates available drives on Windows systems
- **`resolveDirectory(path?: string)`** — Normalizes and validates a directory path, falling back to the user's home directory
- **`getParentDirectory(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:

```typescript
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:

1. **No explicit path on Windows** → Return drive list via `listWindowsDrives()`
2. **Explicit or home directory** → Resolve with `resolveDirectory()` and list children via `listDirectories()`

The response shape (`BrowseResponse`) includes:

```typescript
{
  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`](https://github.com/agegr/pi-web/blob/main/DirectoryPicker.tsx) component manages UI state and coordinates with the browse API. Its key behaviors include:

- **Initial load** — Calls `loadDirectories()` which fetches `/api/cwd/browse` without a path parameter
- **Navigation** — Clicking a folder triggers `navigateTo(entry.path)`, refreshing the directory list
- **Parent traversal** — Uses `parentPath` from 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`](https://github.com/agegr/pi-web/blob/main/FileExplorer.tsx) component manages this through several coordinated steps.

### Upload Trigger and API Call

The `uploadFiles` function (defined in [`FileExplorer.tsx`](https://github.com/agegr/pi-web/blob/main/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:

1. **Directory extraction** — `getUploadDirectory` resolves the target from URL parameters
2. **Name validation** — `validateUploadNames` from [`lib/file-upload.ts`](https://github.com/agegr/pi-web/blob/main/lib/file-upload.ts) ensures safety
3. **Size enforcement** — 25 MiB per-file limit
4. **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:

```typescript
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:

```tsx
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 files
- `skipped` — Files skipped due to conflict strategy
- `errors` — Files that failed validation or write operations

## Key Implementation Files

| File | Purpose |
|------|---------|
| [`lib/directory-browser.ts`](https://github.com/agegr/pi-web/blob/main/lib/directory-browser.ts) | Core filesystem abstraction with Windows drive support |
| [`app/api/cwd/browse/route.ts`](https://github.com/agegr/pi-web/blob/main/app/api/cwd/browse/route.ts) | Next.js route exposing directory listings as JSON |
| [`components/DirectoryPicker.tsx`](https://github.com/agegr/pi-web/blob/main/components/DirectoryPicker.tsx) | React component for interactive directory selection |
| [`components/FileExplorer.tsx`](https://github.com/agegr/pi-web/blob/main/components/FileExplorer.tsx) | File tree UI and upload orchestration |
| [`lib/file-upload.ts`](https://github.com/agegr/pi-web/blob/main/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`](https://github.com/agegr/pi-web/blob/main/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 `validateUploadNames` to 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`](https://github.com/agegr/pi-web/blob/main/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`](https://github.com/agegr/pi-web/blob/main/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.