Osy#betaRun it on Osyrin: osyrin.com

Kits · Storage

S3 and R2

An app.BlobStore for S3-compatible object storage — AWS S3 and Cloudflare R2 — signed with SigV4.

Package
Osysharp.S3
Install
use Osysharp.S3@1 {
  egress "*.amazonaws.com";
  egress "*.r2.cloudflarestorage.com";
}
Group
Storage
Tests
—
Builds on
—

An app.BlobStore for S3-compatible object storage — AWS S3 and Cloudflare R2 — written in Osy# and signed with AWS Signature Version 4.

Use case

An app that keeps uploads — invoices, photos, contracts, exports — and should not hold large files in its own database. Register a bucket and every file too large to live in its row is written there, and read, replaced and deleted there. Choose it when the bytes must sit in your own cloud account for retention or compliance, when you already run storage in AWS or Cloudflare, or when you want R2's free egress for files people download often.

It has no user interface, so there is nothing to screenshot: your app's file features keep working exactly as they did, with the bytes somewhere else.

Install and use

osy

// app.osy
app Shop {
  model "**/*.osy";
  use Osysharp.S3 {
    egress "*.amazonaws.com";
    egress "*.r2.cloudflarestorage.com";
    secret "S3AccessKeyId";
    secret "S3SecretAccessKey";
  }
}

osy

using Osysharp.S3;

app.Secrets = [ new Secret("S3AccessKeyId"), new Secret("S3SecretAccessKey") ];

class Files : S3Store {
  public Files() { Endpoint = "https://s3.eu-north-1.amazonaws.com"; Region = "eu-north-1"; Bucket = "shop-files"; }
}

app.BlobStore = Files;

console

osy secret set S3AccessKeyId
osy secret set S3SecretAccessKey

That is the whole surface. Every file the app keeps that is too large to live in its own row goes to the bucket; reading it, replacing it and deleting it go there too. For Cloudflare R2 the endpoint is https://<account-id>.r2.cloudflarestorage.com and the region is auto.

What it ships

filewhat it is
model/S3Store.osyclass S3Store : IBlobStore — Put, Get, Exists, Delete as signed Http.* requests
model/SigV4.osySigV4Authorization(…) — the AWS Signature Version 4 Authorization header for one request

Requests use path-style addressing (<endpoint>/<bucket>/<key>), which both services accept for any bucket name. Exists asks for one byte rather than the whole object. A request the bucket refuses throws ExternalServiceException, so a caller can catch "the bucket said no" apart from a bug in its own code.

Build your own on top

A different bucket or account needs no change to the package: derive from S3Store once per bucket and register the one your app uses.

Another S3-compatible service — MinIO, Wasabi, Backblaze B2 — needs a fork, and for a reason worth knowing: a package can reach only the hosts its own manifest declares and your app grants, so this package cannot be pointed at a host it never asked for. Fork this repository into your GitHub account or organisation, rename the package to match (package Acme.S3 lives at github.com/acme/s3), add the host to its manifest, and osy publish:

osy

package Acme.S3 {
  version "1.0.0";
  model   "model/*.osy";
  use Osysharp.Http;
  use Osysharp.Storage;

  egress "s3.eu-central-1.wasabisys.com";
  secret "S3AccessKeyId";
  secret "S3SecretAccessKey";
}

A different kind of store is an IBlobStore of your own, and this package is the worked example of one: a class implementing the Storage package's interface, signing with Crypto.*, sending with Http.*, and declaring the hosts and secrets it needs. Nothing in it is privileged — a third party could have written it exactly as it is. The signer is checked against the signature AWS publishes for its own GET Object example, so a fork has a known-good vector to test against.

Get it

Add the kit to your app’s manifest:

osy

app MyApp {
  use Osysharp.S3@1 {
    egress "*.amazonaws.com";
    egress "*.r2.cloudflarestorage.com";
  }
}

Then run osy lock: it downloads the release, verifies it by its content address and pins that version in osyrin.lock. osy kits Osysharp.S3 shows what the kit is for, from inside your app.