> ## 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](https://bunny.net/docs/scripting/limits.md) 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 | 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](https://bunny.net/docs/scripting/limits.md) - Resource limits and quotas This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.