Storage
Volcano Storage provides secure file storage with access control. Upload user avatars, documents, media files, and more with built-in security policies.
Volcano Storage provides secure file storage with access control. Upload user avatars, documents, media files, and more with built-in security policies.
Overview
The storage module offers:
- File Operations - Upload, download, list, delete, move, and copy files
- Access Control - Private by default, with per-file public/private settings
- Resumable Uploads - Handle large files reliably with chunked uploads
- Bucket Organization - Organize files into logical buckets
Basic Concepts
Buckets
Files are organized into buckets. A bucket is a top-level container, similar to a folder or an S3 bucket. You might have buckets like:
avatars- User profile picturesdocuments- PDF reports and filesuploads- General user uploads
Buckets are created in the Volcano dashboard and referenced by name in the SDK.
Paths
Within a bucket, files are identified by their path. Paths can include subdirectories:
avatars/user-123/profile.jpg
documents/reports/2024/q1-summary.pdf
uploads/images/photo-001.pngAccess Control
Files are private by default. Private files require authentication to download. You can make individual files public, allowing anyone to access them via a public URL.
Selecting a Bucket
All storage operations start by selecting a bucket:
const avatars = volcano.storage.from('avatars');
const documents = volcano.storage.from('documents');This returns a StorageFileApi object with methods for file operations.
Uploading Files
Basic Upload
Upload a file from an <input type="file"> element:
const fileInput = document.querySelector('input[type="file"]');
const file = fileInput.files[0];
const { data, error } = await volcano.storage.from('avatars').upload('user-123/profile.jpg', file);
if (error) {
console.error('Upload failed:', error.message);
return;
}
console.log('Uploaded:', data.name);
console.log('Size:', data.size, 'bytes');
console.log('Type:', data.mime_type);Upload with Options
Specify content type:
const { data, error } = await volcano.storage
.from('documents')
.upload('reports/annual-2024.pdf', file, {
contentType: 'application/pdf',
});Upload from Blob or ArrayBuffer
You can upload any binary data:
// From Blob
const blob = new Blob(['Hello, World!'], { type: 'text/plain' });
await volcano.storage.from('uploads').upload('notes/hello.txt', blob);
// From ArrayBuffer
const buffer = await fetchSomeData();
await volcano.storage.from('uploads').upload('data/export.bin', buffer, {
contentType: 'application/octet-stream',
});Upload Path Patterns
Organize files with meaningful paths:
// User-specific files
const userId = user.id;
await storage.upload(`${userId}/avatar.jpg`, file);
await storage.upload(`${userId}/documents/${docId}.pdf`, file);
// Date-organized files
const date = new Date().toISOString().split('T')[0];
await storage.upload(`uploads/${date}/${file.name}`, file);Downloading Files
Basic Download
const { data: blob, error } = await volcano.storage
.from('documents')
.download('reports/annual-2024.pdf');
if (error) {
console.error('Download failed:', error.message);
return;
}
// Create a download link
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'annual-2024.pdf';
a.click();
// Clean up
URL.revokeObjectURL(url);Display in Browser
For images and other displayable content:
const { data: blob } = await volcano.storage.from('avatars').download('user-123/profile.jpg');
const url = URL.createObjectURL(blob);
document.getElementById('avatar').src = url;Partial Download (Range Requests)
Download only a portion of a file:
// Download first 1KB
const { data: blob } = await volcano.storage.from('documents').download('large-file.zip', {
range: 'bytes=0-1023',
});This is useful for streaming media or resuming interrupted downloads.
Listing Files
List All Files in Bucket
const { data: files, error } = await volcano.storage.from('uploads').list();
files.forEach((file) => {
console.log(`${file.name} - ${file.size} bytes`);
});List with Prefix
Filter files by path prefix:
// List files in a specific "folder"
const { data: files } = await volcano.storage.from('uploads').list('images/');
// List user's files
const { data: files } = await volcano.storage.from('documents').list(`${userId}/`);Paginated Listing
For buckets with many files:
const { data: files, nextCursor } = await volcano.storage.from('uploads').list('', { limit: 100 });
console.log(`Found ${files.length} files`);
if (nextCursor) {
// Fetch next page
const { data: moreFiles } = await volcano.storage
.from('uploads')
.list('', { limit: 100, cursor: nextCursor });
}File Metadata
Listed files include metadata:
const { data: files } = await volcano.storage.from('uploads').list();
files.forEach((file) => {
console.log('Name:', file.name);
console.log('Size:', file.size);
console.log('Type:', file.mime_type);
console.log('Public:', file.is_public);
console.log('Created:', file.created_at);
console.log('Owner:', file.owner_id);
if (file.is_public && file.public_url) {
console.log('Public URL:', file.public_url);
}
});Deleting Files
Delete Single File
const { data, error } = await volcano.storage.from('uploads').remove('old-file.txt');
if (!error) {
console.log('File deleted');
}Delete Multiple Files
const { data, error } = await volcano.storage
.from('uploads')
.remove(['temp/file1.txt', 'temp/file2.txt', 'temp/file3.txt']);
if (data) {
console.log(`Deleted ${data.deleted.length} files`);
}Moving and Copying Files
Move (Rename)
const { data, error } = await volcano.storage
.from('documents')
.move('drafts/report.pdf', 'published/report.pdf');
if (data) {
console.log('File moved to:', data.name);
}Copy
const { data, error } = await volcano.storage
.from('documents')
.copy('templates/invoice.pdf', 'invoices/2024-001.pdf');
if (data) {
console.log('File copied to:', data.name);
}Public and Private Files
Default: Private
Files are private by default. They require an authenticated user session to download.
Make File Public
const { data, error } = await volcano.storage
.from('avatars')
.updateVisibility('user-123/profile.jpg', true);
if (data) {
console.log('File is now public');
console.log('Public URL:', data.public_url);
}Make File Private
const { error } = await volcano.storage
.from('avatars')
.updateVisibility('user-123/profile.jpg', false);
if (!error) {
console.log('File is now private');
}Public URLs
Public files have a URL that anyone can access without authentication:
// After making a file public
const { data } = await volcano.storage.from('avatars').updateVisibility('profile.jpg', true);
// Use the public URL
const publicUrl = data.public_url;
// https://api.yourproject.volcano.dev/public/project-id/avatars/profile.jpg
// Use in HTML
<img src={publicUrl} alt="Avatar" />;
// Share via email, embed in external sites, etc.The public URL:
- Requires no authentication
- Works in any browser
- Can be cached by CDNs
- Returns 403 if the file is made private later
Get Public URL
For files you know are public, get their URL directly:
const { data, error } = volcano.storage.from('avatars').getPublicUrl('profile.jpg');
if (data) {
console.log('Public URL:', data.publicUrl);
}Note: This constructs the URL locally and doesn't verify the file is actually public. Use list() or updateVisibility() to get the server-confirmed URL.
Resumable Uploads
For large files (over 100MB) or unreliable connections, use resumable uploads.
Simple Resumable Upload
The uploadResumable method handles everything automatically:
const { data, error } = await volcano.storage
.from('uploads')
.uploadResumable('large-video.mp4', file, {
onProgress: (uploaded, total) => {
const percent = Math.round((uploaded / total) * 100);
console.log(`Progress: ${percent}%`);
},
});
if (data) {
console.log('Upload complete:', data.name);
}Manual Resumable Upload
For more control, manage the upload session yourself:
// 1. Create upload session
const { data: session } = await volcano.storage
.from('uploads')
.createUploadSession('large-video.mp4', {
totalSize: file.size,
contentType: 'video/mp4',
partSize: 10 * 1024 * 1024, // 10MB parts
});
console.log(`Session created: ${session.session_id}`);
console.log(`Will upload in ${session.total_parts} parts`);
// 2. Upload each part
for (let i = 1; i <= session.total_parts; i++) {
const start = (i - 1) * session.part_size;
const end = Math.min(start + session.part_size, file.size);
const partData = file.slice(start, end);
const { error } = await volcano.storage
.from('uploads')
.uploadPart('large-video.mp4', session.session_id, i, partData);
if (error) {
console.error(`Part ${i} failed:`, error.message);
// Can retry this part later
break;
}
console.log(`Part ${i}/${session.total_parts} uploaded`);
}
// 3. Complete the upload
const { data, error } = await volcano.storage
.from('uploads')
.completeUploadSession('large-video.mp4', session.session_id);
if (data) {
console.log('Upload complete!', data.name);
}Resume Interrupted Upload
If an upload is interrupted, you can resume it later:
// Check session status
const { data: status } = await volcano.storage
.from('uploads')
.getUploadSession('large-video.mp4', sessionId);
console.log(`${status.uploaded_parts}/${status.total_parts} parts uploaded`);
console.log(`${status.uploaded_bytes}/${status.total_size} bytes`);
console.log('Missing parts:', status.missing_parts);
// Upload only the missing parts
for (const partNumber of status.missing_parts) {
const start = (partNumber - 1) * status.part_size;
const end = Math.min(start + status.part_size, file.size);
const partData = file.slice(start, end);
await volcano.storage
.from('uploads')
.uploadPart('large-video.mp4', sessionId, partNumber, partData);
}
// Complete when all parts are uploaded
await volcano.storage.from('uploads').completeUploadSession('large-video.mp4', sessionId);Abort Upload
Cancel an in-progress upload and clean up:
const { error } = await volcano.storage
.from('uploads')
.abortUploadSession('large-video.mp4', sessionId);
if (!error) {
console.log('Upload cancelled');
}Resumable Upload Limits
- Minimum part size: 5 MB
- Maximum part size: 25 MB (default)
- Maximum parts: 10,000
- Session expiry: 7 days
Error Handling
Storage operations return errors rather than throwing:
const { data, error } = await volcano.storage.from('uploads').upload('file.txt', file);
if (error) {
// Common errors:
// - "No active session" - User not signed in
// - "File not found" - File doesn't exist
// - "Permission denied" - Access policy violation
// - "Bucket not found" - Invalid bucket name
// - "File too large" - Exceeds size limit
console.error('Upload failed:', error.message);
}Access Policies
Storage access is controlled by policies defined in the Volcano dashboard. Typical patterns:
User's Own Files
Users can only access files in their own "folder":
// Policy: path starts with user's ID
await storage.upload(`${user.id}/avatar.jpg`, file); // Allowed
await storage.upload(`other-user-id/avatar.jpg`, file); // DeniedPublic Read, Authenticated Write
Anyone can download, but only authenticated users can upload:
// Download works without auth (if file is public)
await storage.download('public/logo.png');
// Upload requires authentication
await storage.upload('public/new-file.png', file);Role-Based Access
Admins can access all files, regular users have restrictions:
// Policy checks user's role from JWT
// Admin: full access
// User: only own filesBest Practices
Use Meaningful Paths
Organize files with structured paths:
// Good
`users/${userId}/documents/${year}/${filename}``projects/${projectId}/assets/${assetType}/${filename}`
// Avoid
`file-${Date.now()}.pdf``${Math.random()}.jpg`;Validate Before Upload
Check file type and size client-side:
function validateFile(file) {
const maxSize = 10 * 1024 * 1024; // 10MB
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (file.size > maxSize) {
return 'File too large (max 10MB)';
}
if (!allowedTypes.includes(file.type)) {
return 'Invalid file type';
}
return null;
}
const error = validateFile(file);
if (error) {
showError(error);
return;
}
await storage.upload('images/photo.jpg', file);Handle Large Files
Use resumable uploads for files over 100MB:
const useResumable = file.size > 100 * 1024 * 1024;
if (useResumable) {
await storage.uploadResumable(path, file, { onProgress });
} else {
await storage.upload(path, file);
}Clean Up Object URLs
When creating URLs from blobs, revoke them when done:
const url = URL.createObjectURL(blob);
img.src = url;
// Later, when the image is no longer needed:
URL.revokeObjectURL(url);Show Upload Progress
For better UX with large files:
await volcano.storage.from('uploads').uploadResumable('video.mp4', file, {
onProgress: (uploaded, total) => {
const percent = Math.round((uploaded / total) * 100);
progressBar.style.width = `${percent}%`;
progressText.textContent = `${percent}%`;
},
});Next Steps
Realtime
Volcano Realtime enables live data synchronization using WebSockets. Subscribe to database changes, track user presence, and broadcast messages between clients.
TypeScript
The Volcano SDK includes comprehensive TypeScript definitions for type-safe development. This guide covers type usage, generics, and best practices.