| name | volcano-storage |
| description | Detailed guidance for storage buckets, file paths, and upload or download flows with Volcano |
Volcano Storage Skill
Role
Implement secure storage flows with explicit bucket, path, and access-control intent. This skill is self-contained: bucket selection, every file operation, public/private semantics, resumable uploads, and error handling are embedded.
Workflow
- Select a bucket and design ownership-aware paths.
- Implement upload/download/list/remove using canonical APIs.
- Keep files private by default; explicitly justify any public flag.
- For files >100 MB or unreliable connections, use resumable uploads.
Buckets and Paths
Bucket selection
const avatars = volcano.storage.from('avatars');
const documents = volcano.storage.from('documents');
Buckets are created in the dashboard; reference by name.
Path conventions (recommended)
avatars/<userId>/profile.jpg
documents/<userId>/reports/<year>/<filename>.pdf
projects/<projectId>/assets/<assetType>/<filename>
Avoid: random/timestamp-only paths (file-${Date.now()}.pdf).
Upload
Basic
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
const file = fileInput.files![0];
const { data, error } = await volcano.storage
.from('avatars')
.upload(`${user.id}/profile.jpg`, file);
With content type
await volcano.storage
.from('documents')
.upload('reports/annual-2024.pdf', file, { contentType: 'application/pdf' });
From Blob / ArrayBuffer
const blob = new Blob(['hello'], { type: 'text/plain' });
await volcano.storage.from('uploads').upload('notes/hello.txt', blob);
const buffer = await fetchSomeData();
await volcano.storage.from('uploads').upload('data/export.bin', buffer, {
contentType: 'application/octet-stream',
});
Download
Basic
const { data: blob, error } = await volcano.storage
.from('documents')
.download('reports/annual-2024.pdf');
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'annual-2024.pdf';
a.click();
URL.revokeObjectURL(url);
Display in browser (image)
const { data: blob } = await volcano.storage.from('avatars').download(`${userId}/profile.jpg`);
imgEl.src = URL.createObjectURL(blob);
Partial download (range)
const { data: blob } = await volcano.storage
.from('documents')
.download('large-file.zip', { range: 'bytes=0-1023' });
List
Whole bucket
const { data: files, error } = await volcano.storage.from('uploads').list();
With prefix
const { data: files } = await volcano.storage.from('uploads').list('images/');
const { data: mine } = await volcano.storage.from('documents').list(`${userId}/`);
Paginated
const { data: files, nextCursor } = await volcano.storage
.from('uploads')
.list('', { limit: 100 });
if (nextCursor) {
const { data: more } = await volcano.storage
.from('uploads')
.list('', { limit: 100, cursor: nextCursor });
}
Delete
await volcano.storage.from('uploads').remove('old-file.txt');
const { data } = await volcano.storage.from('uploads').remove([
'temp/file1.txt',
'temp/file2.txt',
'temp/file3.txt',
]);
Move and Copy
await volcano.storage.from('documents').move('drafts/report.pdf', 'published/report.pdf');
await volcano.storage.from('documents').copy('templates/invoice.pdf', 'invoices/2024-001.pdf');
Public vs Private
Default: private
Files require an authenticated session to download.
Toggle visibility
const { data } = await volcano.storage
.from('avatars')
.updateVisibility(`${userId}/profile.jpg`, true);
await volcano.storage
.from('avatars')
.updateVisibility(`${userId}/profile.jpg`, false);
Get public URL (does NOT verify file is actually public)
const { data } = volcano.storage.from('avatars').getPublicUrl(`${userId}/profile.jpg`);
Public URLs require no auth, work in any browser, are CDN-friendly, and 403 if the file is later made private.
Resumable Uploads
Use for files >100 MB or unreliable networks.
Simple resumable upload
const { data, error } = await volcano.storage
.from('uploads')
.uploadResumable('large-video.mp4', file, {
onProgress: (uploaded, total) => {
const pct = Math.round((uploaded / total) * 100);
progressBar.value = pct;
},
});
Manual session control
const { data: session } = await volcano.storage
.from('uploads')
.createUploadSession('large-video.mp4', {
totalSize: file.size,
contentType: 'video/mp4',
partSize: 10 * 1024 * 1024,
});
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) break;
}
await volcano.storage
.from('uploads')
.completeUploadSession('large-video.mp4', session.session_id);
Resume after interruption
const { data: status } = await volcano.storage
.from('uploads')
.getUploadSession('large-video.mp4', sessionId);
for (const partNumber of status.missing_parts) {
const start = (partNumber - 1) * status.part_size;
const end = Math.min(start + status.part_size, file.size);
await volcano.storage
.from('uploads')
.uploadPart('large-video.mp4', sessionId, partNumber, file.slice(start, end));
}
await volcano.storage.from('uploads').completeUploadSession('large-video.mp4', sessionId);
Abort
await volcano.storage.from('uploads').abortUploadSession('large-video.mp4', sessionId);
Limits
- Min part size: 5 MB
- Max part size: 25 MB (default)
- Max parts: 10,000
- Session expiry: 7 days
Error Handling
Common error messages:
No active session — sign in first.
File not found — wrong path or bucket.
Permission denied — access policy violation.
Bucket not found — invalid bucket name.
File too large — exceeds size limit.
const { data, error } = await volcano.storage.from('uploads').upload('file.txt', file);
if (error) {
console.error('Upload failed:', error.message);
}
Access Patterns
User-scoped paths
await volcano.storage.from('uploads').upload(`${user.id}/avatar.jpg`, file);
await volcano.storage.from('uploads').upload(`other-user-id/avatar.jpg`, file);
Public read, authenticated write
- Download policy allows anyone (when
is_public).
- Upload policy requires an authenticated session.
Role-based
Policies inspect the user's role from the JWT (admin/user/etc.) — same client API, different rows visible.
Best Practices
- Validate before upload (size and MIME type) for better UX:
const MAX = 10 * 1024 * 1024;
const ALLOWED = ['image/jpeg', 'image/png', 'image/gif'];
if (file.size > MAX) return showError('File too large');
if (!ALLOWED.includes(file.type)) return showError('Invalid file type');
- Choose resumable above 100 MB automatically:
const useResumable = file.size > 100 * 1024 * 1024;
- Revoke object URLs when no longer displayed:
const url = URL.createObjectURL(blob);
img.src = url;
URL.revokeObjectURL(url);
- Show progress for large uploads using
onProgress.
Verification Checklist
- Bucket and path strategy is explicit and ownership-aware.
- Upload/download/list/remove errors are handled.
- Private/public expectations are explicit and justified.
- Resumable upload is used for large files; cleanup-on-cancel is wired.
- No path uses random tokens without semantic prefix.
Optional Fallback Reference