Skip to content

File System Operations

View as Markdown

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.

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. 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 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}")

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.

FieldFormat
ModeSymbolic, such as -rw-r--r--; d first for a directory, L for a link.
PermissionsThe four-digit octal form, such as 0644.
Owner and groupNumeric 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 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}")

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")

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())

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 raised
for error in result.errors:
print(f"Error: {error}")

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)
ParameterTypeDescription
local_pathstr | os.PathLike[str]Sets the local file to upload. This parameter is required.
remote_pathstrSets the destination path in the sandbox. This parameter is required.
on_progressTransferProgressCallbackReports upload progress. The callback receives the bytes sent so far and the total file size in bytes. Exceptions raised inside the callback are ignored.
cancelTransferCancelSignalStops the upload when the event is set. The event is a threading.Event or an asyncio.Event.
atomicboolWrites through a temporary file in the destination directory, then renames the file into place. The default is False.
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"))

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_bytes for binary 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 raised
for error in result.errors:
print(f"Error downloading {error.path}: {error.message}")

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)
ParameterTypeDescription
remote_pathstrSets the file to download from the sandbox. This parameter is required.
local_pathstr | 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_progressTransferProgressCallbackReports download progress. The callback receives the bytes received so far and the total size in bytes, or None when the total is unknown.
cancelTransferCancelSignalStops the download when the event is set. The event is a threading.Event or an asyncio.Event.

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")
ParameterTypeDescription
recursiveboolDeletes a directory and its contents when True. Without this option, a directory path raises BadRequestError. The default is False.

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")

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 ? wildcards
result = sandbox.fs.search("workspace", "*.py")
for file in result.items:
print(file)

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 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']}")

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 ConflictError
sandbox.fs.move("workspace/data/file.txt", "workspace/archive/file.txt")

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()
ParameterTypeDescription
recursiveboolWatches 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_overflowCallable[[], 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.

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 far
time.sleep(1)
for event in handle.get_new_events():
print(f"{event.type.value}: {event.name} (dir={event.is_dir})")
handle.stop()
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}")

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 sandbox
sandbox.rotate_signing_key()
ParameterTypeDescription
expires_in_secondsintSets 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.
  • After a key rotation, the old URLs can keep working for up to 30 seconds.