ObjectStackObjectStack

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);  // 245760

download

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 false

getInfo

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';
}
OptionTypeDefaultDescription
contentTypestringMIME content type stored with the object
metadataRecord<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>;
}

On this page