Copy page
View as Markdown View this page as plain text

File System

Each thread has an isolated file system for storing files, images, and other binary data. It is addressable through ThreadState and survives execution cycles — two separate invocations of the same thread see the same file tree.

This page documents the file system APIs. For an overview of threads themselves, see Threads.

1. File Record

Files are represented by the FileRecord type:

PropertyTypeDescription
pathstringFile path (relative to thread root)
namestringFile name
mimeTypestringMIME type
storagestringImplementation-defined storage identifier
sizenumberFile size in bytes
isDirectorybooleanWhether this is a directory
metadataRecord<string, unknown>Custom metadata
widthnumberImage width in pixels (if applicable)
heightnumberImage height in pixels (if applicable)

2. Writing Files

// Write a text file
const record = await state.writeFile(
  '/data/config.json',
  JSON.stringify({ key: 'value' }),
  'application/json'
);

// Write a binary file with metadata
const imageRecord = await state.writeFile(
  '/images/photo.jpg',
  imageBuffer,
  'image/jpeg',
  { width: 1920, height: 1080 }
);

3. Reading Files

// Read file content
const data = await state.readFile('/data/config.json');
if (data) {
  const text = new TextDecoder().decode(data);
  const config = JSON.parse(text);
}

// Get file metadata without reading content
const info = await state.statFile('/images/photo.jpg');
if (info) {
  console.log(`Size: ${info.size} bytes`);
}

4. Directory Operations

// Create a directory
await state.mkdirFile('/documents');

// List directory contents
const { entries } = await state.readdirFile('/documents');
for (const entry of entries) {
  console.log(`${entry.isDirectory ? 'DIR' : 'FILE'}: ${entry.name}`);
}

// Remove an empty directory
await state.rmdirFile('/documents');

// Delete a file
await state.unlinkFile('/data/config.json');

5. Moving and Renaming

moveFile relocates a file or directory to a new path, and renameFile changes an entry’s name while keeping it in the same directory. Both return the moved entry’s FileRecord at its new path.

// Move a file to a different directory
await state.moveFile('/drafts/post.md', '/published/post.md');

// Move an entire directory — the whole subtree moves with it
await state.moveFile('/tmp/work', '/archive/2024/work');

// Rename in place (newName must not contain path separators)
await state.renameFile('/notes/draft.txt', 'final.txt');

Both operations apply to files and directories. Moving a directory relocates its entire subtree, preserving the relative layout of its contents.

Constraints:

  1. Source must exist. Moving a missing path throws.
  2. Destination must be free. moveFile does not overwrite; if the destination path (or a subtree rooted at it) already exists, it throws. Delete or move the existing entry first.
  3. No moving a directory into itself. A directory cannot be moved to its own path or to one of its descendants (e.g. /a/a/b).
  4. renameFile names are bare. newName must not contain /; use moveFile to change an entry’s directory.

6. Search Operations

// Search file contents
const grepResults = await state.grepFiles('error');
for (const result of grepResults) {
  console.log(`${result.path}:`);
  for (const match of result.matches) {
    console.log(`  Line ${match.line}: ${match.content}`);
  }
}

// Find files by glob pattern
const { paths } = await state.findFiles('**/*.json');
console.log('JSON files:', paths);

7. File Statistics

const stats = await state.getFileStats();
console.log(`Files: ${stats.fileCount}`);
console.log(`Directories: ${stats.directoryCount}`);
console.log(`Total size: ${stats.totalSize} bytes`);

8. Image Thumbnails

const thumbnail = await state.getFileThumbnail('/images/photo.jpg');
if (thumbnail) {
  // Use thumbnail data
}

9. Streaming Files

For large files, readFileStream provides memory-efficient access by yielding data in chunks.

9.1 Method

readFileStream(
  path: string,
  options?: ReadFileStreamOptions
): Promise<AsyncIterable<FileChunk> | null>

9.2 Options

PropertyTypeDescription
signalAbortSignalOptional abort signal for cancellation

9.3 FileChunk

PropertyTypeDescription
dataUint8ArrayRaw binary data for this chunk
indexnumber0-based index of this chunk
totalChunksnumberTotal number of chunks
isLastbooleanWhether this is the final chunk

9.4 Behavior

  1. File Not Found: Returns null if the file does not exist.
  2. External Files: Returns null for files whose storage identifier references an external system (object storage, a remote URL, etc.). External files must be fetched using their storage location.
  3. Small Files: Files under the chunk threshold are yielded as a single chunk with index: 0, totalChunks: 1, and isLast: true.
  4. Chunked Files: Large files yield multiple chunks in sequential order (0, 1, 2…).
  5. Ordering Guarantee: Chunks are always yielded in sequential order. Implementations MUST NOT yield chunks out of order.
  6. Abort Signal: When triggered, the stream stops yielding new chunks. Already-yielded chunks remain valid.

9.5 Example: Processing Large File

const stream = await state.readFileStream('/uploads/video.mp4');
if (!stream) {
  throw new Error('File not found');
}

for await (const chunk of stream) {
  console.log(`Progress: ${chunk.index + 1}/${chunk.totalChunks}`);
  // Process chunk.data without loading entire file into memory
}

9.6 Example: HTTP Streaming Response

const stream = await state.readFileStream('/uploads/large-file.bin');
if (!stream) {
  return new Response('Not Found', { status: 404 });
}

const readable = new ReadableStream({
  async start(controller) {
    for await (const chunk of stream) {
      controller.enqueue(chunk.data);
    }
    controller.close();
  }
});

return new Response(readable, {
  headers: { 'Content-Type': 'application/octet-stream' }
});