DocsFilesystem

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.

FormatTypeScript returnsPython returns
text (default)stringstr
bytesUint8Arraybytearray
blobBlobnot available
streamReadableStreaman 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

TypeScriptPythonDefaultWhat it does
useruserthe template's userLinux user the write runs as. It owns the created files and resolves relative paths.
gzipgzipfalseCompress the upload. Needs a recent template; against an older one the upload silently falls back to uncompressed multipart/form-data.
useOctetStreamuse_octet_streamchosen for youUpload 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.
metadatametadatanoneKey-value pairs persisted on the file. See File metadata.
requestTimeoutMsrequest_timeout60000 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:

TypeScriptPythonWhat it is
namenameBase name of the entry.
pathpathAbsolute path.
typetypefile or dir. Python adds symlink.
sizesizeSize in bytes.
modemodeMode and permission bits as a number.
permissionspermissionsString form, such as rwxr-xr-x.
owner, groupowner, groupOwning user and group.
modifiedTimemodified_timeLast modification time.
symlinkTargetsymlink_targetTarget of the link, when the entry is a symlink.
metadatametadataCustom 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

TypeScriptPythonDefaultWhat it does
recursiverecursivefalseWatch subdirectories too.
includeEntryinclude_entryfalseAttach the affected entry's info to each event. Best effort: it is absent for events whose entry no longer exists, such as a remove.
timeoutMstimeout (async only)60000 ms / 60 sHow long the watch runs. 0 disables the limit.
onExiton_exit (async only)noneCalled once when the watch ends, with the error that ended it or nothing on a clean end.
allowNetworkMountsallow_network_mountsfalseAllow 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.