Object Storage
S3-compatible storage with built-in CDN, image transformations, and resumable uploads.
Overview
Lubes Storage provides S3-compatible object storage for your files. Files are served through a global CDN for fast downloads, and you can transform images on-the-fly.
S3 Compatible
Use any S3 SDK or tool
Global CDN
Fast downloads worldwide
Image Transforms
Resize and optimize on-the-fly
Buckets
Buckets are containers for your files. Each bucket can have its own access policies, file size limits, and allowed file types.
Creating Buckets
# Via CLI
lubes storage buckets create avatars --public
lubes storage buckets create documents --private
# Via SDK
await storage.createBucket('avatars', {
public: true,
fileSizeLimit: 5 * 1024 * 1024, // 5MB
allowedMimeTypes: ['image/png', 'image/jpeg', 'image/webp'],
})Bucket Policies
| Policy | Description |
|---|---|
| public | Anyone can read files, requires auth to write |
| private | Requires authentication for all operations |
| signed | Access via signed URLs only |
Uploading Files
Basic Upload
// Upload from browser
const file = document.querySelector('input[type="file"]').files[0]
const { url, key } = await storage.upload('avatars', {
file,
contentType: file.type,
})
console.log('Uploaded to:', url)
// Upload with custom path
const { url } = await storage.upload('documents', {
file: pdfBlob,
path: 'reports/2024/q1-report.pdf',
contentType: 'application/pdf',
})Resumable Upload
For large files, use resumable uploads to handle interruptions gracefully:
// Create resumable upload session
const session = await storage.createUploadSession('videos', {
filename: 'video.mp4',
contentType: 'video/mp4',
size: file.size,
})
// Upload in chunks
const chunkSize = 5 * 1024 * 1024 // 5MB chunks
let offset = 0
while (offset < file.size) {
const chunk = file.slice(offset, offset + chunkSize)
await storage.uploadChunk(session.id, {
chunk,
offset,
})
offset += chunkSize
console.log(`Progress: ${Math.round(offset / file.size * 100)}%`)
}
// Complete upload
const { url } = await storage.completeUpload(session.id)CLI Upload
# Upload single file
lubes storage upload avatars ./avatar.png
# Upload with custom path
lubes storage upload documents ./report.pdf --path reports/2024/q1.pdf
# Upload directory
lubes storage upload assets ./dist --recursiveServing Files
Public URLs
// Get public URL (for public buckets)
const url = storage.getPublicUrl('avatars/user-123.png')
// https://storage.lubes.dev/proj_xxx/avatars/user-123.pngSigned URLs
For private files, create time-limited signed URLs:
// Create signed URL for download
const downloadUrl = await storage.createSignedUrl('documents/report.pdf', {
expiresIn: 3600, // 1 hour
})
// Create signed URL for upload
const uploadUrl = await storage.createSignedUploadUrl('documents', {
filename: 'report.pdf',
contentType: 'application/pdf',
expiresIn: 300, // 5 minutes
})Downloading
// Download file content
const data = await storage.download('documents/report.pdf')
// Download as stream
const stream = await storage.downloadStream('videos/large-file.mp4')
// Get file metadata
const metadata = await storage.getMetadata('documents/report.pdf')
console.log(metadata.size, metadata.contentType, metadata.lastModified)Image Transformations
Transform images on-the-fly by adding parameters to the URL. Transformed images are cached at the edge for fast delivery.
URL Parameters
// Resize to specific dimensions
https://storage.lubes.dev/proj_xxx/avatars/user.jpg?width=200&height=200
// Resize maintaining aspect ratio
https://storage.lubes.dev/proj_xxx/photos/hero.jpg?width=800
// Different fit modes
?width=200&height=200&fit=cover // Crop to fill (default)
?width=200&height=200&fit=contain // Fit within bounds
?width=200&height=200&fit=fill // Stretch to fill
// Quality and format
?width=800&quality=80 // Set quality (1-100)
?width=800&format=webp // Convert formatSDK Helper
// Generate transform URL
const thumbnailUrl = storage.getPublicUrl('photos/hero.jpg', {
transform: {
width: 200,
height: 200,
fit: 'cover',
quality: 80,
}
})
// Multiple sizes for responsive images
const sizes = [320, 640, 1280]
const srcset = sizes.map(width =>
`${storage.getPublicUrl('photos/hero.jpg', { transform: { width } })} ${width}w`
).join(', ')Available Transforms
| Parameter | Description | Values |
|---|---|---|
| width | Target width in pixels | 1-4096 |
| height | Target height in pixels | 1-4096 |
| fit | How to fit image | cover, contain, fill |
| quality | Output quality | 1-100 |
| format | Output format | webp, png, jpeg, avif |
| blur | Apply blur effect | 1-100 |
Managing Files
Listing Files
// List all files in bucket
const files = await storage.list('avatars')
// List with prefix
const userFiles = await storage.list('documents', {
prefix: 'user-123/',
})
// Paginate results
const page1 = await storage.list('photos', { limit: 100 })
const page2 = await storage.list('photos', { limit: 100, cursor: page1.cursor })Deleting Files
// Delete single file
await storage.remove('avatars/old-avatar.png')
// Delete multiple files
await storage.remove(['avatars/a.png', 'avatars/b.png', 'avatars/c.png'])
// Delete by prefix (all files in a "folder")
await storage.removeByPrefix('documents/user-123/')Moving/Copying Files
// Copy file
await storage.copy(
'avatars/user-123.png',
'avatars/user-123-backup.png'
)
// Move file (copy + delete)
await storage.move(
'uploads/temp-file.pdf',
'documents/final-report.pdf'
)