# Filesystem

Every sandbox has a `files` module for reading, writing, listing and watching
its filesystem, plus two methods that hand you a plain HTTP URL for bulk
transfers.

Filesystem calls run over the sandbox's own connection, not the REST API. Use
the TypeScript or Python SDK. Every example reads `IMPELLO_API_KEY` from the
environment; get a key at
[dashboard.impello.ai/keys](https://dashboard.impello.ai/keys).

## Read a file

`read()` returns the file's contents as text by default.

<CodeTabs>
<Tab label="TypeScript">

```ts
import { Sandbox } from '@impello/sdk'

const sandbox = await Sandbox.create('base')

await sandbox.files.write('/home/user/hello.txt', 'hello\n')
const text = await sandbox.files.read('/home/user/hello.txt')
console.log(text) // "hello\n"

await sandbox.kill()
```

</Tab>
<Tab label="Python">

```python
from impello import Sandbox

sandbox = Sandbox.create("base")

sandbox.files.write("/home/user/hello.txt", "hello\n")
text = sandbox.files.read("/home/user/hello.txt")
print(text)  # "hello\n"

sandbox.kill()
```

</Tab>
</CodeTabs>

Pass a format to get something other than a string back.

<div className="docs-table-wrap">
<table>
  <thead>
    <tr><th>Format</th><th>TypeScript returns</th><th>Python returns</th></tr>
  </thead>
  <tbody>
    <tr><td>`text` (default)</td><td>`string`</td><td>`str`</td></tr>
    <tr><td>`bytes`</td><td>`Uint8Array`</td><td>`bytearray`</td></tr>
    <tr><td>`blob`</td><td>`Blob`</td><td>not available</td></tr>
    <tr><td>`stream`</td><td>`ReadableStream`</td><td>an iterator of `bytes` you can use as a context manager</td></tr>
  </tbody>
</table>
</div>

In TypeScript the format is an option, `read(path, { format: 'bytes' })`. In
Python it is the second argument, `read(path, format="bytes")`.

Use `stream` for a file too large to hold in memory. The stream holds a
connection until you finish it, so consume it to the end or release it:
`cancel()` the stream in TypeScript, `close()` the reader in Python.

<CodeTabs>
<Tab label="TypeScript">

```ts
import { Sandbox } from '@impello/sdk'

const sandbox = await Sandbox.create('base')

const stream = await sandbox.files.read('/var/log/dpkg.log', {
  format: 'stream',
  streamIdleTimeoutMs: 30_000,
})
const reader = stream.getReader()
while (true) {
  const { done, value } = await reader.read()
  if (done) break
  console.log(value.length)
}

await sandbox.kill()
```

</Tab>
<Tab label="Python">

```python
from impello import Sandbox

sandbox = Sandbox.create("base")

with sandbox.files.read("/var/log/dpkg.log", format="stream") as stream:
    for chunk in stream:
        print(len(chunk))

sandbox.kill()
```

</Tab>
</CodeTabs>

`streamIdleTimeoutMs` aborts a streamed read when no chunk arrives within the
window; it defaults to the request timeout and `0` disables it. The sync
Python client ignores `stream_idle_timeout` — it cannot interrupt a blocking
read, so a stalled stream is bounded by a transport-wide 60-second idle read
timeout instead. `AsyncSandbox.files.read` honours the parameter. Pass
`gzip: true` (`gzip=True`) to ask for a compressed response.

## Write a file

`write()` creates the file if it does not exist, overwrites it if it does, and
creates any missing parent directories. It returns the name, type and path of
what it wrote.

<CodeTabs>
<Tab label="TypeScript">

```ts
import { Sandbox } from '@impello/sdk'

const sandbox = await Sandbox.create('base')

const info = await sandbox.files.write('/home/user/data/report.csv', 'a,b\n1,2\n')
console.log(info.path) // "/home/user/data/report.csv"

await sandbox.kill()
```

</Tab>
<Tab label="Python">

```python
from impello import Sandbox

sandbox = Sandbox.create("base")

info = sandbox.files.write("/home/user/data/report.csv", "a,b\n1,2\n")
print(info.path)  # "/home/user/data/report.csv"

sandbox.kill()
```

</Tab>
</CodeTabs>

Data may be a string, bytes or a stream. TypeScript accepts `string`,
`ArrayBuffer`, `Blob` and `ReadableStream`; Python accepts `str`, `bytes` and
any file-like object. Python streams a file-like object in chunks by default;
set `use_octet_stream=False` and a text-mode object is read into memory
instead.

### Write several files at once

Each entry is a path and its data. String and byte entries go to the
sandbox in one multipart request. On a current template, if any entry is a
stream (or you pass `gzip`), each file is uploaded in its own request;
against an older one every entry goes in the single multipart request
instead.

<CodeTabs>
<Tab label="TypeScript">

```ts
import { Sandbox } from '@impello/sdk'

const sandbox = await Sandbox.create('base')

const written = await sandbox.files.writeFiles([
  { path: '/home/user/app/index.js', data: 'console.log(1)\n' },
  { path: '/home/user/app/package.json', data: '{"name":"app"}\n' },
])
console.log(written.length) // 2

await sandbox.kill()
```

</Tab>
<Tab label="Python">

```python
from impello import Sandbox

sandbox = Sandbox.create("base")

written = sandbox.files.write_files([
    {"path": "/home/user/app/index.js", "data": "console.log(1)\n"},
    {"path": "/home/user/app/package.json", "data": '{"name":"app"}\n'},
])
print(len(written))  # 2

sandbox.kill()
```

</Tab>
</CodeTabs>

In TypeScript `write()` also takes the same array and returns an array;
`writeFiles()` is the explicit name for it.

### Write options

<div className="docs-table-wrap">
<table>
  <thead>
    <tr><th>TypeScript</th><th>Python</th><th>Default</th><th>What it does</th></tr>
  </thead>
  <tbody>
    <tr><td>`user`</td><td>`user`</td><td>the template's user</td><td>Linux user the write runs as. It owns the created files and resolves relative paths.</td></tr>
    <tr><td>`gzip`</td><td>`gzip`</td><td>`false`</td><td>Compress the upload. Needs a recent template; against an older one the upload silently falls back to uncompressed `multipart/form-data`.</td></tr>
    <tr><td>`useOctetStream`</td><td>`use_octet_stream`</td><td>chosen for you</td><td>Upload as `application/octet-stream` instead of `multipart/form-data`. Defaults to octet-stream when an entry is a stream outside the browser, so streamed uploads are not buffered; browsers always use `multipart/form-data` and buffer. Needs a recent template; against an older one it silently falls back to `multipart/form-data`.</td></tr>
    <tr><td>`metadata`</td><td>`metadata`</td><td>none</td><td>Key-value pairs persisted on the file. See [File metadata](#file-metadata).</td></tr>
    <tr><td>`requestTimeoutMs`</td><td>`request_timeout`</td><td>60000 ms / 60 s (buffered uploads only)</td><td>How long the request may take. A streamed upload has no client-side deadline; in TypeScript bound it with `signal` instead.</td></tr>
  </tbody>
</table>
</div>

## List, inspect and remove

`list()` returns one level of a directory. Pass `depth` to go deeper; a depth
below `1` raises `InvalidArgumentError` (`InvalidArgumentException` in
Python).

<CodeTabs>
<Tab label="TypeScript">

```ts
import { Sandbox } from '@impello/sdk'

const sandbox = await Sandbox.create('base')

const entries = await sandbox.files.list('/home/user', { depth: 2 })
for (const entry of entries) {
  console.log(entry.type, entry.path, entry.size)
}

console.log(await sandbox.files.exists('/home/user')) // true
console.log(await sandbox.files.makeDir('/home/user/out')) // true, false if it existed

const moved = await sandbox.files.rename('/home/user/out', '/home/user/output')
console.log(moved.path) // "/home/user/output"

await sandbox.files.remove('/home/user/output')
await sandbox.kill()
```

</Tab>
<Tab label="Python">

```python
from impello import Sandbox

sandbox = Sandbox.create("base")

entries = sandbox.files.list("/home/user", depth=2)
for entry in entries:
    print(entry.type, entry.path, entry.size)

print(sandbox.files.exists("/home/user"))  # True
print(sandbox.files.make_dir("/home/user/out"))  # True, False if it existed

moved = sandbox.files.rename("/home/user/out", "/home/user/output")
print(moved.path)  # "/home/user/output"

sandbox.files.remove("/home/user/output")
sandbox.kill()
```

</Tab>
</CodeTabs>

`makeDir()` creates every directory along the path and returns `false` when
the directory already exists rather than raising. `rename()` moves a file or
directory and returns the entry at its new path. `remove()` deletes a file or
a directory and returns nothing.

`getInfo()` (`get_info()`) returns the same entry shape for a single path.
Every entry carries:

<div className="docs-table-wrap">
<table>
  <thead>
    <tr><th>TypeScript</th><th>Python</th><th>What it is</th></tr>
  </thead>
  <tbody>
    <tr><td>`name`</td><td>`name`</td><td>Base name of the entry.</td></tr>
    <tr><td>`path`</td><td>`path`</td><td>Absolute path.</td></tr>
    <tr><td>`type`</td><td>`type`</td><td>`file` or `dir`. Python adds `symlink`.</td></tr>
    <tr><td>`size`</td><td>`size`</td><td>Size in bytes.</td></tr>
    <tr><td>`mode`</td><td>`mode`</td><td>Mode and permission bits as a number.</td></tr>
    <tr><td>`permissions`</td><td>`permissions`</td><td>String form, such as `rwxr-xr-x`.</td></tr>
    <tr><td>`owner`, `group`</td><td>`owner`, `group`</td><td>Owning user and group.</td></tr>
    <tr><td>`modifiedTime`</td><td>`modified_time`</td><td>Last modification time.</td></tr>
    <tr><td>`symlinkTarget`</td><td>`symlink_target`</td><td>Target of the link, when the entry is a symlink.</td></tr>
    <tr><td>`metadata`</td><td>`metadata`</td><td>Custom metadata, when any is set.</td></tr>
  </tbody>
</table>
</div>

The TypeScript SDK has no symlink type — `list()` omits symlink entries and
`getInfo()` returns `type: undefined` for one. Read `symlinkTarget` on the
entry `getInfo()` hands back to detect one, or use the Python SDK.

## File metadata

Pass `metadata` on a write to persist key-value pairs on the file as extended
attributes. `getInfo()`, `list()` and `rename()` read them back.

<CodeTabs>
<Tab label="TypeScript">

```ts
import { Sandbox } from '@impello/sdk'

const sandbox = await Sandbox.create('base')

await sandbox.files.write('/home/user/report.csv', 'a,b\n1,2\n', {
  metadata: { source: 'nightly-job' },
})

const info = await sandbox.files.getInfo('/home/user/report.csv')
console.log(info.metadata) // { source: "nightly-job" }

await sandbox.kill()
```

</Tab>
<Tab label="Python">

```python
from impello import Sandbox

sandbox = Sandbox.create("base")

sandbox.files.write(
    "/home/user/report.csv",
    "a,b\n1,2\n",
    metadata={"source": "nightly-job"},
)

info = sandbox.files.get_info("/home/user/report.csv")
print(info.metadata)  # {"source": "nightly-job"}

sandbox.kill()
```

</Tab>
</CodeTabs>

The sandbox lowercases keys, so they may come back in a different case than
you sent. Keys must be HTTP token characters and values printable US-ASCII;
anything else raises `InvalidArgumentError` (`InvalidArgumentException`)
before the request leaves the client. In a multi-file write the same metadata
is applied to every file. An older template throws `TemplateError`
(`TemplateException`); rebuild it, see
[Build a custom template](/docs/templates/build).

## Watch a directory

`watchDir()` reports filesystem events under a path. Events have a `name`
relative to the watched directory and a `type`: `chmod`, `create`, `remove`,
`rename` or `write`.

TypeScript takes a callback and returns a handle you stop when you are done.

```ts
import { Sandbox } from '@impello/sdk'

const sandbox = await Sandbox.create('base')

let seen!: () => void
const firstEvent = new Promise<void>((resolve) => {
  seen = resolve
})

const handle = await sandbox.files.watchDir(
  '/home/user',
  (event) => {
    console.log(event.type, event.name)
    seen()
  },
  { recursive: true, timeoutMs: 0, onExit: (err) => console.log('stopped', err) }
)

await sandbox.commands.run('touch /home/user/new.txt')

await firstEvent
await handle.stop()
await sandbox.kill()
```

`stop()` aborts the event stream without draining it, so wait for the events
you need before you stop the handle.

The sync Python client polls instead. `watch_dir()` returns a handle and
`get_new_events()` returns everything that happened since the last call.

```python
from impello import Sandbox

sandbox = Sandbox.create("base")

handle = sandbox.files.watch_dir("/home/user", recursive=True)
sandbox.commands.run("touch /home/user/new.txt")

for event in handle.get_new_events():
    print(event.type, event.name)

handle.stop()
sandbox.kill()
```

`AsyncSandbox.files.watch_dir()` is callback-based like TypeScript: pass
`on_event` and optionally `on_exit`, and `await handle.stop()` when you are
done.

### Watch options

<div className="docs-table-wrap">
<table>
  <thead>
    <tr><th>TypeScript</th><th>Python</th><th>Default</th><th>What it does</th></tr>
  </thead>
  <tbody>
    <tr><td>`recursive`</td><td>`recursive`</td><td>`false`</td><td>Watch subdirectories too.</td></tr>
    <tr><td>`includeEntry`</td><td>`include_entry`</td><td>`false`</td><td>Attach the affected entry's info to each event. Best effort: it is absent for events whose entry no longer exists, such as a remove.</td></tr>
    <tr><td>`timeoutMs`</td><td>`timeout` (async only)</td><td>60000 ms / 60 s</td><td>How long the watch runs. `0` disables the limit.</td></tr>
    <tr><td>`onExit`</td><td>`on_exit` (async only)</td><td>none</td><td>Called once when the watch ends, with the error that ended it or nothing on a clean end.</td></tr>
    <tr><td>`allowNetworkMounts`</td><td>`allow_network_mounts`</td><td>`false`</td><td>Allow watching a network mount, where events may be unreliable or never arrive.</td></tr>
  </tbody>
</table>
</div>

<Callout kind="note">
`includeEntry` and `allowNetworkMounts` need a recent template. Against an
older one they throw `TemplateError` (`TemplateException`), as does
`recursive`.
</Callout>

## Upload and download over HTTP

`uploadUrl()` and `downloadUrl()` return a plain URL for one path, so a
browser or a build step can move a file without the SDK. POST to the upload
URL as `multipart/form-data`; GET the download URL.

<CodeTabs>
<Tab label="TypeScript">

```ts
import { Sandbox } from '@impello/sdk'

const sandbox = await Sandbox.create('base')

const uploadUrl = await sandbox.uploadUrl('/home/user/report.csv')
const downloadUrl = await sandbox.downloadUrl('/home/user/report.csv', {
  useSignatureExpiration: 300,
})

console.log(uploadUrl, downloadUrl)
await sandbox.kill()
```

</Tab>
<Tab label="Python">

```python
from impello import Sandbox

sandbox = Sandbox.create("base")

upload_url = sandbox.upload_url("/home/user/report.csv")
download_url = sandbox.download_url(
    "/home/user/report.csv",
    use_signature_expiration=300,
)

print(upload_url, download_url)
sandbox.kill()
```

</Tab>
<Tab label="curl">

```bash
# $UPLOAD_URL and $DOWNLOAD_URL are the strings the SDK returned above.
curl -X POST "$UPLOAD_URL" -F "file=@./report.csv"
curl -o report.csv "$DOWNLOAD_URL"
```

</Tab>
</CodeTabs>

Sandboxes are created secured, so both URLs carry a signature. The signature
does not expire unless you ask for one: `useSignatureExpiration`
(`use_signature_expiration`) takes a number of seconds and adds an expiry to
the URL. On a sandbox created with `secure: false` there is no signature to
expire and passing the option raises `InvalidArgumentError`
(`InvalidArgumentException`).

<Callout kind="warning">
A signed URL grants whoever holds it read or write access to that one path
until it expires. Set an expiry before you hand one to a browser.
</Callout>

Both URLs address one file. To move a directory, archive it first with
[a command](/docs/sandbox/commands) and transfer the archive:

```bash
tar -czf /tmp/out.tar.gz -C /home/user/output .
```

Then download `/tmp/out.tar.gz`, or upload an archive and unpack it with
`tar -xzf` in the sandbox.

## Errors

A path that does not exist raises `FileNotFoundError`
(`FileNotFoundException` in Python). A write that fills the sandbox's disk
raises `NotEnoughSpaceError` (`NotEnoughSpaceException`); disk size is set by
your plan, see [Limits and pricing](/docs/limits-and-pricing). Every error
class is listed on [Errors](/docs/errors).

A sandbox's filesystem lives and dies with the sandbox unless you pause it;
see [Pause, resume and snapshots](/docs/sandbox/pause-and-snapshots).

Next: [Git](/docs/sandbox/git).
