Docs/Storage

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

PolicyDescription
publicAnyone can read files, requires auth to write
privateRequires authentication for all operations
signedAccess 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 --recursive

Serving 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.png

Signed 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 format

SDK 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

ParameterDescriptionValues
widthTarget width in pixels1-4096
heightTarget height in pixels1-4096
fitHow to fit imagecover, contain, fill
qualityOutput quality1-100
formatOutput formatwebp, png, jpeg, avif
blurApply blur effect1-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'
)

Next Steps