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 itNothing 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 — fromConfig.*andSecret.*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
PutcallsFile.Createwould 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 write | why |
|---|---|
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 class | there is nothing for the platform to construct |
| a generic class | the platform would have to choose its type argument — derive a closed class and register that |
| a class whose every constructor takes arguments | the platform has nothing to pass; add a parameterless constructor |
a class that does not implement IBlobStore, or leaves out one of its members | the 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#
- declaring secrets (app.Secrets) — where a store's bucket credentials come from
- exporting your app's logs (app.LogExport) — the same seam, for keeping the app's own log lines past its plan's retention
- per-environment config (app.Config) — per-environment settings such as a bucket name
- App configuration — the other things an app declares once