Documentation ziprack/1SDK 0.3.0

Capabilities

Storage

storage@1capability contract

On this page

Introduction

storage@1 lets a participant put a file into the Rack: a photo, a drawing, a recorded reflection or a document. Your applet never sees a bucket or a key, only signed URLs to its own files, and a file has no URL at all until it has been scanned clean.

Declaring Upload Slots

An upload is a named slot with a scope and a MIME allowlist. Files always belong to a slot. You may declare your slots in the manifest's uploads array:

JSON
"uploads": [
    { "name": "artifacts",  "scope": "room",    "mime": ["image/png", "image/jpeg"], "max_bytes": 5242880,  "max_files": 10 },
    { "name": "reflection", "scope": "attempt", "mime": ["audio/*"],                 "max_bytes": 10485760 }
]

Each slot accepts the following fields:

Field Rules
name Required. ^[a-z][a-z0-9_]*$, max 40, unique.
scope Required. attempt, user, room or applet. Same meaning as db scopes, with one difference: an applet slot still belongs to the version, so its files do not carry over to the next one.
mime Required, at least one. type/subtype or type/*.
max_bytes Optional. Default 10 MB, ceiling 50 MB.
max_files Optional. Default 20, ceiling 500.

The attempt scope requires attempt@1, and the room scope requires broadcast@1.

Uploading

To upload a file, you may pass the slot name and a File to the put method. The optional third argument carries metadata, such as a caption:

TypeScript
input.addEventListener('change', async () => {
    const file = input.files?.[0];
    if (! file) return;

    const result = await Rack.storage.put('artifacts', file, { caption: caption.value });

    if (! result.ok) {
        return showError(result.error === 'quota_exceeded'
            ? 'That slot is full.'
            : 'We could not accept that file.');
    }

    render(result.data.file);   // status is usually 'pending' at this point
});

Pass the File itself. The SDK hands it to the Rack, so you never encode it as JSON or upload it with your own fetch. The ack timeout for storage:put is 120 seconds rather than the usual 5, because upload time is participant time.

A stored file looks like this:

TypeScript
interface StoredFile {
    id: string;
    upload: string;                                       // the slot name
    name: string;
    mime: string;
    bytes: number;
    status: 'pending' | 'clean' | 'infected' | 'failed';
    created_at: string;
}

Reading Files Back

You may list a slot's files with the list method, fetch a URL for one with the url method, and remove one with the delete method:

TypeScript
const { data } = await Rack.storage.list('artifacts');    // omit the slot for all of them

const url = await Rack.storage.url(file.id);
if (url.ok) img.src = url.data.url;                        // expires; refetch rather than cache

await Rack.storage.delete(file.id);

url() returns a short-lived signed URL, 10 minutes by default, and never an object key. Don't store it. Request it when you are about to render, and request it again if the participant leaves the page open.

Pending Files

url() fails for a file that is not yet clean. Render pending files as placeholders and refresh until they are ready:

TypeScript
if (file.status === 'pending') {
    setTimeout(async () => {
        const again = await Rack.storage.list('artifacts');
        // …look for this id, re-render when it turns clean
    }, 2000);
}

Scanning

The Rack scans every upload. Until a file is marked clean, url() won't return a link for it, so render it as pending.

An infected file never gets a URL, and the participant sees a failure rather than a mystery.

Quotas and Limits

The Rack enforces these limits on the server, per slot and per scope, regardless of what the manifest asks for:

  • max_bytes per file, capped at 50 MB.
  • max_files per scope, capped at 500.
  • The MIME allowlist. A file whose type is not in the slot's list is refused with validation.

The server-side MIME check is not a substitute for setting accept on your <input>. Do both, for the participant's sake.

Uploads count against the applet, so declare the smallest slot that does the job.