> ## Documentation Index
> Fetch the complete documentation index at: https://bunny.net/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Node:FS

> Read and write files in Edge Scripts using the Node.js-compatible file system API

> [!WARNING]
>
> No breaking changes are expected, but additional features will be implemented soon.

## Overview

Edge Scripting supports the Node.js file system API through the `node:fs` and
`node:fs/promises` modules, so you can read, write, and manipulate files and
directories from inside an Edge Script.

The implementation sits on Deno's sandboxed environment with Node.js
compatibility. Both the Promise-based API (`node:fs/promises`) and the older
callback-based API (`node:fs`) work. We recommend the Promise-based one for the
cleaner async/await syntax.

## Virtual file system

Every script gets a virtual file system for file operations. It lives in the
virtual memory available to the worker.

The directory tree currently has two folders:

```
/ # Root directory
├── home/
│   └── user/ # Your current working directory, R/W access
└── tmp/ # Temporary directory, R/W access
```

You can create directories anywhere. Permissions are enforced for the owner (user) but not for group or others.
All files are opened in `w+`.

> [!NOTE]
>
> Avoid creating directories in the root directory `/`. We plan to add folders
> there for other functions, such as `/dev/*` and other read only directories.

Paths in the virtual file system are limited to **4096 characters** (`foo/bar`
counts as 7) and to **48 segments** (`a/b/c` is 3 segments).

Capacity checks for creating, copying and moving happen before the operation
runs. Copying a large file can therefore fail with an out of memory error even
though there is still space to create new files or directories.

## Importing the module

EdgeScripting supports both the Promise-based API and the callback-based API.

### Promise-based API (recommended)

```typescript theme={null}
import * as fs from "node:fs/promises";

// Use with async/await
const data = await fs.readFile("/tmp/file.txt", "utf-8");
```

### Callback-based API

```typescript theme={null}
import * as fs from "node:fs";

// Use with callbacks
fs.readFile("/tmp/file.txt", "utf-8", (err, data) => {
  if (err) {
    console.error("Error:", err);
    return;
  }
  console.log("Data:", data);
});
```

## Limitations

The file system runs in a sandbox, which constrains what it can do. The
following sections cover where it differs from Node.js.

> [!NOTE]
>
> Always handle file system errors gracefully. The sandboxed environment may
> behave differently from traditional Node.js environments.

### Symbolic links and hard links

Symbolic links are supported. `symlink()`, `symlinkSync()`, `readlink()`, `readlinkSync()`, and `lstat()` work as expected.

> [!WARNING]
>
> Hard links are not supported. `link()` and `linkSync()` will throw errors.

### File statistics and timestamps

All `Stats` timestamp properties are supported and return correct values:

**Reliable Stats properties:**

* `size` - File size in bytes
* `isFile()` - Check if entry is a file
* `isDirectory()` - Check if entry is a directory
* `atime` - Last access time
* `mtime` - Last modification time
* `ctime` - Last status change time
* `atimeMs`, `mtimeMs`, `ctimeMs` - Millisecond timestamps
* `birthtime` - File creation time

### File and directory permissions

File and directory permissions are supported for the **owner (user)** bits. `chmod()` and `chmodSync()` work, and `Stats.mode` reflects the permissions you set. `access()` respects owner-level read, write, and execute bits.

> [!NOTE]
>
> **Group and others** permission bits are accepted but have no effect: the sandboxed environment always runs as the file owner (uid=1000, gid=1000), so only the user/owner bits matter.
>
> `chown()` and `chownSync()` are implemented for compatibility, but ownership is not enforced. Changing the uid or gid has no effect on file access or permissions.

### File watching

File system watching is not supported.

> [!WARNING]
>
> The following operations are unavailable:
>
> * `watch()` - Watch for file changes
> * `watchFile()` - Poll for file changes
> * `unwatchFile()` - Stop watching
> * `FSWatcher` class

**Workaround:** Use polling with `stat()` if you need to detect changes, but
be mindful of performance implications and CPU time limits.

### Performance considerations

> [!NOTE]
>
> **Memory limits:** The whole Virtual FileSystem lives inside your script memory.
> The current file system limits of your script is set up to 64MB.
> Reading large files may cause memory exhaustion if you store it in memory.
> Leverage streaming to avoid allocating too much memory at a time.
>
> **CPU time:** File I/O counts toward the 30-second CPU time limit per request.
>
> **Best practices:**
>
> * Stream large files instead of reading entirely into memory
> * Clean up temporary files to avoid storage bloat
> * See [Limits](/docs/scripting/limits) for more details on resource constraints

