Storage Package

A Laravel-style file storage abstraction supporting local filesystem and cloud storage (S3) with a unified API.

Installation

bun add @stacksjs/storage

Basic Usage

import { Storage } from '@stacksjs/storage'

// Write a file
await Storage.put('documents/report.txt', 'Report content')

// Read a file
const content = await Storage.get('documents/report.txt')

// Check if file exists
const exists = await Storage.exists('documents/report.txt')

// Delete a file
await Storage.delete('documents/report.txt')

Configuration

Configure storage in config/filesystems.ts:

export default {
  // Default disk
  driver: 'local',

  // Root directory for local storage
  root: process.cwd(),

  // Default file visibility
  defaultVisibility: 'private',

  // S3 configuration
  s3: {
    bucket: process.env.AWS_BUCKET,
    region: process.env.AWS_DEFAULT_REGION || 'us-east-1',
    prefix: '', // Optional path prefix
    endpoint: process.env.AWS_ENDPOINT, // For S3-compatible services
    credentials: {
      accessKeyId: process.env.AWS_ACCESS_KEY_ID,
      secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
    },
  },

  // Public URL configuration
  publicUrl: {
    domain: process.env.ASSET_URL,
  },
}

Working with Disks

Available Disks

By default, three disks are configured:

  • local: Private storage in storage/app
  • public: Web-accessible storage in public
  • s3: Amazon S3 or compatible service

Switching Disks

import { Storage } from '@stacksjs/storage'

// Use local disk (default)
await Storage.put('file.txt', 'content')

// Use public disk
await Storage.disk('public').put('images/logo.png', imageBuffer)

// Use S3 disk
await Storage.disk('s3').put('backups/db.sql', sqlDump)

Configuring Custom Disks

import { Storage } from '@stacksjs/storage'

// Add a custom disk at runtime
Storage.configure('backups', {
  driver: 'local',
  root: '/mnt/backups',
  visibility: 'private',
})

// Use custom disk
await Storage.disk('backups').put('daily.tar.gz', archiveData)

File Operations

Writing Files

import { Storage } from '@stacksjs/storage'

// Write string content
await Storage.put('documents/readme.txt', 'Hello World')

// Write buffer
const imageBuffer = await fetch('https://example.com/image.png')
  .then(r => r.arrayBuffer())
await Storage.put('images/photo.png', new Uint8Array(imageBuffer))

// Write with visibility
await Storage.disk('s3').put('reports/q1.pdf', pdfData, {
  visibility: 'public'
})

Reading Files

import { Storage } from '@stacksjs/storage'

// Read as string
const content = await Storage.get('documents/readme.txt')

// Read as buffer
const disk = Storage.disk('local')
const buffer = await disk.readToBuffer('images/photo.png')

// Stream large files
const stream = Storage.disk('s3').readStream('videos/large.mp4')

Checking File Existence

import { Storage } from '@stacksjs/storage'

// Check if file exists
const exists = await Storage.exists('documents/readme.txt')

// Check if file is missing
const missing = await Storage.missing('documents/readme.txt')

if (missing) {
  await Storage.put('documents/readme.txt', 'Default content')
}

Deleting Files

import { Storage } from '@stacksjs/storage'

// Delete single file
await Storage.delete('temp/cache.txt')

// Delete multiple files
await Storage.disk('local').deleteFile('file1.txt')
await Storage.disk('local').deleteFile('file2.txt')

Copying and Moving Files

import { Storage } from '@stacksjs/storage'

// Copy file
await Storage.copy('original.txt', 'backup/original.txt')

// Move file
await Storage.move('uploads/temp.txt', 'documents/final.txt')

File Information

Getting File Metadata

import { Storage } from '@stacksjs/storage'

// Get file size in bytes
const size = await Storage.size('documents/report.pdf')

// Get last modified timestamp
const lastModified = await Storage.lastModified('documents/report.pdf')

// Get MIME type
const mimeType = await Storage.mimeType('images/photo.jpg')
// Returns: 'image/jpeg'

// Get file checksum
const md5 = await Storage.checksum('documents/report.pdf', 'md5')
const sha256 = await Storage.checksum('documents/report.pdf', 'sha256')

Getting Public URLs

import { Storage } from '@stacksjs/storage'

// Get public URL for a file
const url = await Storage.url('images/logo.png')

// For S3, get temporary signed URL
const disk = Storage.disk('s3')
const signedUrl = await disk.temporaryUrl('private/document.pdf', 3600)
// URL expires in 1 hour

