Osy#betaRun it on Osyrin: osyrin.com

Reference / Config

your own file store (app.BlobStore)

app.BlobStore = TableStore;

`app.BlobStore` names a class of yours that keeps the app's file content — an S3 or R2 bucket, a table, anything you can reach from Osy# — instead of the platform's own store. The class implements `IBlobStore`; the platform constructs it and calls it whenever a file is written, read, replaced or deleted.

stable1 example compiled by CIconfigstoragefilesproviders

Summary#

Name a class that implements IBlobStore, and the app's file content is kept wherever that class puts it.

using Osysharp.Storage;

class TableStore : IBlobStore {
  public void Put(string key, byte[] content, string contentType) { … }
  public byte[]? Get(string key) { … }
  public bool Exists(string key) { … }
  public void Delete(string key) { … }
}

app.BlobStore = TableStore;      // the CLASS — the platform constructs it

Nothing else in the app changes. File.Create, File.Supersede, reading a file and serving it to a browser all work exactly as before; only where the bytes live is different.

Signature#

public interface IBlobStore {
  void Put(string key, byte[] content, string contentType);
  byte[]? Get(string key);        // null when there is nothing under the key
  bool Exists(string key);
  void Delete(string key);
}

app.BlobStore = <a class implementing IBlobStore>;

Description#

Which files it keeps#

A small file — 256 KB or less — is kept with the file's own record, whether or not a store is registered. Everything larger goes to your store. So the store sees the files that are worth keeping somewhere else, and a thumbnail or a short text file does not cost a round trip to a bucket.

How the platform calls it#

  • It constructs the class for each call, the way new TableStore() would: field initializers first, then the parameterless constructor. Read what the store needs — a bucket name, a credential — from Config.* and Secret.* there. Do not keep state in fields from one call to the next; there is no next call on the same object.
  • Each member runs to completion. A store runs inside a file read or write, where there is nobody to show a dialog to and nothing to resume, so a member that tries to pause is refused.
  • Its writes are saved when the member returns. A store that keeps bytes in one of the app's own entities has them committed there and then, the same as any external bucket would.
  • It is the storage, not a reader. Whether someone may see a file is decided on the file before its bytes are asked for, so the rows your store keeps are not checked again against that person.
  • It must not store its bytes as a platform file. A store whose Put calls File.Create would store through itself for ever; that is refused by name.

Keys#

The platform chooses every key and remembers it with the file. Store the content under exactly the key you are given and answer it back from Get — the key is opaque, and nothing ever has to work one out.

Changing the registration#

Every stored version of a file remembers which store holds it. A file written before you registered a store is still read from the platform's store; a file written through your store is read from your store, even after a later File.Supersede of another file went elsewhere.

If you remove the registration, files your store holds cannot be read until you register it again — reading one says so, rather than answering as if the file were empty. Their bytes are still wherever your store put them.

Deleting#

When a file is deleted, its content is deleted through the store that holds it — after the delete is saved, never before, so a delete that fails to save cannot lose content that is still referenced. If the store's Delete fails, the file is still gone and the failure is logged with the key that was left behind.

A store from a package#

Osysharp.S3 keeps files in an AWS S3 or Cloudflare R2 bucket. Grant it the hosts and credentials it asks for, derive a class that says where your bucket is, and register that — a class whose base implements IBlobStore is a store.

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

using Osysharp.S3;

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

class Files : S3Store {
  public Files() { Endpoint = "https://<account-id>.r2.cloudflarestorage.com"; Region = "auto"; Bucket = "shop-files"; }
}

app.BlobStore = Files;

Give the two secrets their values with osy secret set. Every request the store sends is signed with AWS Signature Version 4.

What is refused#

you writewhy
app.BlobStore = new TableStore();name the class; the platform has to be able to construct it again whenever it needs one
an interface, or an abstract classthere is nothing for the platform to construct
a generic classthe platform would have to choose its type argument — derive a closed class and register that
a class whose every constructor takes argumentsthe platform has nothing to pass; add a parameterless constructor
a class that does not implement IBlobStore, or leaves out one of its membersthe message names what is missing

Examples#

A store that keeps content in one of the app's own tables:

using Osysharp.Storage;

entity StoredBlob {
  [Unique, MaxLength(400)] string Key;
  byte[] Content;
  [MaxLength(200)] string ContentType;
}

class TableStore : IBlobStore {
  public void Put(string key, byte[] content, string contentType) {
    new StoredBlob { Key = key, Content = content, ContentType = contentType };
  }

  public byte[]? Get(string key) {
    var row = StoredBlob.FirstOrDefault(b => b.Key == key);
    return row == null ? null : row.Content;
  }

  public bool Exists(string key) => StoredBlob.Any(b => b.Key == key);

  public void Delete(string key) {
    var row = StoredBlob.FirstOrDefault(b => b.Key == key);
    if (row != null) row.Delete();
  }
}

app.BlobStore = TableStore;

See also#

Related

App configuration

The cross-cutting settings an app declares in one place — the app config object. Two kinds live here: the external…

declaring secrets (app.Secrets)

`app.Secrets` declares the named secrets your app uses — API keys, tokens, client secrets. Each is `new…

per-environment config (app.Config)

`app.Config` declares your app's per-environment settings — values that differ between development and production, like…

exporting your app's logs (app.LogExport)

`app.LogExport` names a class of yours implementing `IBlobStore` — an S3 or R2 bucket, a table, anything you can reach…