# File System

Per-thread file storage with SQLite backing

Each thread has isolated file storage accessible via these endpoints. Files are stored in the thread's SQLite database with support for external references (URLs, S3, R2).

## File paths in URLs

`{path}` is the file's path in the thread file system without its leading `/`, with each path segment percent-encoded.
The `path` of an attachment is such a path, so encode each segment before placing it in a URL:

```ts
const encoded = path.replace(/^\/+/, '').split('/').map(encodeURIComponent).join('/')
const url = `/api/threads/${threadId}/fs/${encoded}`
```

The file `/my dir/a file.txt` is served at `/api/threads/{threadId}/fs/my%20dir/a%20file.txt`.
`getFileUrl()` in `@standardagents/client`, `@standardagents/react`, and `@standardagents/vue` encodes paths this way.

The server decodes each segment separately, for reads, writes, and deletes.
An encoded slash (`%2F`) inside a segment is not a path separator, and no file name contains a slash.
A path with such a segment or with a malformed escape (for example a bare `%`) names no file: a read or delete returns `404`, and a write returns `400`.

## List Directory / Query Files

- threadId (string), required

  The thread identifier

* stats (boolean)

  Return storage statistics instead of file list

- grep (string)

  Full-text search pattern

* find (string)

  Glob pattern for path matching (e.g., `*.jpg`)

```bash
# List root directory
GET /api/threads/{threadId}/fs

# Get storage stats
GET /api/threads/{threadId}/fs?stats=1

# Full-text search
GET /api/threads/{threadId}/fs?grep=searchterm

# Find by pattern
GET /api/threads/{threadId}/fs?find=*.jpg
```

```json List Directory
{
  "files": [
    {
      "path": "/photo.jpg",
      "name": "photo.jpg",
      "mimeType": "image/jpeg",
      "storage": "local",
      "size": 245123,
      "isDirectory": false,
      "createdAt": 1704067200000,
      "width": 2048,
      "height": 1536
    }
  ],
  "path": "/"
}
```

```json Storage Stats
{
  "totalSize": 5242880,
  "fileCount": 12,
  "directoryCount": 2
}
```

---

## Get File

- threadId (string), required

  The thread identifier

* path (string), required

  File path (e.g., `attachments/photo.jpg`)

- thumbnail (boolean)

  Return image thumbnail instead of full file

* format (string)

  Set to `json` to return metadata only

```bash
# Get file content
GET /api/threads/{threadId}/fs/{path}

# Get thumbnail
GET /api/threads/{threadId}/fs/{path}?thumbnail=1

# Get metadata only
GET /api/threads/{threadId}/fs/{path}?format=json
```

Local files return binary data with `Content-Type` and `X-Content-Type-Options: nosniff`.
HTML, SVG, and XHTML files use `Content-Disposition: attachment`.
`format=json` returns `{ file: FileRecord }`.
An external reference returns `{ file, external: true, location }`.
A directory path returns `{ files, path }`.

---

## Write File

- threadId (string), required

  The thread identifier

* path (string), required

  File path to write

- data (string)

  Base64-encoded file data

* mimeType (string)

  MIME type of the file

- metadata (object)

  Optional custom metadata

* width (number)

  Image width in pixels (for images)

- height (number)

  Image height in pixels (for images)

* thumbnail (string)

  Base64-encoded thumbnail for images

- size (number)

  Size in bytes for an external reference.

* type (string)

  Set to `"directory"` to create a directory

- action (string)

  Set `move` with a destination path in `destination`.
  Set `rename` with a filename in `newName`.
  A successful move or rename returns `200` with an attachment reference.

* location (string)

  External URL for linked files (instead of `data`)

```bash
# Write file (JSON)
POST /api/threads/{threadId}/fs/{path}
Content-Type: application/json
{
  "data": "base64...",
  "mimeType": "image/jpeg"
}

# Create directory
POST /api/threads/{threadId}/fs/{path}
Content-Type: application/json
{ "type": "directory" }

# Link external file
POST /api/threads/{threadId}/fs/{path}
Content-Type: application/json
{
  "location": "https://example.com/file.pdf",
  "mimeType": "application/pdf",
  "size": 1024000
}

# Raw binary upload (auto-chunks large files)
POST /api/threads/{threadId}/fs/{path}
Content-Type: application/octet-stream
[binary data]
```

> **Note:**
>
> Files larger than 1.75 MiB use chunked storage.
> A binary POST can stream into chunks when its `Content-Length` exceeds that threshold.
> A normal POST accepts the complete binary body.
> The file appears when the whole upload has arrived.
> Until then, the file at that path stays readable.
> A second large upload to a path that is still receiving one returns 400.

```json 201
{
  "id": "/attachments/photo.jpg",
  "type": "file",
  "path": "/attachments/photo.jpg",
  "name": "photo.jpg",
  "mimeType": "image/jpeg",
  "size": 245123,
  "width": 2048,
  "height": 1536
}
```

---

## Delete File

- threadId (string), required

  The thread identifier

* path (string), required

  File or directory path to delete

```bash
DELETE /api/threads/{threadId}/fs/{path}
```

Returns `204 No Content` on success. Directories must be empty to delete.

---

## FileRecord Schema

```typescript
interface FileRecord {
  path: string;           // Full path (e.g., "/attachments/photo.jpg")
  name: string;           // Filename
  mimeType: string;       // MIME type
  storage: 'local' | 'url' | 's3' | 'r2';
  location?: string | null; // External URL for linked files
  size: number;           // Size in bytes
  metadata?: Record<string, unknown> | null; // Custom metadata
  isDirectory: boolean;
  createdAt: number;      // Unix timestamp (ms)
  width?: number | null;  // Image width in pixels
  height?: number | null; // Image height in pixels
  isChunked?: boolean;    // True if file is stored in chunks (>1.75MB)
  chunkCount?: number;    // Number of chunks (if chunked)
}
```