Directory Operations

Creating Directories

import { Storage } from '@stacksjs/storage'

// Create directory
await Storage.makeDirectory('uploads/2024/01')

// Nested directories are created automatically
await Storage.put('deep/nested/path/file.txt', 'content')

Listing Files

import { Storage } from '@stacksjs/storage'

// List files in directory (shallow)
for await (const item of Storage.files('documents')) {
  console.log(item.path, item.type) // 'file' or 'directory'
}

// List all files recursively (deep)
for await (const item of Storage.allFiles('documents')) {
  if (item.type === 'file') {
    console.log(item.path)
  }
}

Deleting Directories

import { Storage } from '@stacksjs/storage'

// Delete directory and all contents
await Storage.deleteDirectory('temp')

File Uploads

Handling Uploaded Files

import { route } from '@stacksjs/router'

route.post('/upload', async (req) => {
  // Get uploaded file
  const file = req.file('document')

  if (file) {
    // Store with auto-generated name
    const path = await file.store('uploads')
    // Returns: 'uploads/abc123.pdf'

    // Store with custom name
    const customPath = await file.storeAs('uploads', 'report-2024.pdf')
    // Returns: 'uploads/report-2024.pdf'

    // Store on specific disk
    await file.store('uploads', { disk: 's3' })
  }

  return Response.json({ success: true })
})

UploadedFile Methods

import { UploadedFile } from '@stacksjs/storage'

route.post('/upload', async (req) => {
  const file = req.file('avatar')

  if (file) {
    // Get file information
    const originalName = file.getClientOriginalName()
    const extension = file.getClientOriginalExtension()
    const mimeType = file.getMimeType()
    const size = file.getSize()

    // Validate file
    if (!['image/jpeg', 'image/png'].includes(mimeType)) {
      return Response.json({ error: 'Invalid file type' }, { status: 400 })
    }

    if (size > 5 _ 1024 _ 1024) { // 5MB
      return Response.json({ error: 'File too large' }, { status: 400 })
    }

    // Store file
    await file.store('avatars')
  }
})

Multiple File Uploads

route.post('/upload-multiple', async (req) => {
  const files = req.getFiles('documents')

  const paths = []
  for (const file of files) {
    const path = await file.store('documents')
    paths.push(path)
  }

  return Response.json({ uploaded: paths })
})

S3 Storage

Basic S3 Operations

import { Storage } from '@stacksjs/storage'

const s3 = Storage.disk('s3')

// Upload to S3
await s3.put('uploads/file.txt', 'content')

// Download from S3
const content = await s3.get('uploads/file.txt')

// Delete from S3
await s3.delete('uploads/file.txt')

S3 with Custom Endpoint

For S3-compatible services like MinIO, DigitalOcean Spaces, or Cloudflare R2:

// config/filesystems.ts
export default {
  s3: {
    bucket: 'my-bucket',
    region: 'us-east-1',
    endpoint: 'https://s3.example.com', // Custom endpoint
    credentials: {
      accessKeyId: process.env.S3_ACCESS_KEY,
      secretAccessKey: process.env.S3_SECRET_KEY,
    },
  },
}

S3 Visibility

import { Storage } from '@stacksjs/storage'

const s3 = Storage.disk('s3')

// Upload as public
await s3.put('public/image.jpg', imageData, { visibility: 'public' })

// Upload as private (default)
await s3.put('private/document.pdf', pdfData)

// Get visibility
const visibility = await s3.getVisibility('public/image.jpg')
// Returns: 'public' or 'private'

// Change visibility
await s3.setVisibility('file.txt', 'public')

Storage Adapters

Using Adapters Directly

import { createLocalStorage, S3StorageAdapter } from '@stacksjs/storage'
import { S3Client } from '@aws-sdk/client-s3'

// Create local storage adapter
const local = createLocalStorage({ root: '/data/storage' })
await local.write('file.txt', 'content')
const content = await local.readToString('file.txt')

// Create S3 adapter
const s3Client = new S3Client({ region: 'us-east-1' })
const s3 = new S3StorageAdapter(s3Client, {
  bucket: 'my-bucket',
  region: 'us-east-1',
  prefix: 'app/'
})

Adapter Methods

