Store files from your app
Let buyers keep large files, such as audio tracks or images, in your app with the ambient window.panoplyFiles API
Some apps need more than saved settings: a DJ app keeps its buyer's tracks, a photo app keeps their pictures. File storage gives each buyer their own space for large files inside your app, without you running a server. Small data such as settings and lists still belongs in window.panoplyStorage.
Who gets it
File storage is off by default. Panoply turns it on for an app at review and sets how much each buyer may store. If your app needs it, say so in your submission, with the amount per buyer and why. The figure is fixed at review, not chosen by you, because Panoply pays for the storage.
An app also needs its storage slot (the per-app database behind panoplyStorage), because the list of each buyer's files lives there. See the plan matrix on the pricing page.
The API
window.panoplyFiles is defined in every app, before your own scripts run. When file storage is off, every method rejects with the code files_not_enabled, so you can detect it without guarding a missing global.
list()→ the buyer's files:[{ id, name, type, size, createdAt }], newest first.usage()→{ usedBytes, reservedBytes, quotaBytes, warnBytes }.upload(file, { name?, onProgress?, signal? })→ the stored file, with freshusage.fileis aFileorBlob.onProgressgets{ loaded, total }, and anAbortSignalcancels.url(id)→ an address on your app for that file. Use it as thesrcof an<audio>,<video>or<img>. Seeking works, because the browser asks for byte ranges.get(id)→ the file as aResponse, so you can stream or read it.delete(id)→{ usage }.
// Upload with a progress bar
const track = await panoplyFiles.upload(fileFromInput, {
onProgress: ({ loaded, total }) => bar.value = loaded / total,
})
// Play it, seeking included
audio.src = panoplyFiles.url(track.id)
// Warn before the buyer runs out
const u = await panoplyFiles.usage()
if (u.warnBytes !== null && u.usedBytes >= u.warnBytes) showStorageWarning(u)
A failed call rejects with an error carrying a code, a status and, where useful, details:
| code | meaning |
|---|---|
files_quota_exceeded | The upload would go over the buyer's limit. details has usedBytes, quotaBytes and requestedBytes. |
files_not_enabled | File storage is off for this app. |
file_not_found | No such file for this buyer. |
file_name_invalid | The name is empty, over 255 characters, or has control characters. |
upload_expired | A large upload was not finished within a day. Start it again. |
Limits
| Limit | Value |
|---|---|
| Storage per buyer | Set by Panoply at review |
| Largest file | No practical limit: files over 95 MB are sent in 50 MB parts |
| File name | 1 to 255 characters |
Usage counts every finished file, plus uploads still in progress. An upload that is never finished stops counting after a day.
Scope and privacy
- Each buyer's files are their own. Another buyer of your app, or another app, cannot list, read or delete them. An agent that uses your app has its own files, separate from the person it works for.
- Files leave with the buyer. When a purchase is refunded and no other purchase of the app is left, or when the buyer deletes their account, their files and their saved
panoplyStoragedata in your app are deleted. If the app is taken down, buyers' files are kept, in case it comes back. - Downloads are served safely. Audio, video, common image types and PDFs open in the page. Everything else, HTML and SVG included, is offered as a download. A file a buyer uploads can never run as part of your app.
Under the hood: the routes
You normally only need the methods above. For tools generating an app, or for debugging, these are the routes on your app's own address that the client calls. All of them need the buyer's session and refuse requests from other sites (403, code cross_origin).
| Method and path | Does |
|---|---|
GET /__panoply/files | List: { files, usage } |
GET /__panoply/files/usage | Usage |
POST /__panoply/files | Upload up to 95 MB. The body is the file. Send Content-Type, Content-Length, and the name URI-encoded in X-Panoply-File-Name. |
POST /__panoply/files/uploads | Start a large upload: { name, type, size }, with the name as plain text (not URI-encoded, unlike the header above), returns { fileId, partSize, partCount } |
PUT /__panoply/files/uploads/<fileId>/parts/<n> | Send part n, exactly partSize bytes (the last part is the rest). Returns { partNumber, etag }. |
POST /__panoply/files/uploads/<fileId>/complete | { parts: [{ partNumber, etag }] } |
DELETE /__panoply/files/uploads/<fileId> | Cancel a large upload |
GET /__panoply/files/<id> | The file. Honours Range. Add ?download=1 to force a download. |
DELETE /__panoply/files/<id> | Delete it |