Skip to content

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 ​

StorageDriver

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).

ts
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.

ts
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 ​

ImagePipeline


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&lt;string[]&gt;


put() ​

> put(path, content, options?): Promise&lt;void&gt;

Defined in: storage/src/index.ts:435

Parameters ​

path ​

string

content ​

string | Buffer&lt;ArrayBufferLike&gt;

options? ​

PutOptions

Returns ​

Promise&lt;void&gt;


putStream() ​

> putStream(path, source, options): Promise&lt;void&gt;

Defined in: storage/src/index.ts:471

Streams a body straight to the backend: the bytes never sit in this process as one buffer.

ts
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 ​

StreamSource

options ​

PutStreamInput

Returns ​

Promise&lt;void&gt;


stat() ​

> stat(path): Promise&lt;StorageStat&gt;

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&lt;StorageStat&gt;


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.

ts
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&lt;TemporaryUploadUrl&gt;

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.

ts
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 ​

TemporaryUploadUrlOptions

Returns ​

Promise&lt;TemporaryUploadUrl&gt;


temporaryUrl() ​

> temporaryUrl(path, expiresIn, options?): Promise&lt;string&gt;

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 ​

DurationInput

options? ​

TemporaryUrlOptions = {}

Returns ​

Promise&lt;string&gt;

Released under the MIT License.