File Storage
Stacks.js provides a unified file storage API with support for local, cloud, and custom storage drivers.
Configuration
Storage Configuration
// config/storage.ts
export default {
// Default disk
default: 'local',
disks: {
// Local filesystem
local: {
driver: 'local',
root: 'storage/app',
url: '/storage',
visibility: 'private',
},
// Public files
public: {
driver: 'local',
root: 'storage/app/public',
url: '/storage',
visibility: 'public',
},
// Amazon S3
s3: {
driver: 's3',
bucket: process.env.AWS_BUCKET,
region: process.env.AWS_REGION || 'us-east-1',
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
url: process.env.AWS_URL,
endpoint: process.env.AWS_ENDPOINT, // For S3-compatible services
},
// Google Cloud Storage
gcs: {
driver: 'gcs',
bucket: process.env.GCS_BUCKET,
projectId: process.env.GCS_PROJECT_ID,
keyFile: process.env.GCS_KEY_FILE,
},
// Cloudflare R2
r2: {
driver: 's3',
bucket: process.env.R2_BUCKET,
region: 'auto',
endpoint: process.env.R2_ENDPOINT,
accessKeyId: process.env.R2_ACCESS_KEY_ID,
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
},
}
}
Basic Operations
Writing Files
import { Storage } from '@stacksjs/storage'
// Write string content
await Storage.put('documents/report.txt', 'File content here')
// Write with visibility
await Storage.put('images/photo.jpg', buffer, {
visibility: 'public',
})
// Write to specific disk
await Storage.disk('s3').put('backups/data.json', jsonContent)
// Prepend/Append to file
await Storage.prepend('logs/app.log', 'New log entry\n')
await Storage.append('logs/app.log', 'Another entry\n')
Reading Files
// Read file contents
const content = await Storage.get('documents/report.txt')
// Check if file exists
if (await Storage.exists('documents/report.txt')) {
// File exists
}
// Get file size
const size = await Storage.size('documents/report.txt')
// Get last modified time
const lastModified = await Storage.lastModified('documents/report.txt')
// Get file URL
const url = await Storage.url('images/photo.jpg')
// Get temporary URL (for private files)
const tempUrl = await Storage.temporaryUrl('documents/secret.pdf', {
expiresIn: 3600, // 1 hour
})
Deleting Files
// Delete single file
await Storage.delete('documents/old-report.txt')
// Delete multiple files
await Storage.delete([
'documents/file1.txt',
'documents/file2.txt',
'documents/file3.txt',
])
// Delete if exists
if (await Storage.exists(path)) {
await Storage.delete(path)
}
Copying and Moving
// Copy file
await Storage.copy('source/file.txt', 'destination/file.txt')
// Move file
await Storage.move('old-location/file.txt', 'new-location/file.txt')
// Copy between disks
await Storage.disk('local')
.copy('file.txt', Storage.disk('s3'), 'backups/file.txt')
Directories
Directory Operations
// List files in directory
const files = await Storage.files('documents')
// List all files recursively
const allFiles = await Storage.allFiles('documents')
// List directories
const directories = await Storage.directories('uploads')
// List all directories recursively
const allDirectories = await Storage.allDirectories('uploads')
// Create directory
await Storage.makeDirectory('new-folder')
// Delete directory
await Storage.deleteDirectory('old-folder')
Glob Patterns
// Find files matching pattern
const images = await Storage.glob('uploads/**/*.{jpg,png,gif}')
const recentLogs = await Storage.glob('logs/2024-*.log')
File Uploads
Handling Uploads
// routes/api.ts
router.post('/upload', async (request) => {
const file = request.file('avatar')
// Validate file
await request.validate({
avatar: 'required|file|mimes:jpg,png|max:2048', // 2MB max
})
// Store file
const path = await file.store('avatars')
return { path, url: Storage.url(path) }
})
Upload Options
// Store with custom filename
const path = await file.storeAs('avatars', 'user-123-avatar.jpg')
// Store to specific disk
const path = await file.store('avatars', { disk: 's3' })
// Store with visibility
const path = await file.store('avatars', { visibility: 'public' })
// Get file info before storing
console.log(file.name) // Original filename
console.log(file.extension) // File extension
console.log(file.size) // Size in bytes
console.log(file.mimeType) // MIME type
Multiple File Uploads
router.post('/gallery', async (request) => {
const files = request.files('images')
const paths = await Promise.all(
files.map(file => file.store('gallery'))
)
return { paths }
})
Chunked Uploads
// For large files, use chunked uploads
router.post('/upload/chunk', async (request) => {
const {
chunkIndex,
totalChunks,
uploadId,
} = request.body
const chunk = request.file('chunk')
// Store chunk
await chunk.storeAs('temp/chunks', `${uploadId}-${chunkIndex}`)
// If last chunk, combine
if (chunkIndex === totalChunks - 1) {
const finalPath = await combineChunks(uploadId, totalChunks)
return { path: finalPath, complete: true }
}
return { complete: false }
})
async function combineChunks(uploadId: string, totalChunks: number) {
const chunks = []
for (let i = 0; i < totalChunks; i++) {
const chunk = await Storage.get(`temp/chunks/${uploadId}-${i}`)
chunks.push(chunk)
}
const combined = Buffer.concat(chunks)
const finalPath = `uploads/${uploadId}`
await Storage.put(finalPath, combined)
// Clean up chunks
for (let i = 0; i < totalChunks; i++) {
await Storage.delete(`temp/chunks/${uploadId}-${i}`)
}
return finalPath
}
Image Processing
Image Manipulation
import { Image } from '@stacksjs/image'
router.post('/upload/avatar', async (request) => {
const file = request.file('avatar')
// Process image
const processed = await Image.make(file)
.resize(200, 200)
.crop('center')
.quality(80)
.format('webp')
.toBuffer()
// Store processed image
const path = await Storage.put('avatars/user-123.webp', processed, {
visibility: 'public',
})
return { path }
})
Image Variants
// Generate multiple sizes
const variants = {
thumbnail: { width: 150, height: 150 },
medium: { width: 500, height: 500 },
large: { width: 1200, height: 1200 },
}
const paths = {}
for (const [name, size] of Object.entries(variants)) {
const processed = await Image.make(file)
.resize(size.width, size.height, { fit: 'inside' })
.toBuffer()
paths[name] = await Storage.put(
`images/${id}/${name}.webp`,
processed
)
}
return { paths }
Lazy Image Processing
// Process on-demand via URL
router.get('/images/:path(*)', async (request) => {
const { path } = request.params
const { w, h, q, f } = request.query // width, height, quality, format
// Check cache first
const cacheKey = `processed/${path}?w=${w}&h=${h}&q=${q}&f=${f}`
if (await Storage.exists(cacheKey)) {
return Storage.response(cacheKey)
}
// Process image
const original = await Storage.get(`originals/${path}`)
const processed = await Image.make(original)
.resize(Number(w) || null, Number(h) || null)
.quality(Number(q) || 80)
.format(f || 'webp')
.toBuffer()
// Cache result
await Storage.put(cacheKey, processed)
return new Response(processed, {
headers: {
'Content-Type': `image/${f || 'webp'}`,
'Cache-Control': 'public, max-age=31536000',
},
})
})
Streaming
Stream Large Files
// Read as stream
const stream = await Storage.readStream('large-file.zip')
// Write from stream
await Storage.writeStream('backup.zip', inputStream)
// Pipe to response
router.get('/download/:file', async (request) => {
const { file } = request.params
const stream = await Storage.readStream(file)
return new Response(stream, {
headers: {
'Content-Type': 'application/octet-stream',
'Content-Disposition': `attachment; filename="${file}"`,
},
})
})
Stream Processing
import { Transform } from 'stream'
// Process file while streaming
const readStream = await Storage.readStream('input.csv')
const transform = new Transform({
transform(chunk, encoding, callback) {
// Process chunk
const processed = processChunk(chunk)
callback(null, processed)
}
})
const writeStream = await Storage.writeStream('output.csv')
readStream.pipe(transform).pipe(writeStream)
File Visibility
Managing Visibility
// Set visibility on upload
await Storage.put('file.txt', content, {
visibility: 'public', // or 'private'
})
// Change visibility later
await Storage.setVisibility('file.txt', 'public')
// Get current visibility
const visibility = await Storage.getVisibility('file.txt')
Visibility Configuration
// config/storage.ts
export default {
disks: {
s3: {
driver: 's3',
// ... other config
// Custom visibility mappings
visibility: {
public: {
ACL: 'public-read',
},
private: {
ACL: 'private',
},
},
},
},
}
Signed URLs
Temporary URLs for Private Files
// Generate signed URL
const url = await Storage.disk('s3').temporaryUrl('private/document.pdf', {
expiresIn: 3600, // seconds
})
// With custom response headers
const url = await Storage.disk('s3').temporaryUrl('private/document.pdf', {
expiresIn: 3600,
responseContentDisposition: 'attachment; filename="download.pdf"',
responseContentType: 'application/pdf',
})
Signed Upload URLs
// Generate pre-signed upload URL
router.get('/upload-url', async (request) => {
const { filename, contentType } = request.query
const { url, fields } = await Storage.disk('s3').signedUploadUrl({
key: `uploads/${crypto.randomUUID()}/${filename}`,
contentType,
expiresIn: 3600,
maxSize: 10 * 1024 * 1024, // 10MB
})
return { url, fields }
})
// Client-side direct upload
// const formData = new FormData()
// Object.entries(fields).forEach(([key, value]) => {
// formData.append(key, value)
// })
// formData.append('file', file)
// await fetch(url, { method: 'POST', body: formData })
File Metadata
Working with Metadata
// Get file metadata
const metadata = await Storage.metadata('document.pdf')
// { size, lastModified, contentType, ... }
// Set custom metadata (S3)
await Storage.put('file.txt', content, {
metadata: {
'x-custom-key': 'custom-value',
'uploaded-by': 'user-123',
},
})
// Read custom metadata
const { metadata } = await Storage.disk('s3').metadata('file.txt')
MIME Type Detection
import { getMimeType } from '@stacksjs/storage'
// From filename
const mimeType = getMimeType('document.pdf') // 'application/pdf'
// From file content
const mimeType = await getMimeType(buffer)
Custom Drivers
Creating a Custom Driver
// app/Storage/CustomDriver.ts
import type { StorageDriver, StorageConfig } from '@stacksjs/storage'
export class CustomDriver implements StorageDriver {
constructor(private config: StorageConfig) {}
async put(path: string, content: Buffer): Promise<void> {
// Implementation
}
async get(path: string): Promise<Buffer> {
// Implementation
}
async delete(path: string): Promise<void> {
// Implementation
}
async exists(path: string): Promise<boolean> {
// Implementation
}
async url(path: string): Promise<string> {
// Implementation
}
// ... other methods
}
// Register driver
Storage.extend('custom', (config) => new CustomDriver(config))
Using Custom Driver
// config/storage.ts
export default {
disks: {
custom: {
driver: 'custom',
// Custom config options
},
},
}
Storage Events
Listening to Storage Events
import { Storage } from '@stacksjs/storage'
Storage.on('write', (path, disk) => {
console.log(`File written: ${path} on ${disk}`)
})
Storage.on('delete', (path, disk) => {
console.log(`File deleted: ${path} on ${disk}`)
})
Storage.on('copy', (source, destination) => {
console.log(`File copied: ${source} to ${destination}`)
})
Cleanup Jobs
// app/Jobs/CleanupTempFiles.ts
export default {
schedule: '0 2 * * *', // Daily at 2 AM
async handle() {
const tempFiles = await Storage.allFiles('temp')
const oneDayAgo = Date.now() - 24 * 60 * 60 * 1000
for (const file of tempFiles) {
const lastModified = await Storage.lastModified(file)
if (lastModified < oneDayAgo) {
await Storage.delete(file)
}
}
}
}
Best Practices
File Organization
storage/
├── app/
│ ├── avatars/ # User avatars
│ │ └── {user-id}/
│ ├── documents/ # User documents
│ │ └── {user-id}/
│ ├── public/ # Publicly accessible
│ │ ├── images/
│ │ └── assets/
│ └── temp/ # Temporary files
├── framework/ # Framework files
│ ├── cache/
│ └── sessions/
└── logs/ # Application logs
Security Considerations
// Validate file types
await request.validate({
file: 'required|file|mimes:jpg,png,pdf|max:10240',
})
// Use random filenames
const filename = `${crypto.randomUUID()}${extname(file.name)}`
// Don't expose internal paths
const publicUrl = Storage.url(path) // Use this, not the raw path
// Set proper visibility
await Storage.put('private/document.pdf', content, {
visibility: 'private',
})
Performance Tips
// Use streaming for large files
const stream = await Storage.readStream('large-file.zip')
// Batch operations
await Promise.all(files.map(f => Storage.delete(f)))
// Use CDN for public assets
// Configure in storage config with `url` pointing to CDN
// Clean up temp files regularly
// Use scheduled jobs for cleanup