# your own file store (app.BlobStore)

> `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.

<!-- id: config-blob-store · area: config · stability: stable · html: https://osysharp.com/reference/config/blob-store/ -->

## Summary        {#summary}

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

```osy syntax
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      {#signature}

```osy syntax
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    {#description}

### Which files it keeps   {#which-files}

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   {#calls}

- **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   {#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   {#changing}

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   {#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   {#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.

```osy syntax
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   {#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       {#examples}

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

```osy test app=blob-store-table
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       {#see-also}
- [declaring secrets (app.Secrets)](https://osysharp.com/reference/config/secrets/) — where a store's bucket credentials come from
- [exporting your app's logs (app.LogExport)](https://osysharp.com/reference/config/log-export/) — the same seam, for keeping the app's own log lines past its plan's retention
- [per-environment config (app.Config)](https://osysharp.com/reference/config/app-config/) — per-environment settings such as a bucket name
- [App configuration](https://osysharp.com/reference/config/index/) — the other things an app declares once
