Stone.jsDocs
Paradigm

Extensions

Cloud File

Your domain talks to one agnostic storage contract, never to a cloud SDK. @stone-js/cloud-file supplies the cloud drivers, S3 (and every S3-compatible store: R2, MinIO, Spaces, OSS, COS), Google Cloud Storage and Azure Blob, and wires them into the container, so switching or mixing backends is configuration, not code.

#Install

The provider SDK is never bundled: install the module, then the SDK for the store you use.

terminalbash
npm i @stone-js/cloud-file

# then the SDK for your provider:
npm i @aws-sdk/client-s3 @aws-sdk/s3-request-presigner   # S3 / R2 / MinIO / Spaces / OSS / COS
npm i @google-cloud/storage                              # Google Cloud Storage
npm i @azure/storage-blob                                # Azure Blob

#Enable it

The principle

Code that depends on a concrete backend is welded to it. Depend on the capability, a file store, and the backend becomes a deployment choice.

In Stone.js

Every driver implements the same FileSystem contract from @stone-js/filesystem. The provider registers them lazily (the SDK is only constructed when a disk is first used) and injects the default disk as fileSystem.

app/Application.tsdeclarativeimperative
import { StoneApp } from '@stone-js/core'
import { CloudFile } from '@stone-js/cloud-file'

@CloudFile({ driver: 's3', bucket: 'uploads', region: 'eu-west-3' })
@StoneApp({ name: 'app' })
export class Application {}
import { defineConfig } from '@stone-js/core'

export const AppConfig = defineConfig({
  filesystem: {
    default: 's3',
    disks: [
      { name: 's3', driver: 's3', bucket: 'uploads', region: 'eu-west-3' },
      { name: 'r2', driver: 's3', bucket: 'assets', endpoint: 'https://<acct>.r2.cloudflarestorage.com' }
    ]
  }
})

#Inject the store

The default disk is injected as fileSystem; the multi-disk manager as storage.

app/UploadService.tsts
export class UploadService {
  constructor (private readonly fileSystem) {}          // the default disk

  async save (path, bytes) {
    await this.fileSystem.put(path, bytes)              // same API on local, S3, R2, ...
    return await this.fileSystem.url(path)
  }
}

#Signed URLs: upload straight to the cloud

Large files should not travel through your function. Issue a short-lived signed URL and let the client PUT the bytes straight to the bucket; your handler only ever sees metadata.

app/UploadController.tsts
import { EventHandler, Post } from '@stone-js/router'

@EventHandler('/uploads')
export class UploadController {
  constructor (private readonly fileSystem) {}

  @Post('/sign')
  async sign (event) {
    // hand the client a short-lived PUT target
    return await this.fileSystem.temporaryUploadUrl(`avatars/${event.get('id')}.png`, {
      contentType: 'image/png',
      expiresIn: 600
    })
  }
}

For a private download, mint a temporary read URL: fileSystem.temporaryUrl(path, { expiresIn: 60 }). Feature-detect with supportsSignedUrls(disk) from @stone-js/filesystembefore calling, since the local driver has no signed URLs.

#Drivers

driverTypeDescription
s3@aws-sdk/client-s3Amazon S3 + S3-compatible: Cloudflare R2, MinIO (forcePathStyle), DigitalOcean Spaces, Alibaba OSS, Tencent COS (set endpoint).
gcs@google-cloud/storageGoogle Cloud Storage. Signed v4 upload/download URLs.
azure@azure/storage-blobAzure Blob Storage. SAS upload/download URLs (needs accountName + accountKey).
localbuilt-inFrom @stone-js/filesystem; the default disk. No signed URLs.

Stone.js

Your app exists in every runtime. Until you run it.

An open-source project by Stone Foundation
Created by Mr. Stone (Evens Pierre)