Files
Each of the four storage scopes also keeps files: data too large for the JSON object, or not JSON at all — a student's essay as a PDF, a photo of their lab setup, an audio recording, a map the instructor uploaded for everyone. Who can read and write them follows the same rules as the scope's object.
// A student uploads their submission
const input = this.shadowRoot.querySelector('input[type=file]')
await this.api.storage.memberLocation.files.put('essay.pdf', input.files[0])
// Later, show what they uploaded
const files = await this.api.storage.memberLocation.files.list() // [{ key, size, modified }]
// Display an image the instructor stored for the course
const { url } = await this.api.storage.course.files.info('diagram.png')
img.src = url // valid for a few minutes
Each file has a key your tool chooses — 1 to 128 characters, letters, digits, ., _, and -, starting with a letter or digit, no /. A put to a key that already holds a file replaces it.
The methods
All are on api.storage.<scope>.files, all are async, and all throw on failure.
list()Promise<Array>Every file on the scope: [{ key, size, modified }], with size in bytes and modified in milliseconds since the epoch. Empty when there are none.
info(key)Promise<object>One file: { key, size, modified, content_type, url, expires }. url downloads the file until expires (milliseconds since the epoch, a few minutes away). Put it in an <img>, an <a>, or an <audio> right away; ask again for a fresh one rather than keeping it. Throws VoshiNotFoundError when no file has the key.
get(key)Promise<Blob>The file's contents, as a Blob — for a file your code reads, such as JSON or text your tool saved earlier. For something the browser displays, info().url is cheaper.
put(key, file, options?)Promise<void>Store file (a File from an input, or any Blob) at key, replacing what was there.
remove(key)Promise<void>Delete the file. Removing a key that holds nothing is not an error.
Limits
| Limit | |
|---|---|
| One file | 25 MB |
| Files on one scope | 100 files, 100 MB together |
A put that would exceed the scope's quota throws before anything is uploaded.
How it works, briefly
The bytes go straight between the browser and Voshi's file storage, not through the API, which is why info() hands you a short-lived URL instead of the file. Your tool does not need to know this — but it explains why a stored URL stops working after a few minutes, and why you should ask for a new one each time you render.
Saving something that is not a file
A Blob can be made from anything:
const blob = new Blob([JSON.stringify(bigDataset)], { type: 'application/json' })
await this.api.storage.memberLocation.files.put('dataset.json', blob)
const back = JSON.parse(await (await this.api.storage.memberLocation.files.get('dataset.json')).text())
Use this when a student's work outgrows the storage object's size limit.