// Common adapter methods
await adapter.write(path, contents)
await adapter.readToString(path)
await adapter.readToBuffer(path)
await adapter.fileExists(path)
await adapter.deleteFile(path)
await adapter.copyFile(from, to)
await adapter.moveFile(from, to)
await adapter.fileSize(path)
await adapter.lastModified(path)
await adapter.mimeType(path)
await adapter.checksum(path, { algorithm: 'md5' })
await adapter.publicUrl(path)
await adapter.createDirectory(path)
await adapter.deleteDirectory(path)
adapter.list(path, { deep: true })

Utility Functions

File System Helpers

import {
  copyFile,
  deleteFile,
  deleteFolder,
  ensureDir,
  fileExists,
  folderExists,
  getFileHash,
  getFileSize,
  glob,
  readFile,
  writeFile,
  zipFolder,
  unzipFile
} from '@stacksjs/storage'

// Check existence
const exists = await fileExists('/path/to/file.txt')
const dirExists = await folderExists('/path/to/dir')

// Read/Write
const content = await readFile('/path/to/file.txt')
await writeFile('/path/to/file.txt', 'content')

// Copy/Delete
await copyFile('/source.txt', '/dest.txt')
await deleteFile('/file.txt')
await deleteFolder('/directory')

// Ensure directory exists
await ensureDir('/path/to/directory')

// File info
const size = await getFileSize('/path/to/file.txt')
const hash = await getFileHash('/path/to/file.txt', 'sha256')

// Glob patterns
const files = await glob('**/*.ts', { cwd: '/src' })

// Compression
await zipFolder('/source/dir', '/archive.zip')
await unzipFile('/archive.zip', '/destination')

Storage Manager

Manager Methods

import { Storage, StorageManager } from '@stacksjs/storage'

// Get configured disks
const disks = Storage.getConfiguredDisks()
// Returns: ['local', 'public', 's3']

// Get default disk name
const defaultDisk = Storage.getDefaultDisk()
// Returns: 'local'

// Set default disk
Storage.setDefaultDisk('s3')

// Get disk configuration
const config = Storage.getDiskConfig('s3')

// Reset manager (useful for testing)
Storage.reset()

Edge Cases

Handling Large Files

import { Storage } from '@stacksjs/storage'

// For large files, use streaming
const disk = Storage.disk('s3')

// Read stream
const readStream = disk.readStream('large-file.zip')

// Write in chunks
const largeData = await fetchLargeData()
await disk.writeStream('output.dat', largeData)

Handling Missing Files

import { Storage } from '@stacksjs/storage'

try {
  const content = await Storage.get('nonexistent.txt')
} catch (error) {
  if (error.code === 'ENOENT') {
    console.log('File not found')
  }
}

// Or use exists check
if (await Storage.exists('file.txt')) {
  const content = await Storage.get('file.txt')
}

Handling Concurrent Access

import { Storage } from '@stacksjs/storage'
import { CacheLock } from '@stacksjs/cache'

async function updateFile(path: string, updater: (content: string) => string) {
  const lock = new CacheLock(cache, { lockTimeout: 5000 })

  await lock.withLock(`file:${path}`, async () => {
    const content = await Storage.get(path)
    const updated = updater(content)
    await Storage.put(path, updated)
  })
}

Handling Special Characters in Paths

import { Storage } from '@stacksjs/storage'

// Paths are automatically normalized
await Storage.put('docs/file with spaces.txt', 'content')
await Storage.put('docs/file-with-unicode-\u00e9.txt', 'content')

// Use URL encoding for problematic characters
const safePath = encodeURIComponent('file#with#hashes.txt')
await Storage.put(`docs/${safePath}`, 'content')

API Reference

Storage Facade Methods

MethodDescription
disk(name?)Get disk instance
put(path, contents)Write file
get(path)Read file as string
exists(path)Check if file exists
missing(path)Check if file is missing
delete(path)Delete file
copy(from, to)Copy file
move(from, to)Move file
url(path)Get public URL
size(path)Get file size
lastModified(path)Get modification time
mimeType(path)Get MIME type
checksum(path, algo?)Get file checksum
makeDirectory(path)Create directory
deleteDirectory(path)Delete directory
files(path)List files (shallow)
allFiles(path)List files (deep)

Configuration Options

OptionTypeDescription
driverstringDefault disk driver
rootstringRoot directory
defaultVisibilitystringDefault file visibility
s3.bucketstringS3 bucket name
s3.regionstringAWS region
s3.endpointstringCustom S3 endpoint
s3.prefixstringPath prefix
s3.credentialsobjectAWS credentials