basalt / storage/src / Disk
Class: Disk
Defined in: storage/src/index.ts:373
A named disk: driver + tenant scoping. All app code talks to this API.
Constructors
Constructor
> new Disk(name, driver, options?, tenancyActive?): Disk
Defined in: storage/src/index.ts:380
Parameters
name
string
driver
options?
DiskOptions = {}
tenancyActive?
() => boolean
Whether the host app registered @basaltkit/tenancy. storagePlugin wires this to the container's 'tenancy:active' metadata marker. It is read on every operation, not once at construction, so the fail-closed default does not depend on plugin order (a disk resolved before tenancy registers). Omitted (a hand-built disk), whether tenancy exists is unknown, and a scoped disk fails closed without a tenant.
Returns
Disk
Properties
name
> readonly name: string
Defined in: storage/src/index.ts:381
Methods
copy()
> copy(from, to, options?): Promise<void>
Defined in: storage/src/index.ts:535
Copies an object without the bytes passing through this process, when the driver can (S3 CopyObject, Azure copy-from-URL, GCS file.copy, local fs.copyFile).
await disk.copy('drafts/a.pdf', 'final/a.pdf')
await disk.copy('drafts/a.pdf', 'archive/a.pdf', { disk: storage.disk('cold') })Both keys are validated and tenant-prefixed — the destination against the destination disk's own scope, so a copy can never write outside the tenant it runs in. A central destination (scope: null) has no scope to hold the key, so from a tenant-scoped disk a destination inside tenants/ is refused with StorageCrossTenantCopyError: it would name some tenant's tree. Anywhere else on a central disk (backups/…) is allowed.
Fallbacks, in order: a different driver (or one with no copy) is copied with getStream → putStream, and a driver without those streams with get → put, which does buffer the object. Both fallbacks move the bytes through this process — for cross-cloud copies of large objects, prefer the provider's own transfer service, or pass { requireServerSide: true } to make a fallback an error (CopyUnsupportedError) instead.
Parameters
from
string
to
string
options?
CopyOptions = {}
Returns
Promise<void>
delete()
> delete(path): Promise<boolean>
Defined in: storage/src/index.ts:597
Parameters
path
string
Returns
Promise<boolean>
exists()
> exists(path): Promise<boolean>
Defined in: storage/src/index.ts:593
Parameters
path
string
Returns
Promise<boolean>
get()
> get(path): Promise<Buffer<ArrayBufferLike>>
Defined in: storage/src/index.ts:489
Parameters
path
string
Returns
Promise<Buffer<ArrayBufferLike>>
getStream()
> getStream(path): Promise<Readable>
Defined in: storage/src/index.ts:506
Reads an object as a stream. The caller MUST consume the returned readable to completion or destroy() it — an abandoned stream holds a socket (S3, Azure, GCS) or a file descriptor (local) open.
const body = await disk.getStream('reports/2026.csv')
await pipeline(body, createWriteStream('/tmp/2026.csv'))Drivers without the capability throw GetStreamUnsupportedError; a missing object throws StorageFileNotFoundError, as get does.
Parameters
path
string
Returns
Promise<Readable>
image()
> image(path): ImagePipeline
Defined in: storage/src/index.ts:425
Opens a fluent image pipeline reading path from this disk: disk.image('a.png').resize(256, 256).webp().save('a.webp'). Requires an imageProcessor (from @basaltkit/image-sharp); otherwise the terminal throws ImageProcessingUnavailableError.
Parameters
path
string
Returns
list()
> list(prefix?): Promise<string[]>
Defined in: storage/src/index.ts:611
Keys under prefix, recursive and sorted — RELATIVE to this disk's scope, exactly as put/get/delete take them: a key from list() goes back into get() as-is. (Before 5.0 a tenant-scoped disk returned tenants/<id>/…, which get() then prefixed a second time.)
The prefix is a directory on every driver: list('a') is list('a/'), never ab/… — object stores match prefixes as plain strings, so without this list('tenants/acme') on a central S3 disk also listed acme2.
Parameters
prefix?
string = ''
Returns
Promise<string[]>
put()
> put(path, content, options?): Promise<void>
Defined in: storage/src/index.ts:435
Parameters
path
string
content
string | Buffer<ArrayBufferLike>
options?
Returns
Promise<void>
putStream()
> putStream(path, source, options): Promise<void>
Defined in: storage/src/index.ts:471
Streams a body straight to the backend: the bytes never sit in this process as one buffer.
await disk.putStream('imports/2026.csv', request.raw, {
contentType: 'text/csv',
maxBytes: 200 * 1024 * 1024,
})Same safety rules as put: the key is validated and tenant-prefixed (fail-closed without a tenant), allowedContentTypes is checked before any byte is read, and maxBytes is enforced WHILE the body arrives — past the cap the upload is aborted with StorageTooLargeError and the source is destroyed (Node Readable) or cancelled (web ReadableStream). A partial object may remain on backends that cannot roll back a half-written upload; delete the key when that matters.
Pass contentLength whenever it is known — S3 streams it straight into one PutObject (see the @basaltkit/storage-s3 README). It is a promise about the body, and it is held to it: a value that is not a non-negative safe integer is refused before any byte is read (StorageContentLengthInvalidError), and a body that carries more or fewer bytes fails with StorageContentLengthMismatchError — before its end reaches the driver, so a backend that commits on end (S3, GCS, Azure) stores nothing and the local driver removes its partial file.
Drivers without the capability throw PutStreamUnsupportedError.
Parameters
path
string
source
options
Returns
Promise<void>
stat()
> stat(path): Promise<StorageStat>
Defined in: storage/src/index.ts:588
Object metadata — size, content type, etag, last-modified — without downloading it (S3 HeadObject, Azure getProperties, GCS getMetadata).
Drivers without the capability throw StatUnsupportedError; a missing object throws StorageFileNotFoundError, as get does.
Parameters
path
string
Returns
Promise<StorageStat>
supports()
> supports(capability): boolean
Defined in: storage/src/index.ts:415
Whether this disk's driver implements an optional capability, so callers can take the streaming path where it exists and buffer where it does not instead of catching a STORAGE_*_UNSUPPORTED error.
if (disk.supports('putStream')) await disk.putStream(key, body, { contentType })
else await disk.put(key, await buffer(body), { contentType })Parameters
capability
"temporaryUploadUrl" | "putStream" | "copy" | "temporaryUrl" | "getStream" | "stat"
Returns
boolean
temporaryUploadUrl()
> temporaryUploadUrl(path, options): Promise<TemporaryUploadUrl>
Defined in: storage/src/index.ts:660
Pre-signed direct upload: the browser PUTs the file straight to the bucket, never through the app server.
const upload = await disk.temporaryUploadUrl(`avatars/${randomUUID()}.png`, {
expiresIn: '5m', contentType: 'image/png', contentLength: 48_213,
})
// client: fetch(upload.url, { method: upload.method, headers: upload.headers, body: file })Same safety rules as temporaryUrl: the key is validated and tenant-prefixed (fail-closed without a tenant), and the lifetime is capped by maxTemporaryUploadUrlTtl (default 1 hour). contentType is required and signed; contentLength/checksumSha256 are signed where the backend can. Generate the key server-side — never accept it from the client.
Parameters
path
string
options
Returns
Promise<TemporaryUploadUrl>
temporaryUrl()
> temporaryUrl(path, expiresIn, options?): Promise<string>
Defined in: storage/src/index.ts:630
Pre-signed URL: disk.temporaryUrl('report.pdf', '15m').
Served as Content-Disposition: attachment by default (fail-closed against uploaded HTML/SVG rendering on the storage origin); pass { disposition: 'inline' } when top-level rendering is deliberate. Embedded uses (<img> etc.) render regardless of disposition.
Parameters
path
string
expiresIn
options?
TemporaryUrlOptions = {}
Returns
Promise<string>