Skip to content

Files

Store files such as images or documents next to your collections. Files are organized in directories (paths) and count toward the owner’s file storage.

const input = document.querySelector('input[type="file"]');
const file = await bzr.files.upload(input.files[0], {
path: "photos/",
onProgress: ({ loaded, total }) => console.log(loaded, total),
});

All options are optional:

  • path: directory, e.g. photos/2026/. Defaults to the root.
  • filename: defaults to the file’s name.
  • metadata: custom data, see below.
  • onProgress: called while uploading.

A file is at most 10 MB. A file looks as follows:

type BazaarFile = {
id: string;
filename: string;
contentType: string;
size: number; // bytes
path: string;
createdAt: string;
createdBy: string; // user ID
metadata?: Record<string, unknown>; // custom data
};
const blob = await bzr.files.download(file.id);
const url = URL.createObjectURL(blob);

Get only the metadata (null if the file doesn’t exist):

const meta = await bzr.files.getMetadata(file.id);

Downloads and uploads have rate limits per user and app: 1000 downloads and 5 GB downloaded per hour, 300 uploads and 300 replaces per hour, 10 MB per file. Past a limit the call fails with RequestLimit, and the server’s Retry-After header says when to try again.

<img>, <video> and links can’t send a login, so get a URL for them:

img.src = await bzr.files.getUrl(file.id);
  • expiresIn: seconds it’s good for, at least. Defaults to 1 hour, at most 24 hours: bzr.files.getUrl(file.id, { expiresIn: 300 }).
  • Calling it again soon gives the same URL, so the browser can reuse what it loaded.
  • It’s good for this file, this user and this app. Anyone who has it can use it until it expires, so don’t publish it. It stops working at once when the user loses access to the file, or removes your app.
  • <video> can seek. The 10 MB limit applies. Each part a browser fetches counts as a download towards the rate limit, about 10 for a video played and sought in; each request that goes unanswered from its cache counts too.
  • Opening the URL itself downloads the file, whatever its type, so uploaded HTML never runs on Bazaar’s domain.
  • Browsers keep what they loaded. After bzr.files.replace(), or once a user lost access, a new page can show the old file for up to a minute, and a page that stays open for as long as it reuses the URL in an <img>. To show a replaced file right away, add something to the URL: url + "&v=2".

Throws an error of type FileDoesNotExist for a missing file, NoPermission without access to it. For a file in another owner’s storage, use the files API of a context.

const { files, directories } = await bzr.files.list({ path: "photos/" });
  • path: defaults to the root.
  • depth: "one" (default) lists the path itself, "all" also its subdirectories.
  • filter: a filter on file fields, e.g. { contentType: { $startsWith: "image/" } }.
await bzr.files.replace(file.id, newBlob);
await bzr.files.delete(file.id);

Give a file custom data on upload, at most 4 KB of JSON. It comes with the file and permission filters can use it.

const file = await bzr.files.upload(blob, {
metadata: { caption: "Sunset", tiers: ["example-product-id"] },
});

Replace it as a whole later. A key you leave out is gone:

await bzr.files.updateMetadata(file.id, { caption: "Sunrise" });
const unsubscribe = await bzr.files.subscribePath(
{ path: "photos/" },
{
onAdd: (file) => {},
onChange: (oldFile, newFile) => {},
onDelete: (file) => {},
},
);

See the subscribe listener.

File permissions use the reserved collection name _files. Use a $startsWith filter on path to share a directory:

import { GranteeType, PermissionType } from "@bzr/bazaar";
await bzr.permissions.create({
collectionName: "_files",
granteeType: GranteeType.USER,
granteeId: "friend-user-id",
types: [PermissionType.READ],
filter: { path: { $startsWith: "photos/shared/" } },
});

Filters can use custom metadata too. For example, this lets buyers of a product download the files that name it, see Monetization:

await bzr.permissions.create({
collectionName: "_files",
granteeType: GranteeType.ANY,
types: [PermissionType.READ],
filter: { "metadata.tiers": { $containsAny: "$purchases" } },
});

An UPDATE grant with a filter also applies to what a write leaves: replacing a file with one whose contentType or size falls outside the filter is refused, and the old file stays. So is updateMetadata with metadata outside it. See Filters and Writes.

To use files in another owner’s storage, use the files API of a context.

Failed calls throw a BazaarError. A file the user may not access is ErrorTypes.NoPermission, a full storage ErrorTypes.NoSpace, a missing file ErrorTypes.FileDoesNotExist (getMetadata answers null instead).