# 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](#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 <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.


```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.
- 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.
- 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.
- After a key rotation, the old URLs can keep working for up to **`30`** seconds.