--- title: File System Operations description: Interact with the file system in your sandboxes. --- File system operations are available through the file system module of a sandbox. Each sandbox has its own isolated filesystem, and operations run through the Daytona API, so your application manages files in a sandbox directly, without executing shell commands inside it. The file system module covers listing directories, reading file metadata, creating directories, uploading and downloading files, setting permissions, searching file contents, watching directories for changes, and moving, renaming, or deleting files. The sandbox streams uploads to disk and downloads from disk, so transfer size is bounded by sandbox storage, not sandbox memory. Most SDK methods hold file content in your application's memory; to move large files without buffering them, stream them from and to local disk with the SDK, as in [upload large files](#upload-large-files) and [download large files](#download-large-files), or use [presigned URLs](#presigned-urls) with an HTTP client. File operations assume you are operating in the sandbox user's home directory (e.g. `workspace` implies `/home/[username]/workspace`). Use a leading `/` when providing absolute paths. ## Use cases File system operations are used when an application moves or inspects files in a sandbox without a shell. - **Input files**: an application uploads source code, datasets, or configuration before code runs. Several files upload in one request, and a failed file does not stop the others. - **Results and artifacts**: generated files, reports, and build outputs are downloaded after a run. - **Agent file tools**: an agent lists, reads, searches, and edits files through the API as tools. - **Project-wide edits**: find returns the file, line number, and line of every match. Replace rewrites each listed file, so an agent renames an identifier across a project. - **Large transfers**: large files stream between local disk and the sandbox without being held in the application's memory. - **Change tracking**: a directory watch reports created, written, removed, and renamed files, so an application can sync or rebuild. - **Direct transfers for other clients**: a presigned URL lets a browser or another service upload or download one file without an API key. - **Executable scripts and ownership**: an uploaded script is made executable, or a file is handed to another user or group in the sandbox. ## List files and directories List files and directories. By default, only entries directly inside the directory are listed. A depth option lists nested entries up to 32 levels deep; a listing over 10,000 entries fails, so request a smaller depth for large trees. Symbolic links are listed, not followed. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.fs.write_text("workspace/hello.txt", "hello") # List files in a directory files = sandbox.fs.list("workspace") for file in files: print(f"Name: {file.name}") print(f"Is directory: {file.is_dir}") print(f"Size: {file.size}") print(f"Modified: {file.modified_at}") ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() await sandbox.fs.writeText('workspace/hello.txt', 'hello') // List files in a directory const files = await sandbox.fs.list('workspace') files.items.forEach(file => { console.log(`Name: ${file.name}`) console.log(`Is directory: ${file.isDir}`) console.log(`Size: ${file.size}`) console.log(`Modified: ${file.modifiedAt ?? file.modTime}`) }) ``` ```bash curl 'https://toolbox.daytona.com/toolbox/{sandboxId}/files?path=workspace' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ## Get directory or file information Get metadata for a file or directory. The metadata describes the entry at the path itself: for a symbolic link, it describes the link, not its target. | **Field** | **Format** | | ----------------------- | -------------------------------------------------------------------------- | | **Mode** | Symbolic, such as `-rw-r--r--`; `d` first for a directory, `L` for a link. | | **Permissions** | The four-digit octal form, such as `0644`. | | **Owner** and **group** | Numeric user and group IDs as strings, such as `1000`, not names. | ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.fs.write_text("workspace/data/file.txt", "hello") # Get file metadata info = sandbox.fs.stat("workspace/data/file.txt") print(f"Size: {info.size} bytes") print(f"Modified: {info.modified_at}") print(f"Mode: {info.mode}") ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() await sandbox.fs.writeText('workspace/data/file.txt', 'hello') // Get file metadata const info = await sandbox.fs.stat('workspace/data/file.txt') console.log(`Size: ${info.size} bytes`) console.log(`Modified: ${info.modifiedAt ?? info.modTime}`) console.log(`Mode: ${info.mode}`) ``` ```bash curl 'https://toolbox.daytona.com/toolbox/{sandboxId}/files/info?path=workspace/data/file.txt' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ```json { "name": "file.txt", "path": "/home/daytona/workspace/data/file.txt", "size": 1024, "isDir": false, "mode": "-rw-r--r--", "permissions": "0644", "owner": "1000", "group": "1000", "modifiedAt": "2026-09-13T09:41:12Z", "modTime": "2026-09-13 09:41:12 +0000 UTC" } ``` ## Create directories Create a directory and any missing parents, with octal mode 755 by default. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.fs.mkdir("workspace/new-dir") ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() await sandbox.fs.mkdir('workspace/new-dir') ``` ```bash curl 'https://toolbox.daytona.com/toolbox/{sandboxId}/files/folder?path=workspace/new-dir' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ## Upload files ### Upload a single file Upload a file to a sandbox. Missing parent directories at the destination are created, and an existing file at the destination is overwritten. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() with open("local_file.txt", "rb") as f: sandbox.fs.write_bytes("remote_file.txt", f.read()) ``` ```typescript import { readFile } from 'node:fs/promises' import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() await sandbox.fs.write('remote_file.txt', await readFile('local_file.txt')) ``` ```bash # The sandbox side is : daytona sandbox cp local_file.txt {sandboxId}:/home/daytona/remote_file.txt ``` - Exactly one of the two arguments is a sandbox path. - A sandbox destination that ends with `/` receives the file under its local name. ```bash curl 'https://toolbox.daytona.com/toolbox/{sandboxId}/files/upload?path=remote_file.txt' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' \ --form 'file=@local_file.txt' ``` - The file content goes in a multipart part named `file`. - If the request body is cut off, the partial file stays at the destination. ### Upload multiple files Upload several files in one request. Each file is written independently: missing parent directories are created, existing files are overwritten, and a file that fails does not stop the others. The result lists each file written with its byte count, and an error message for each failure. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() result = sandbox.fs.write_many({ "data/file1.txt": b"Content of file 1", "data/file2.txt": b"Content of file 2", }) # Partial success is reported in the result, not raised for error in result.errors: print(f"Error: {error}") ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() const result = await sandbox.fs.writeMany({ 'data/file1.txt': new TextEncoder().encode('Content of file 1'), 'data/file2.txt': new TextEncoder().encode('Content of file 2'), }) // Partial success is reported in the result, not thrown result.errors.forEach(error => console.error(`Error: ${error}`)) ``` ```bash curl 'https://toolbox.daytona.com/toolbox/{sandboxId}/files/bulk-upload' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' \ --form 'files[0].path=data/file1.txt' \ --form 'files[0].file=@file1.txt' \ --form 'files[1].path=data/file2.txt' \ --form 'files[1].file=@file2.txt' ``` ```json { "files": [ { "path": "data/file1.txt", "bytes": 17 }, { "path": "data/file2.txt", "bytes": 17 } ], "errors": [] } ``` - Each `files[i].path` part must come before its `files[i].file` part. - The call skips a file part without a preceding path part and reports the skipped part in `errors`. ### Upload large files Upload large files from local disk. The SDK streams the file in chunks without holding it in memory, and reports progress as it sends. The sandbox writes the upload to disk as it arrives with no request size limit, so free sandbox disk space bounds the upload size. The CLI streams the file from disk over a [presigned URL](#presigned-urls) it creates. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() def on_progress(sent: int, total: int | None) -> None: print(f"Uploaded {sent} of {total} bytes") sandbox.fs.upload_file("large_dataset.csv", "workspace/dataset.csv", on_progress=on_progress) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------ | | `local_path` | `str \| os.PathLike[str]` | Sets the local file to upload. This parameter is required. | | `remote_path` | `str` | Sets the destination path in the sandbox. This parameter is required. | | `on_progress` | `TransferProgressCallback` | Reports upload progress. The callback receives the bytes sent so far and the total file size in bytes. Exceptions raised inside the callback are ignored. | | `cancel` | `TransferCancelSignal` | Stops the upload when the event is set. The event is a `threading.Event` or an `asyncio.Event`. | | `atomic` | `bool` | Writes through a temporary file in the destination directory, then renames the file into place. The default is `False`. | ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() await sandbox.fs.uploadFile('large_dataset.csv', 'workspace/dataset.csv', { onProgress: (sent, total) => console.log(`Uploaded ${sent} of ${total} bytes`), }) ``` | **Parameter** | **Type** | **Description** | | -------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------ | | `localPath` | `string` | Sets the local file to upload. This parameter is required. | | `remotePath` | `string` | Sets the destination path in the sandbox. This parameter is required. | | `options.onProgress` | `TransferProgressCallback` | Reports upload progress. The callback receives the bytes sent so far and the total file size in bytes. Errors thrown inside the callback are ignored. | | `options.signal` | `AbortSignal` | Stops the upload when the signal is aborted. | | `options.atomic` | `boolean` | Writes through a temporary file in the destination directory, then renames the file into place. The default is `false`. | - `uploadFile` works in Node.js only, not in a browser. ```bash daytona sandbox cp large_dataset.csv {sandboxId}:/home/daytona/workspace/dataset.csv ``` ```bash curl 'https://toolbox.daytona.com/toolbox/{sandboxId}/files/upload?path=workspace/dataset.csv' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' \ --form 'file=@large_dataset.csv' ``` ## Download files ### Download a single file ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.fs.write_text("file1.txt", "Content of file 1") content = sandbox.fs.read_bytes("file1.txt") print(content.decode("utf-8")) ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() await sandbox.fs.writeText('file1.txt', 'Content of file 1') const content = await sandbox.fs.read('file1.txt') console.log(new TextDecoder().decode(content)) ``` ```bash daytona sandbox cp {sandboxId}:/home/daytona/file1.txt file1.txt ``` - A local destination that ends in `/` or names a directory keeps the sandbox file name. - The call creates missing local parent directories. ```bash curl 'https://toolbox.daytona.com/toolbox/{sandboxId}/files/download?path=file1.txt' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` #### Download a text file Download a text file. The SDK downloads the file and decodes its content as UTF-8. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.fs.write_text("file1.txt", "Content of file 1") content = sandbox.fs.read_text("file1.txt") print(content) ``` - The file must be valid UTF-8. - Use `read_bytes` for binary files. ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() await sandbox.fs.writeText('file1.txt', 'Content of file 1') const content = await sandbox.fs.readText('file1.txt') console.log(content) ``` - Invalid UTF-8 becomes `U+FFFD` without an error. - Use `read` for binary files. ### Download multiple files Download several files in one request. Each file is read independently, so a missing or unreadable file does not stop the others; it is reported as an error for its path. The SDK methods hold every downloaded file in memory before they return. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.fs.write_many({ "data/file1.txt": b"Content of file 1", "data/file2.txt": b"Content of file 2", }) result = sandbox.fs.read_many(["data/file1.txt", "data/file2.txt"]) for path, content in result.files.items(): print(f"Downloaded {path} ({len(content)} bytes)") # Partial success is reported in the result, not raised for error in result.errors: print(f"Error downloading {error.path}: {error.message}") ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() await sandbox.fs.writeMany({ 'data/file1.txt': new TextEncoder().encode('Content of file 1'), 'data/file2.txt': new TextEncoder().encode('Content of file 2'), }) const result = await sandbox.fs.readMany(['data/file1.txt', 'data/file2.txt']) Object.entries(result.files).forEach(([path, content]) => { console.log(`Downloaded ${path} (${content.byteLength} bytes)`) }) // Partial success is reported in the result, not thrown result.errors.forEach(error => console.error(`Error downloading ${error.path}: ${error.message}`)) ``` ```bash curl 'https://toolbox.daytona.com/toolbox/{sandboxId}/files/bulk-download' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "paths": [ "data/file1.txt", "data/file2.txt" ] }' ``` - The response is `multipart/form-data` with the fixed boundary `DAYTONA-FILE-BOUNDARY`, not JSON. - Each file is a part named `file` whose filename is the requested path. - A path that cannot be read becomes a part named `error`, with a JSON body that holds `code`, `message`, `source`, and `path`. - The response status stays `200` when a path cannot be read. ### Download large files Download large files to local disk. The SDK streams the file in chunks without holding it in memory, and reports progress as it receives. It writes to a temporary file next to the destination and renames it into place only after the whole file arrives, so a failed or cancelled download leaves an existing destination file unchanged. The CLI writes the file to disk as it arrives, over a [presigned URL](#presigned-urls) that it creates for the transfer. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.fs.write_text("workspace/large-file.bin", "hello") def on_progress(received: int, total: int | None) -> None: print(f"Downloaded {received} of {total} bytes") sandbox.fs.download_file("workspace/large-file.bin", "local_copy.bin", on_progress=on_progress) ``` | **Parameter** | **Type** | **Description** | | ------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------- | | `remote_path` | `str` | Sets the file to download from the sandbox. This parameter is required. | | `local_path` | `str \| os.PathLike[str]` | Sets the local destination. The parent directory of the destination must exist. A replaced destination file keeps its permission bits. This parameter is required. | | `on_progress` | `TransferProgressCallback` | Reports download progress. The callback receives the bytes received so far and the total size in bytes, or `None` when the total is unknown. | | `cancel` | `TransferCancelSignal` | Stops the download when the event is set. The event is a `threading.Event` or an `asyncio.Event`. | ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() await sandbox.fs.writeText('workspace/large-file.bin', 'hello') await sandbox.fs.downloadFile('workspace/large-file.bin', 'local_copy.bin', { onProgress: (received, total) => console.log(`Downloaded ${received} of ${total} bytes`), }) ``` | **Parameter** | **Type** | **Description** | | -------------------- | -------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | `remotePath` | `string` | Sets the file to download from the sandbox. This parameter is required. | | `localPath` | `string` | Sets the local destination. The parent directory of the destination must exist. A replaced destination file keeps its permission bits. This parameter is required. | | `options.onProgress` | `TransferProgressCallback` | Reports download progress. The callback receives the bytes received so far and the total size in bytes, or `undefined` when the total is unknown. | | `options.signal` | `AbortSignal` | Stops the download when the signal is aborted. | - `downloadFile` works in Node.js only, not in a browser. ```bash daytona sandbox cp {sandboxId}:/home/daytona/workspace/large-file.bin local_copy.bin ``` ```bash curl 'https://toolbox.daytona.com/toolbox/{sandboxId}/files/download?path=workspace/large-file.bin' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --output local_copy.bin ``` ## Delete files Delete a file or directory. A directory is deleted, with everything in it, only when the recursive option is set; otherwise it fails. A symbolic link is deleted as a link; its target is kept. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.fs.write_text("workspace/file.txt", "hello") sandbox.fs.delete("workspace/file.txt") ``` | **Parameter** | **Type** | **Description** | | ------------- | -------- | --------------------------------------------------------------------------------------------------------------- | | `recursive` | `bool` | Deletes a directory and its contents when `True`. Without this option, a directory path raises `BadRequestError`. The default is `False`. | ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() await sandbox.fs.writeText('workspace/file.txt', 'hello') await sandbox.fs.rm('workspace/file.txt') ``` | **Parameter** | **Type** | **Description** | | ------------- | --------- | ----------------------------------------------------------------------------------------------------------- | | `recursive` | `boolean` | Deletes a directory and its contents when `true`. Without this option, a directory path throws `BadRequestError`. | ```bash curl 'https://toolbox.daytona.com/toolbox/{sandboxId}/files?path=workspace/file.txt' \ --request DELETE \ --header 'Authorization: Bearer YOUR_API_KEY' ``` | **Field** | **Type** | **Description** | | ----------- | -------- | ------------------------------------------------------------------------------------------------- | | `recursive` | `string` | Deletes a directory and its contents when set to `true`. Without this field, a directory path returns `400`. | ## File permissions Change the permission mode, owner, or group of a file or directory; at least one is required. The mode is an octal string. Owner and group accept a numeric ID or a name, which the sandbox resolves from its user and group lists. Symbolic links are followed. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.fs.write_text("workspace/file.txt", "hello") sandbox.fs.chmod("workspace/file.txt", mode="644") ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() await sandbox.fs.writeText('workspace/file.txt', 'hello') await sandbox.fs.chmod('workspace/file.txt', { mode: '644' }) ``` ```bash curl 'https://toolbox.daytona.com/toolbox/{sandboxId}/files/permissions?path=workspace/file.txt&mode=644' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ## Search files by pattern Search for files by name. The search walks the directory tree without following symbolic links and matches the name of every entry against the pattern, so directories with a matching name are returned as well. Directories the sandbox cannot read are skipped without an error, and a path that does not exist returns an empty result. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.fs.write_text("workspace/main.py", "print('hello')") # Recursive; the pattern is matched against file names with * and ? wildcards result = sandbox.fs.search("workspace", "*.py") for file in result.items: print(file) ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() await sandbox.fs.writeText('workspace/main.py', "print('hello')") // Recursive; the pattern is matched against file names with * and ? wildcards const result = await sandbox.fs.search('workspace', '*.py') result.items.forEach(file => console.log(file)) ``` ```bash curl 'https://toolbox.daytona.com/toolbox/{sandboxId}/files/search?path=workspace&pattern=*.py' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` Returned paths are prefixed with the path you passed: ```json { "files": [ "workspace/main.py", "workspace/lib/util.py" ] } ``` ## Find and replace text in files Find text in files and replace it. Find checks each file line by line and returns the file, the line number starting at 1, and the full line for every match. Files that are not valid UTF-8 text are skipped, and a path that does not exist returns no matches. Replace takes a list of files, not directories, and rewrites each file in full with every occurrence replaced. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.fs.write_text("workspace/src/app.py", "text-of-interest") sandbox.fs.write_text("workspace/file1.txt", "old_text") sandbox.fs.write_text("workspace/file2.txt", "old_text") # Search for text in files; the pattern is a literal substring, not a regular expression. # If a directory is given, the search is recursive. results = sandbox.fs.find("workspace/src", "text-of-interest") for match in results: print(f"{match['file']}:{match['line']}: {match['content']}") # Replace every occurrence of a literal substring in each listed file result = sandbox.fs.replace(["workspace/file1.txt", "workspace/file2.txt"], "old_text", "new_text") # Per-file failures are reported in the result, not raised for item in result: if not item["success"]: print(f"Failed to update {item['file']}: {item['error']}") ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() await sandbox.fs.writeText('workspace/src/app.py', 'text-of-interest') await sandbox.fs.writeText('workspace/file1.txt', 'old_text') await sandbox.fs.writeText('workspace/file2.txt', 'old_text') // Search for text in files; the pattern is a literal substring, not a regular expression. // If a directory is given, the search is recursive. const results = await sandbox.fs.find('workspace/src', 'text-of-interest') results.items.forEach(match => console.log(`${match.file}:${match.line}: ${match.content}`)) // Replace every occurrence of a literal substring in each listed file const result = await sandbox.fs.replace(['workspace/file1.txt', 'workspace/file2.txt'], 'old_text', 'new_text') // Per-file failures are reported in the result, not thrown result.items .filter(item => !item.success) .forEach(item => console.error(`Failed to update ${item.file}: ${item.error}`)) ``` Find text in files: ```bash curl 'https://toolbox.daytona.com/toolbox/{sandboxId}/files/find?path=workspace/src&pattern=text-of-interest' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ```json [ { "file": "workspace/src/main.py", "line": 12, "content": "print('text-of-interest')" } ] ``` Replace text in files: ```bash curl 'https://toolbox.daytona.com/toolbox/{sandboxId}/files/replace' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "files": [ "workspace/file1.txt", "workspace/file2.txt" ], "newValue": "new_text", "pattern": "old_text" }' ``` ```json [ { "file": "workspace/file1.txt", "success": true }, { "file": "workspace/file2.txt", "success": false, "error": "No such file or directory (os error 2)" } ] ``` ## Move or rename directory or file Move or rename a file or directory. A move never replaces or merges into an existing destination entry. Across filesystems, the sandbox copies the entry, keeping each copied entry's owner and mode, and removes the source only after the whole copy succeeds. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.fs.write_text("workspace/data/file.txt", "hello") # Missing parent directories are created; an existing destination is rejected with ConflictError sandbox.fs.move("workspace/data/file.txt", "workspace/archive/file.txt") ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() await sandbox.fs.writeText('workspace/data/file.txt', 'hello') // Missing parent directories are created; an existing destination is rejected with ConflictError await sandbox.fs.move('workspace/data/file.txt', 'workspace/archive/file.txt') ``` ```bash curl 'https://toolbox.daytona.com/toolbox/{sandboxId}/files/move?source=workspace/data/file.txt&destination=workspace/archive/file.txt' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ## Watch for file changes Watch a directory for changes. The watch streams create, write, remove, rename, and permission change events as they happen; it does not replay earlier changes. By default, only entries directly inside the directory are watched. A recursive watch also covers subdirectories, including ones created later. Event paths are relative to the watched directory. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.fs.mkdir("workspace/src") handle = sandbox.fs.watch_dir( "/home/daytona/workspace/src", lambda event: print(f"{event.type.value}: {event.name}"), ) sandbox.fs.write_text("workspace/src/new.py", "print('hello')") handle.stop() ``` | **Parameter** | **Type** | **Description** | | ------------- | --------------------------------------- | --------------------------------------------------------------------------------------------------------- | | `recursive` | `bool` | Watches subdirectories when `True`. A recursive watch covers at most `4096` directories. A larger tree is refused when the watch starts. Directories created later beyond the limit are reported as an overflow. The default is `False`. | | `on_overflow` | `Callable[[], None \| Awaitable[None]]` | Runs when events are dropped. If events outpace delivery, the watch drops them and sends an overflow notice. `handle.overflow_count` counts the overflow notices. | - The path must be absolute and name an existing directory. - A sandbox runs at most `128` watches at the same time. - An exception raised in the callback does not end the watch. - `handle.last_callback_error` holds the latest exception from the callback. ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() await sandbox.fs.mkdir('workspace/src') const handle = await sandbox.fs.watchDir('/home/daytona/workspace/src', (event) => { console.log(`${event.type}: ${event.name}`) }) await sandbox.fs.writeText('workspace/src/new.py', "print('hello')") await handle.stop() ``` | **Parameter** | **Type** | **Description** | | ------------- | ------------ | --------------------------------------------------------------------------------- | | `recursive` | `boolean` | Watches subdirectories when you pass `{ recursive: true }` as the third argument. A recursive watch covers at most `4096` directories. A larger tree is refused when the watch starts. Directories created later beyond the limit are reported as an overflow. | | `onOverflow` | `() => void` | Runs when events are dropped. Pass this callback in the options. If events outpace delivery, the watch drops them and sends an overflow notice. | - The path must be absolute and name an existing directory. - A sandbox runs at most `128` watches at the same time. - An error thrown by the callback ends the watch. - `waitForExit()` resolves with the error thrown by the callback. ```bash curl --no-buffer 'https://toolbox.daytona.com/toolbox/{sandboxId}/files/watch?path=/home/daytona/workspace/src' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ```text event: start data: {} event: fs data: {"type":"create","name":"new.py","isDir":false,"timestamp":1789121730123} event: overflow data: {"dropped":true} ``` | **Field** | **Type** | **Description** | | ----------- | --------- | --------------------------------------- | | `recursive` | `boolean` | Watches subdirectories when set to `true`. A recursive watch covers at most `4096` directories. A larger tree is refused when the watch starts. Directories created later beyond the limit are reported as an overflow. | - The path must be absolute and name an existing directory. - A sandbox runs at most `128` watches at the same time. - If events outpace delivery, the watch drops them and sends an overflow notice. - The `timestamp` of an `fs` event is in Unix milliseconds. ### Poll for events Poll a watch for events instead of handling them in a callback. The handle queues every event, with or without a callback, and each poll returns the queued events and clears them. ```python import time from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.fs.mkdir("workspace/src") handle = sandbox.fs.watch_dir("/home/daytona/workspace/src") sandbox.fs.write_text("workspace/src/new.py", "print('hello')") # Events arrive asynchronously; get_new_events() returns only those received so far time.sleep(1) for event in handle.get_new_events(): print(f"{event.type.value}: {event.name} (dir={event.is_dir})") handle.stop() ``` ### Wait for the watch to end ```python import threading from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() sandbox.fs.mkdir("workspace/src") handle = sandbox.fs.watch_dir( "/home/daytona/workspace/src", lambda event: print(f"{event.type.value}: {event.name}"), ) # Nothing ends a healthy watch on its own: stop it from another thread, # or the stream ends when the sandbox stops threading.Timer(60, handle.stop).start() # Blocks until the watch is stopped or the stream ends error = handle.wait_for_exit() if error is not None: print(f"Watch ended: {error}") ``` ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() await sandbox.fs.mkdir('workspace/src') const handle = await sandbox.fs.watchDir('/home/daytona/workspace/src', (event) => { console.log(`${event.type}: ${event.name}`) }) // Nothing ends a healthy watch on its own: stop it from elsewhere, // or the stream ends when the sandbox stops setTimeout(() => handle.stop(), 60_000) // Resolves when the watch is stopped or the stream ends const error = await handle.waitForExit() if (error) console.error('Watch ended:', error.message) ``` ## Presigned URLs Create presigned URLs. A presigned URL downloads or uploads one sandbox file without an API key until it expires, so any HTTP client can transfer the file. Daytona signs each URL with the sandbox's signing key; rotating it invalidates all its outstanding URLs. ```python from daytona import Daytona daytona = Daytona() sandbox = daytona.v2.create() # Download link, valid for 1 hour (default) download_url = sandbox.fs.presigned_download_url("workspace/report.pdf") # Upload link, valid for 1 hour (default) upload_url = sandbox.fs.presigned_upload_url("workspace/incoming/data.bin") # Invalidate every outstanding presigned URL for this sandbox sandbox.rotate_signing_key() ``` | **Parameter** | **Type** | **Description** | | -------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------- | | `expires_in_seconds` | `int` | Sets the URL lifetime in seconds, from `1` to `604800`. A URL is valid for at most `7` days. A value outside that range raises `BadRequestError` before any request is sent. The default is `3600`. | - Creating a URL requires the `write:sandboxes` permission. - Windows sandboxes cannot have presigned URLs. - After a key rotation, the old URLs can keep working for up to `30` seconds. ```typescript import { Daytona } from '@daytona/sdk' const daytona = new Daytona() const sandbox = await daytona.v2.create() // Download link, valid for 1 hour (default) const downloadUrl = await sandbox.fs.presignedDownloadUrl('workspace/report.pdf') // Upload link, valid for 1 hour (default) const uploadUrl = await sandbox.fs.presignedUploadUrl('workspace/incoming/data.bin') // Invalidate every outstanding presigned URL for this sandbox await sandbox.rotateSigningKey() ``` | **Parameter** | **Type** | **Description** | | ------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `expiresInSeconds` | `number` | Sets the URL lifetime in seconds, as an integer from `1` to `604800`. A URL is valid for at most `7` days. A value outside that range throws `BadRequestError` before any request is sent. The default is `3600`. | - Creating a URL requires the `write:sandboxes` permission. - Windows sandboxes cannot have presigned URLs. - After a key rotation, the old URLs can keep working for up to `30` seconds. ```bash # Sign a download link: {"url": "...", "method": "GET", "path": "...", "expires_at": "...", "signature": "v1_..."} # Use "method": "POST" to sign an upload link curl 'https://api.daytona.com/sandboxes/{sandboxId}/files/sign' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "method": "GET", "path": "workspace/report.pdf", "expires_at": "2026-06-12T03:22:47Z" }' # Consume a download link (no Authorization header) curl --output report.pdf \ 'https://toolbox.daytona.com/toolbox/{sandboxId}/files/download?path=workspace%2Freport.pdf&expires=1781234567&signature=v1_...' # Consume an upload link curl --request POST \ --form 'file=@data.bin' \ 'https://toolbox.daytona.com/toolbox/{sandboxId}/files/upload?path=workspace%2Fincoming%2Fdata.bin&expires=1781234567&signature=v1_...' # Rotate the signing key to revoke all outstanding links curl 'https://api.daytona.com/sandboxes/{sandboxId}/signing-key/rotate' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY' ``` | **Field** | **Type** | **Description** | | ---------- | --------- | -------------------------------------------------------------------------------------------------------- | | `expires_at` | `string` | Sets the expiry as an RFC 3339 timestamp. The expiry must lie in the future and at most `7` days ahead. | | `method` | `string` | Sets the link type: `GET` for a download link and `POST` for an upload link. Any other value returns `400`. | - Creating a URL requires the `write:sandboxes` permission. - Windows sandboxes cannot have presigned URLs. - After a key rotation, the old URLs can keep working for up to `30` seconds.