Skip to main content

Upload & Download

R2-based file storage with $0 egress cost.

How Storage Works

EdgeBase storage is built on Cloudflare R2. Files are organized into buckets — each bucket has its own access rules for read, write, and delete.

storage/
├── avatars/ ← bucket (public read, auth write)
│ ├── user-1.jpg
│ └── user-2.jpg
├── documents/ ← bucket (auth read/write, admin delete)
│ ├── report-q1.pdf
│ └── invoice-2024.pdf
└── uploads/ ← bucket (auth write, signed URL download)
└── large-file.zip

Buckets are declared in edgebase.config.ts:

storage: {
buckets: {
avatars: {
access: {
read: () => true, // Anyone can view
write: (auth) => auth !== null, // Must be logged in to upload
delete: (auth, file) => auth?.id === file.uploadedBy, // Only uploader can delete
},
},
},
}

Each file has a key (its path within the bucket, e.g. user-1.jpg) and auto-tracked metadata including size, content type, upload timestamp, and who uploaded it.


Upload

const bucket = client.storage.bucket('avatars');

await bucket.upload('user-1.jpg', file, {
contentType: 'image/jpeg',
customMetadata: { userId: 'user-1' },
onProgress: (progress) => console.log(`${progress.percent}%`),
});
Auto-detected Content Type

When contentType is omitted, the SDK auto-detects it from the file extension (e.g. .jpgimage/jpeg, .pdfapplication/pdf). For File objects, the browser-provided MIME type is used first. You only need to specify contentType explicitly when using an uncommon extension or when the auto-detected type is wrong.

Cancel Upload

All upload methods (including uploadString) return an UploadTask — a Promise<FileInfo> with a .cancel() method. Calling .cancel() immediately aborts the underlying HTTP request, and the promise rejects with an AbortError:

const task = bucket.upload('video.mp4', largeFile, {
onProgress: (p) => progressBar.style.width = `${p.percent}%`,
});

// Cancel from a button click
cancelButton.onclick = () => task.cancel();

try {
const result = await task;
} catch (err) {
if (err.name === 'AbortError') {
console.log('Upload cancelled');
}
}

Upload from String

Upload string data with format conversion:

// Raw text
await bucket.uploadString('readme.txt', 'Hello, world!', 'raw', {
contentType: 'text/plain',
});

// Base64
await bucket.uploadString('image.png', base64Data, 'base64', {
contentType: 'image/png',
});

// Base64 URL-safe
await bucket.uploadString('file.bin', urlSafeBase64, 'base64url');

// Data URL (content type auto-detected from header)
await bucket.uploadString('photo.jpg', 'data:image/jpeg;base64,/9j/4AAQ...', 'data_url');
FormatDescription
'raw'Plain text (default content type: text/plain)
'base64'Standard Base64 encoded binary
'base64url'URL-safe Base64 (- and _ instead of + and /)
'data_url'Data URL with MIME header (e.g. data:image/png;base64,...)

uploadString returns an UploadTask (same as upload()), so you can use .cancel() and onProgress with string uploads as well.

Upload Response

All upload methods return a FileInfo object:

interface FileInfo {
key: string; // e.g. 'user-1.jpg'
size: number; // File size in bytes
contentType: string; // MIME type
etag: string; // R2 ETag
uploadedAt: string; // ISO 8601 timestamp
uploadedBy: string | null; // Auth user ID (auto-set)
customMetadata: Record<string, string>;
}

Download

const bucket = client.storage.bucket('avatars');

// Get public URL (synchronous — no network call)
const url = bucket.getUrl('user-1.jpg');

// Download as Blob (default)
const blob = await bucket.download('user-1.jpg');

// Download as text
const text = await bucket.download('readme.txt', { as: 'text' });

// Download as ArrayBuffer
const buffer = await bucket.download('data.bin', { as: 'arraybuffer' });

// Download as ReadableStream
const stream = await bucket.download('large.zip', { as: 'stream' });

Download Formats (JavaScript)

FormatReturn TypeUse Case
'blob' (default)BlobImages, files for <img> or URL.createObjectURL()
'text'stringText files, JSON, config files
'arraybuffer'ArrayBufferBinary processing, crypto operations
'stream'ReadableStreamLarge files, progressive processing
info

getUrl() is synchronous — it builds the URL locally without a network call. Use createSignedUrl() if the bucket requires authentication for reads.

Check File Exists

const bucket = client.storage.bucket('avatars');

const exists = await bucket.exists('user-1.jpg');
if (!exists) {
// Upload default avatar
}

Delete

const bucket = client.storage.bucket('avatars');

// Single file
await bucket.delete('old-avatar.jpg');

// Multiple files
const result = await bucket.deleteMany([
'old-avatar-1.jpg',
'old-avatar-2.jpg',
'old-avatar-3.jpg',
]);
// result.deleted: ['old-avatar-1.jpg', 'old-avatar-3.jpg']
// result.failed: [{ key: 'old-avatar-2.jpg', error: 'File not found.' }]

List Files

const bucket = client.storage.bucket('avatars');

const result = await bucket.list({
prefix: 'users/',
limit: 50,
});
// result.files: FileInfo[]
// result.cursor: string | null
// result.truncated: boolean

Pagination

Use cursor to load the next page:

let cursor: string | null = null;
const allFiles: FileInfo[] = [];

do {
const result = await bucket.list({
prefix: 'photos/',
limit: 100,
cursor: cursor ?? undefined,
});
allFiles.push(...result.files);
cursor = result.cursor;
} while (cursor);
info

Maximum limit is 1000 per request. Default is 100.

Bucket Security

// edgebase.config.ts
storage: {
buckets: {
avatars: {
access: {
read() { return true },
write(auth, file) {
return auth !== null &&
file.size <= 5 * 1024 * 1024 &&
['image/jpeg', 'image/png', 'image/webp'].includes(file.contentType);
},
delete(auth, file) { return auth !== null && auth.id === file.uploadedBy },
},
},
},
}

file Object Properties

The file parameter in access rules has different properties depending on the action:

write rule — receives WriteFileMeta (from form data, before upload):

PropertyTypeDescription
sizenumberFile size in bytes
contentTypestringMIME type
keystringRequested file path

read / delete rules — receive R2FileMeta (from stored file):

PropertyTypeDescription
sizenumberFile size in bytes
contentTypestringMIME type
keystringFile path
uploadedBystring?ID of the user who uploaded the file
customMetadataRecord<string, string>?Custom key-value metadata
etagstring?R2 ETag
uploadedAtstring?ISO 8601 upload timestamp