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.
Read a file
read() returns the file's contents as text by default.
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()Pass a format to get something other than a string back.
| Format | TypeScript returns | Python returns |
|---|---|---|
text (default) | string | str |
bytes | Uint8Array | bytearray |
blob | Blob | not available |
stream | ReadableStream | an iterator of bytes you can use as a context manager |
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.
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()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.
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()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.
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()In TypeScript write() also takes the same array and returns an array;
writeFiles() is the explicit name for it.
Write options
| TypeScript | Python | Default | What it does |
|---|---|---|---|
user | user | the template's user | Linux user the write runs as. It owns the created files and resolves relative paths. |
gzip | gzip | false | Compress the upload. Needs a recent template; against an older one the upload silently falls back to uncompressed multipart/form-data. |
useOctetStream | use_octet_stream | chosen for you | 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. |
metadata | metadata | none | Key-value pairs persisted on the file. See File metadata. |
requestTimeoutMs | request_timeout | 60000 ms / 60 s (buffered uploads only) | How long the request may take. A streamed upload has no client-side deadline; in TypeScript bound it with signal instead. |
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).
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()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:
| TypeScript | Python | What it is |
|---|---|---|
name | name | Base name of the entry. |
path | path | Absolute path. |
type | type | file or dir. Python adds symlink. |
size | size | Size in bytes. |
mode | mode | Mode and permission bits as a number. |
permissions | permissions | String form, such as rwxr-xr-x. |
owner, group | owner, group | Owning user and group. |
modifiedTime | modified_time | Last modification time. |
symlinkTarget | symlink_target | Target of the link, when the entry is a symlink. |
metadata | metadata | Custom metadata, when any is set. |
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.
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()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.
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.
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.
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
| TypeScript | Python | Default | What it does |
|---|---|---|---|
recursive | recursive | false | Watch subdirectories too. |
includeEntry | include_entry | false | 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. |
timeoutMs | timeout (async only) | 60000 ms / 60 s | How long the watch runs. 0 disables the limit. |
onExit | on_exit (async only) | none | Called once when the watch ends, with the error that ended it or nothing on a clean end. |
allowNetworkMounts | allow_network_mounts | false | Allow watching a network mount, where events may be unreliable or never arrive. |
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.
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()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).
Both URLs address one file. To move a directory, archive it first with a command and transfer the archive:
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. Every error
class is listed on Errors.
A sandbox's filesystem lives and dies with the sandbox unless you pause it; see Pause, resume and snapshots.
Next: Git.