## Quickstart

Common file system patterns in EdgeScripting.

### Example 1: Reading a file

```typescript theme={null}
import * as fs from "node:fs/promises";
import * as BunnySDK from "@bunny.net/edgescript-sdk";

BunnySDK.net.http.serve(async (request: Request) => {
  try {
    // Read file as UTF-8 string
    const content = await fs.readFile("/tmp/data.txt", "utf-8");

    return new Response(content, {
      headers: { "content-type": "text/plain" }
    });
  } catch (error) {
    return new Response(`Error reading file: ${error.message}`, {
      status: 500
    });
  }
});
```

### Example 2: Writing a file

```typescript theme={null}
import * as fs from "node:fs/promises";
import * as BunnySDK from "@bunny.net/edgescript-sdk";

BunnySDK.net.http.serve(async (request: Request) => {
  const data = await request.json();

  try {
    // Write JSON data to file
    await fs.writeFile(
      "/tmp/output.json",
      JSON.stringify(data, null, 2),
      "utf-8"
    );

    return new Response("File written successfully", { status: 201 });
  } catch (error) {
    return new Response(`Error writing file: ${error.message}`, {
      status: 500
    });
  }
});
```

### Example 3: Working with directories

```typescript theme={null}
import * as fs from "node:fs/promises";
import * as BunnySDK from "@bunny.net/edgescript-sdk";

BunnySDK.net.http.serve(async (request: Request) => {
  try {
    // Create directory (recursive option creates parent directories)
    await fs.mkdir("/tmp/myapp/data", { recursive: true });

    // Write a file in the new directory
    await fs.writeFile("/tmp/myapp/data/config.json", "{}", "utf-8");

    // List directory contents
    const files = await fs.readdir("/tmp/myapp/data");

    return new Response(JSON.stringify({ files }), {
      headers: { "content-type": "application/json" }
    });
  } catch (error) {
    return new Response(`Error: ${error.message}`, { status: 500 });
  }
});
```

### Example 4: Error handling patterns

```typescript theme={null}
import * as fs from "node:fs/promises";
import * as BunnySDK from "@bunny.net/edgescript-sdk";

BunnySDK.net.http.serve(async (request: Request) => {
  const filepath = "/tmp/cache/data.txt";

  try {
    // Check if file exists by trying to access it
    await fs.access(filepath);

    // File exists, read it
    const content = await fs.readFile(filepath, "utf-8");
    return new Response(content, {
      headers: { "x-cache": "hit" }
    });
  } catch (error) {
    if (error.code === "ENOENT") {
      // File doesn't exist, create it
      const defaultContent = "Default cached data";
      await fs.writeFile(filepath, defaultContent, "utf-8");

      return new Response(defaultContent, {
        status: 201,
        headers: { "x-cache": "miss" }
      });
    }

    // Re-throw other errors
    throw error;
  }
});
```

### Example 5: Using FileHandle for chunked reading

> [!WARNING]
>
> This script is not optimized. It shows how chunked reading works, so please
> don't run anything like it in production.

