File System Operations
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 and download large files, or use 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
Section titled “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
Section titled “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.
from daytona import Daytona
daytona = Daytona()sandbox = daytona.v2.create()sandbox.fs.write_text("workspace/hello.txt", "hello")
# List files in a directoryfiles = 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}")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 directoryconst 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}`)})curl 'https://toolbox.daytona.com/toolbox/{sandboxId}/files?path=workspace' \ --header 'Authorization: Bearer YOUR_API_KEY'Get directory or file information
Section titled “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. |
from daytona import Daytona
daytona = Daytona()sandbox = daytona.v2.create()sandbox.fs.write_text("workspace/data/file.txt", "hello")
# Get file metadatainfo = sandbox.fs.stat("workspace/data/file.txt")print(f"Size: {info.size} bytes")print(f"Modified: {info.modified_at}")print(f"Mode: {info.mode}")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 metadataconst 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}`)curl 'https://toolbox.daytona.com/toolbox/{sandboxId}/files/info?path=workspace/data/file.txt' \ --header 'Authorization: Bearer YOUR_API_KEY'{ "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
Section titled “Create directories”Create a directory and any missing parents, with octal mode 755 by default.
from daytona import Daytona
daytona = Daytona()sandbox = daytona.v2.create()
sandbox.fs.mkdir("workspace/new-dir")import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()const sandbox = await daytona.v2.create()
await sandbox.fs.mkdir('workspace/new-dir')curl 'https://toolbox.daytona.com/toolbox/{sandboxId}/files/folder?path=workspace/new-dir' \ --request POST \ --header 'Authorization: Bearer YOUR_API_KEY'Upload files
Section titled “Upload files”Upload a single file
Section titled “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.
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())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'))# The sandbox side is <sandbox-id>:<absolute path>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.
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
Section titled “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.
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 raisedfor error in result.errors: print(f"Error: {error}")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 thrownresult.errors.forEach(error => console.error(`Error: ${error}`))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'{ "files": [ { "path": "data/file1.txt", "bytes": 17 }, { "path": "data/file2.txt", "bytes": 17 } ], "errors": []}- Each
files[i].pathpart must come before itsfiles[i].filepart. - The call skips a file part without a preceding path part and reports the skipped part in
errors.
Upload large files
Section titled “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 it creates.
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. |
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. |
uploadFileworks in Node.js only, not in a browser.
daytona sandbox cp large_dataset.csv {sandboxId}:/home/daytona/workspace/dataset.csvcurl '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
Section titled “Download files”Download a single file
Section titled “Download a single file”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"))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))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.
curl 'https://toolbox.daytona.com/toolbox/{sandboxId}/files/download?path=file1.txt' \ --header 'Authorization: Bearer YOUR_API_KEY'Download a text file
Section titled “Download a text file”Download a text file. The SDK downloads the file and decodes its content as UTF-8.
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_bytesfor binary files.
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+FFFDwithout an error. - Use
readfor binary files.
Download multiple files
Section titled “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.
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 raisedfor error in result.errors: print(f"Error downloading {error.path}: {error.message}")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 thrownresult.errors.forEach(error => console.error(`Error downloading ${error.path}: ${error.message}`))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-datawith the fixed boundaryDAYTONA-FILE-BOUNDARY, not JSON. - Each file is a part named
filewhose filename is the requested path. - A path that cannot be read becomes a part named
error, with a JSON body that holdscode,message,source, andpath. - The response status stays
200when a path cannot be read.
Download large files
Section titled “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 that it creates for the transfer.
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. |
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. |
downloadFileworks in Node.js only, not in a browser.
daytona sandbox cp {sandboxId}:/home/daytona/workspace/large-file.bin local_copy.bincurl 'https://toolbox.daytona.com/toolbox/{sandboxId}/files/download?path=workspace/large-file.bin' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --output local_copy.binDelete files
Section titled “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.
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. |
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. |
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
Section titled “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.
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")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' })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
Section titled “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.
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 ? wildcardsresult = sandbox.fs.search("workspace", "*.py")for file in result.items: print(file)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 ? wildcardsconst result = await sandbox.fs.search('workspace', '*.py')result.items.forEach(file => console.log(file))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:
{ "files": [ "workspace/main.py", "workspace/lib/util.py" ]}Find and replace text in files
Section titled “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.
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 fileresult = sandbox.fs.replace(["workspace/file1.txt", "workspace/file2.txt"], "old_text", "new_text")
# Per-file failures are reported in the result, not raisedfor item in result: if not item["success"]: print(f"Failed to update {item['file']}: {item['error']}")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 fileconst result = await sandbox.fs.replace(['workspace/file1.txt', 'workspace/file2.txt'], 'old_text', 'new_text')
// Per-file failures are reported in the result, not thrownresult.items .filter(item => !item.success) .forEach(item => console.error(`Failed to update ${item.file}: ${item.error}`))Find text in files:
curl 'https://toolbox.daytona.com/toolbox/{sandboxId}/files/find?path=workspace/src&pattern=text-of-interest' \ --header 'Authorization: Bearer YOUR_API_KEY'[ { "file": "workspace/src/main.py", "line": 12, "content": "print('text-of-interest')" }]Replace text in files:
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"}'[ { "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
Section titled “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.
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 ConflictErrorsandbox.fs.move("workspace/data/file.txt", "workspace/archive/file.txt")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 ConflictErrorawait sandbox.fs.move('workspace/data/file.txt', 'workspace/archive/file.txt')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
Section titled “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.
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
128watches at the same time. - An exception raised in the callback does not end the watch.
handle.last_callback_errorholds the latest exception from the callback.
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
128watches at the same time. - An error thrown by the callback ends the watch.
waitForExit()resolves with the error thrown by the callback.
curl --no-buffer 'https://toolbox.daytona.com/toolbox/{sandboxId}/files/watch?path=/home/daytona/workspace/src' \ --header 'Authorization: Bearer YOUR_API_KEY'event: startdata: {}
event: fsdata: {"type":"create","name":"new.py","isDir":false,"timestamp":1789121730123}
event: overflowdata: {"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
128watches at the same time. - If events outpace delivery, the watch drops them and sends an overflow notice.
- The
timestampof anfsevent is in Unix milliseconds.
Poll for events
Section titled “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.
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 fartime.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
Section titled “Wait for the watch to end”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 stopsthreading.Timer(60, handle.stop).start()
# Blocks until the watch is stopped or the stream endserror = handle.wait_for_exit()if error is not None: print(f"Watch ended: {error}")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 stopssetTimeout(() => handle.stop(), 60_000)
// Resolves when the watch is stopped or the stream endsconst error = await handle.waitForExit()if (error) console.error('Watch ended:', error.message)Presigned URLs
Section titled “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.
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 sandboxsandbox.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:sandboxespermission. - After a key rotation, the old URLs can keep working for up to
30seconds.
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 sandboxawait 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:sandboxespermission. - After a key rotation, the old URLs can keep working for up to
30seconds.
# Sign a download link: {"url": "...", "method": "GET", "path": "...", "expires_at": "...", "signature": "v1_..."}# Use "method": "POST" to sign an upload linkcurl '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 linkcurl --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 linkscurl '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:sandboxespermission. - After a key rotation, the old URLs can keep working for up to
30seconds.