IStorageService Contract
Reference for the Storage Service contract — file upload, download, deletion, metadata, and signed URL generation
IStorageService Contract
The Storage Service provides a unified interface for file management — uploading, downloading, and organizing files across different storage backends (local filesystem and S3-compatible object storage).
Source: packages/spec/src/contracts/storage-service.ts
Service name: 'storage' — resolve via kernel.getService('storage'). The
pre-rename spelling 'file-storage' stays accepted as a deprecated alias within v17
(#9683); both names resolve the same instance.
Interface Definition
export interface IStorageService {
// Core Operations
upload(key: string, data: Buffer | ReadableStream, options?: StorageUploadOptions): Promise<void>;
download(key: string): Promise<Buffer>;
delete(key: string): Promise<void>;
exists(key: string): Promise<boolean>;
getInfo(key: string): Promise<StorageFileInfo>;
// Signed URL (optional)
getSignedUrl?(key: string, expiresIn: number, options?: PresignedDownloadOptions): Promise<string>;
// Presigned browser-direct upload / download (optional)
getPresignedUpload?(
key: string,
expiresIn: number,
options?: StorageUploadOptions,
): Promise<PresignedUploadDescriptor>;
getPresignedDownload?(
key: string,
expiresIn: number,
options?: PresignedDownloadOptions,
): Promise<PresignedDownloadDescriptor>;
// Chunked / multipart upload (optional)
initiateChunkedUpload?(key: string, options?: StorageUploadOptions): Promise<string>;
uploadChunk?(uploadId: string, partNumber: number, data: Buffer): Promise<string>;
completeChunkedUpload?(
uploadId: string,
parts: Array<{ partNumber: number; eTag: string }>,
): Promise<string>;
abortChunkedUpload?(uploadId: string): Promise<void>;
}Only upload, download, delete, exists, and getInfo are required.
Signed/presigned URLs and chunked upload are optional — call them only after checking
the method exists on the active adapter (the local and S3 adapters implement them).
Core Operations
upload
Uploads a file to storage. The key is passed as the first argument and the
content as the second; the call resolves to void. Use getInfo() afterwards
to read back stored metadata.
await storageService.upload(
'attachments/tasks/tsk_01HQ4A7B/document.pdf',
fileBuffer,
{
contentType: 'application/pdf',
acl: 'private',
metadata: {
objectType: 'task',
recordId: 'tsk_01HQ4A7B9D3F5G8J2K4L',
uploadedBy: 'usr_01HQ3V5K8N2M4P6R7T9W',
},
},
);
const info = await storageService.getInfo(
'attachments/tasks/tsk_01HQ4A7B/document.pdf',
);
console.log(info.key); // "attachments/tasks/tsk_01HQ4A7B/document.pdf"
console.log(info.size); // 245760download
Downloads a file and returns its content as a Buffer. Use getInfo() for
content type and size.
const content = await storageService.download(
'attachments/tasks/tsk_01HQ4A7B/document.pdf'
);
console.log(content.length); // 245760 (Buffer)delete
Permanently removes a file from storage.
await storageService.delete('attachments/tasks/tsk_01HQ4A7B/document.pdf');exists
Checks if a file exists at the given path without downloading it.
const fileExists = await storageService.exists(
'attachments/tasks/tsk_01HQ4A7B/document.pdf'
);
// true or falsegetInfo
Retrieves file metadata without downloading the content.
const info = await storageService.getInfo(
'attachments/tasks/tsk_01HQ4A7B/document.pdf'
);
console.log(info.key); // "attachments/tasks/tsk_01HQ4A7B/document.pdf"
console.log(info.size); // 245760
console.log(info.contentType); // "application/pdf"
console.log(info.lastModified); // Date
console.log(info.metadata); // { objectType: 'task', ... }StorageUploadOptions
Configuration options for upload behavior.
export interface StorageUploadOptions {
/** MIME content type */
contentType?: string;
/** Custom metadata key-value pairs */
metadata?: Record<string, string>;
/** Access control level */
acl?: 'private' | 'public-read';
}| Option | Type | Default | Description |
|---|---|---|---|
contentType | string | — | MIME content type stored with the object |
metadata | Record<string, string> | — | Custom metadata key-value pairs |
acl | 'private' | 'public-read' | — | Access control level |
File Path Convention: Use the pattern {category}/{object}/{record_id}/{filename} for organized storage. Example: attachments/task/tsk_01HQ4A7B/report.pdf.
Listing Files
list(prefix) was removed in @objectstack/spec 5.x — the contract has no
prefix-enumeration method. It was declared but never consumed, and the two shipped
adapters answered it differently while both silently returned an incomplete answer:
the local adapter listed a single level and reported directories as files, the S3
adapter recursed and stopped at 1000 objects without reading IsTruncated /
ContinuationToken.
There is no drop-in replacement, deliberately — a prefix listing that cannot paginate
is the wrong shape to keep. If you were enumerating a prefix, track the keys you wrote
(the sys_file / file-reference records already do this, and are queryable through
ObjectQL with real pagination) instead of asking the bucket. When a first-party caller
genuinely needs bucket enumeration, it returns cursor-shaped —
list(prefix, { cursor, limit }) — with adapter-conformance cases proving both
backends agree.
URL Generation
getSignedUrl
Generates a temporary signed URL for private file access. Optional — verify the
active adapter implements it. Takes the key and an expiresIn value (seconds)
as positional arguments.
const url = await storageService.getSignedUrl?.(
'attachments/tasks/tsk_01HQ4A7B/document.pdf',
3600, // 1 hour
);
// "https://storage.example.com/...?signature=abc&expires=..."getPresignedUpload / getPresignedDownload
For browser-direct uploads, optional adapters expose presigned descriptors.
getPresignedUpload(key, expiresIn, options?) returns a PresignedUploadDescriptor
the client SDK posts the bytes to; getPresignedDownload(key, expiresIn) returns
a PresignedDownloadDescriptor.
const upload = await storageService.getPresignedUpload?.(
'attachments/tasks/tsk_01HQ4A7B/document.pdf',
3600,
{ contentType: 'application/pdf' },
);
// { uploadUrl, method: 'PUT', headers?, expiresIn, downloadUrl? }Descriptor Types
export interface PresignedUploadDescriptor {
uploadUrl: string;
method: 'PUT' | 'POST';
headers?: Record<string, string>;
expiresIn: number;
downloadUrl?: string;
}
export interface PresignedDownloadDescriptor {
downloadUrl: string;
expiresIn: number;
}Chunked Upload
S3-compatible adapters support multipart (chunked) uploads for large files. All
chunked methods are optional. ETags from uploadChunk must be collected and
passed to completeChunkedUpload in order.
const uploadId = await storageService.initiateChunkedUpload?.(
'attachments/tasks/tsk_01HQ4A7B/large-video.mp4',
{ contentType: 'video/mp4' },
);
const eTag1 = await storageService.uploadChunk?.(uploadId, 1, chunk1);
const eTag2 = await storageService.uploadChunk?.(uploadId, 2, chunk2);
const key = await storageService.completeChunkedUpload?.(uploadId, [
{ partNumber: 1, eTag: eTag1 },
{ partNumber: 2, eTag: eTag2 },
]);
// On failure, release uploaded parts:
// await storageService.abortChunkedUpload?.(uploadId);StorageFileInfo
The return type for file metadata (getInfo).
export interface StorageFileInfo {
key: string;
size: number;
contentType?: string;
lastModified: Date;
metadata?: Record<string, string>;
}