```typescript theme={null}
import * as fs from "node:fs/promises";
import * as BunnySDK from "@bunny.net/edgescript-sdk";

// Global promise to track the download operation
let downloadPromise: Promise<void> | null = null;
const filePath = "/tmp/large-file.txt";

async function downloadLoremIpsum() {
  try {
    const response = await fetch('https://lorem-api.com/api/lorem', {
      method: 'GET',
      headers: {
        'Content-Type': 'application/json'
      }
    });

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const content = await response.text();
    
    // Calculate how many times we need to repeat to reach ~10MB
    const targetSize = 10 * 1024 * 1024; // 10MB in bytes
    const contentSize = Buffer.byteLength(content, 'utf-8');
    const repeatCount = Math.ceil(targetSize / contentSize);
    
    // Create the repeated content
    let finalContent = '';
    for (let i = 0; i < repeatCount; i++) {
      finalContent += content;
    }
    
    // Write to file
    await fs.writeFile(filePath, finalContent, 'utf-8');
  } catch (error) {
    // Clear the promise on error so it can be retried
    downloadPromise = null;
    throw error;
  }
}

async function ensureFileExists() {
  try {
    // Check if file exists
    await fs.access(filePath);
  } catch (error) {
    // File doesn't exist, initiate download
    if (!downloadPromise) {
      // Start download and store the promise
      downloadPromise = downloadLoremIpsum();
    }
    // Wait for the download to complete (whether we started it or another request did)
    await downloadPromise;
  }
}

BunnySDK.net.http.serve(async (request: Request) => {
  try {
    // Ensure file exists (download if necessary, blocking until complete)
    await ensureFileExists();

    // Create SSE stream
    const stream = new ReadableStream({
      async start(controller) {
        const encoder = new TextEncoder();
        let fileHandle;
        
        try {
          // Open file for reading
          fileHandle = await fs.open(filePath, "r");
          
          // Get file stats to know the total size
          const stats = await fileHandle.stat();
          const totalSize = stats.size;
          
          // Send initial event with file info
          controller.enqueue(encoder.encode(`data: ${JSON.stringify({
            type: "start",
            totalSize,
            chunkSize: 1024,
            timestamp: new Date().toISOString()
          })}\n\n`));
          
          const buffer = new Uint8Array(1024);
          let position = 0;
          let chunkNumber = 0;
          
          // Read file 1KB at a time
          while (position < totalSize) {
            const { bytesRead } = await fileHandle.read(buffer, 0, 1024, position);
            
            if (bytesRead === 0) break; // End of file
            
            // Convert to string
            const content = new TextDecoder().decode(buffer.slice(0, bytesRead));
            
            // Send chunk as SSE event
            controller.enqueue(encoder.encode(`data: ${JSON.stringify({
              type: "chunk",
              chunkNumber,
              bytesRead,
              position,
              content,
              progress: ((position + bytesRead) / totalSize * 100).toFixed(2) + "%",
              timestamp: new Date().toISOString()
            })}\n\n`));
            
            position += bytesRead;
            chunkNumber++;
            
            // Small delay to prevent overwhelming the client
            await new Promise(resolve => setTimeout(resolve, 10));
          }
          
          // Send completion event
          controller.enqueue(encoder.encode(`data: ${JSON.stringify({
            type: "complete",
            totalChunks: chunkNumber,
            totalBytesRead: position,
            timestamp: new Date().toISOString()
          })}\n\n`));
          
        } catch (error) {
          // Send error event
          controller.enqueue(encoder.encode(`data: ${JSON.stringify({
            type: "error",
            message: error.message,
            timestamp: new Date().toISOString()
          })}\n\n`));
        } finally {
          // Close file handle
          await fileHandle?.close();
          controller.close();
        }
      }
    });
    
    return new Response(stream, {
      headers: {
        "Content-Type": "text/event-stream",
        "Cache-Control": "no-cache",
        "Connection": "keep-alive",
        "X-Accel-Buffering": "no"
      }
    });
    
  } catch (error) {
    return new Response(`Error: ${error.message}`, { status: 500 });
  }
});
```

### Example 6: Symbolic links

```typescript theme={null}
import * as fs from "node:fs/promises";
import * as BunnySDK from "@bunny.net/edgescript-sdk";

BunnySDK.net.http.serve(async (request: Request) => {
  const original = "/tmp/original.txt";
  const link = "/tmp/link.txt";

  try {
    await fs.writeFile(original, "hello from original", "utf-8");

    // Create a symbolic link pointing to the original file
    await fs.symlink(original, link);

    // Reading through the symlink is transparent
    const content = await fs.readFile(link, "utf-8");

    // lstat inspects the link itself, not the target
    const linkStats = await fs.lstat(link);
    const target = await fs.readlink(link);

    return new Response(
      JSON.stringify({ content, isSymlink: linkStats.isSymbolicLink(), target }),
      { headers: { "content-type": "application/json" } }
    );
  } catch (error) {
    return new Response(`Error: ${error.message}`, { status: 500 });
  }
});
```

## References

* [Node.js File System Documentation](https://nodejs.org/api/fs.html) - Complete Node.js fs module reference
* [Node.js fs/promises API](https://nodejs.org/api/fs.html#promises-api) - Promise-based file system API
* [Node.js FileHandle Class](https://nodejs.org/api/fs.html#class-filehandle) - Advanced file operations
* [Node.js File System Flags](https://nodejs.org/api/fs.html#file-system-flags) - Available flags for file operations
* [MDN File API](https://developer.mozilla.org/en-US/docs/Web/API/File) - Web standard File interface
* [MDN Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob) - Binary data objects
* [MDN TextEncoder](https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder) - Encoding strings to bytes
* [MDN TextDecoder](https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder) - Decoding bytes to strings
* [EdgeScripting Limits](/docs/scripting/limits) - Resource limits and quotas


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.