- name
- stacks-storage
- description
- Use when working with file storage in Stacks - the Storage facade (put/get/delete/copy/move/list), StorageAdapter interface, local and S3 disk configurations, file uploads (UploadedFile class), file operations (read/write/copy/move/delete/hash/glob/zip), visibility management, checksums, MIME types, temporary URLs, or filesystem configuration. Covers @stacksjs/storage and config/filesystems.ts.
- license
- MIT
- compatibility
- Bun >= 1.3.0, TypeScript
- allowed-tools
- Read Edit Write Bash Grep Glob
# Stacks Storage
File system abstraction with a Laravel-style Storage facade, local/S3/Azure adapters, file upload handling, and low-level file utilities.
## Key Paths
- Core package: `storage/framework/core/storage/src/`
- Storage facade: `storage/framework/core/storage/src/facade.ts`
- Uploaded file: `storage/framework/core/storage/src/uploaded-file.ts`
- Types: `storage/framework/core/storage/src/types.ts`
- Filesystem config types: `storage/framework/core/storage/src/types/filesystem.ts`
- Local adapter: `storage/framework/core/storage/src/adapters/local.ts`
- S3 adapter: `storage/framework/core/storage/src/adapters/s3.ts`
- Azure adapter: `storage/framework/core/storage/src/adapters/azure.ts`
- Azure request signing: `storage/framework/core/storage/src/azure-signing.ts`
- Memory adapter: `storage/framework/core/storage/src/adapters/memory.ts`
- Bun adapter: `storage/framework/core/storage/src/adapters/bun.ts`
- File utilities: `storage/framework/core/storage/src/files.ts`
- Folder utilities: `storage/framework/core/storage/src/folders.ts`
- Copy: `storage/framework/core/storage/src/copy.ts`
- Move: `storage/framework/core/storage/src/move.ts`
- Delete: `storage/framework/core/storage/src/delete.ts`
- Hash: `storage/framework/core/storage/src/hash.ts`
- Glob: `storage/framework/core/storage/src/glob.ts`
- Zip: `storage/framework/core/storage/src/zip.ts`
- Helpers: `storage/framework/core/storage/src/helpers.ts`
- Configuration: `config/filesystems.ts`
## Package Exports
```typescript
// Storage facade (Laravel-style)
import { Storage, StorageManager } from '@stacksjs/storage'
// File uploads
import { UploadedFile, uploadedFile, uploadedFiles } from '@stacksjs/storage'
// Adapters
import { createLocalStorage, LocalStorageAdapter } from '@stacksjs/storage'
import { createS3Storage, S3StorageAdapter } from '@stacksjs/storage'
// Config helpers
import { localDisk, s3Disk, azureDisk, r2Disk, gcsDisk, filebaseDisk, backblazeDisk, hetznerDisk, configFromEnv } from '@stacksjs/storage'
// Types
import type { StorageAdapter, FileContents, StatEntry, DirectoryEntry, DirectoryListing } from '@stacksjs/storage'
import type { ListOptions, PublicUrlOptions, TemporaryUrlOptions, ChecksumOptions, MimeTypeOptions } from '@stacksjs/storage'
import type { DiskConfig, FilesystemConfig, LocalDiskConfig, S3DiskConfig } from '@stacksjs/storage'
import { Visibility, createDirectoryListing, normalizeExpiryToMilliseconds, normalizeExpiryToDate, isFile, isDirectory } from '@stacksjs/storage'
// Low-level file operations
import { copy, copyFile, copyFolder } from '@stacksjs/storage'
import { move, rename } from '@stacksjs/storage'
import { del, deleteFile, deleteFolder, deleteEmptyFolder, deleteEmptyFolders, deleteGlob, isDirectoryEmpty } from '@stacksjs/storage'
import { readJsonFile, readPackageJson, readTextFile, writeFile, writeJsonFile, writeTextFile, put, get, getFiles, deleteFiles, hasFiles } from '@stacksjs/storage'
import { isFolder, isDir, doesFolderExist, createFolder, getFolders } from '@stacksjs/storage'
import { glob, globSync } from '@stacksjs/storage'
import { hashDirectory, hashPath, hashPaths } from '@stacksjs/storage'
import { zip, unzip, archive, unarchive, compress, decompress, gzipSync, gunzipSync, deflateSync, inflateSync } from '@stacksjs/storage'
import * as storage from '@stacksjs/storage'
```
## Storage Facade (StorageManager)
The `Storage` singleton is a pre-instantiated `StorageManager`. It lazily builds its config from `@stacksjs/config` (which reads `config/filesystems.ts` and env vars).
### Basic Operations
```typescript
import { Storage } from '@stacksjs/storage'
// Write a file (to default disk)
await Storage.put('file.txt', 'Hello World')
await Storage.put('data.bin', new Uint8Array([1, 2, 3]))
// Read a file as string
const content = await Storage.get('file.txt')
// Check existence
const exists = await Storage.exists('file.txt')
const missing = await Storage.missing('file.txt')
// Delete
await Storage.delete('file.txt')
// Copy and move
await Storage.copy('source.txt', 'dest.txt')
await Storage.move('old.txt', 'new.txt')
// File info
const size = await Storage.size('file.txt') // bytes
const modified = await Storage.lastModified('file.txt') // ms timestamp
const mime = await Storage.mimeType('file.txt')
const hash = await Storage.checksum('file.txt', 'sha256') // 'md5' | 'sha1' | 'sha256'
// Public URL
const url = await Storage.url('file.txt')
// Directories
await Storage.makeDirectory('uploads/images')
await Storage.deleteDirectory('uploads/old')
// List files (returns AsyncIterable<{ path, type }>)
for await (const entry of Storage.files('uploads/')) {
console.log(entry.path, entry.type) // 'file' or 'directory'
}
// Recursive listing
for await (const entry of Storage.allFiles('uploads/')) {
console.log(entry.path)
}
```
### Using Named Disks
```typescript
// Use a specific disk
await Storage.disk('s3').write('uploads/file.txt', contents)
await Storage.disk('public').write('images/logo.png', imageData)
await Storage.disk('local').readToString('config.json')
// Disk instances are cached -- subsequent calls return the same adapter
```
### Configuring Disks
```typescript
// Initialize with custom config (overrides env-based config)
Storage.init({
default: 's3',
disks: {
custom: { driver: 'local', root: '/custom/path' },
},
})
// Add/update a disk at runtime
Storage.configure('backups', { driver: 's3', bucket: 'my-backups', region: 'eu-west-1' })
// Change default disk
Storage.setDefaultDisk('s3')
// Query configuration
Storage.getDefaultDisk() // 'local'
Storage.getConfiguredDisks() // ['local', 'public', 's3']
Storage.getDiskConfig('s3') // S3DiskConfig object
// Reset all caches (useful for testing)
Storage.reset()
```
### Built-in Disk Configurations
The facade auto-configures these disks from `config/filesystems.ts`:
- **`local`** -- `driver: 'local'`, root: `<project>/storage/app`, visibility: from config (default `'private'`)
- **`public`** -- `driver: 'local'`, root: `<project>/public`, url: `<appUrl>/storage`, visibility: `'public'`
- **`s3`** -- only added if `s3.bucket` is configured in filesystems config
## StorageAdapter Interface
All adapters implement this interface:
```typescript
interface StorageAdapter {
write(path: string, contents: FileContents): Promise<void>
read(path: string): Promise<FileContents>
readToString(path: string): Promise<string>
readToBuffer(path: string): Promise<Buffer>
readToUint8Array(path: string): Promise<Uint8Array>
deleteFile(path: string): Promise<void>
deleteDirectory(path: string): Promise<void>
createDirectory(path: string): Promise<void>
moveFile(from: string, to: string): Promise<void>
copyFile(from: string, to: string): Promise<void>
stat(path: string): Promise<StatEntry>
list(path: string, options?: ListOptions): DirectoryListing // AsyncIterable
changeVisibility(path: string, visibility: Visibility): Promise<void>
visibility(path: string): Promise<Visibility>
fileExists(path: string): Promise<boolean>
directoryExists(path: string): Promise<boolean>
publicUrl(path: string, options?: PublicUrlOptions): Promise<string>
temporaryUrl(path: string, options: TemporaryUrlOptions): Promise<string>
checksum(path: string, options?: ChecksumOptions): Promise<string>
mimeType(path: string, options?: MimeTypeOptions): Promise<string>
lastModified(path: string): Promise<number>
fileSize(path: string): Promise<number>
}
```
### Local Adapter (`LocalStorageAdapter`)
```typescript
import { createLocalStorage } from '@stacksjs/storage'
const local = createLocalStorage({ root: './storage' })
```
- Uses Node.js `fs/promises` for file operations
- Path traversal protection: throws if resolved path escapes the root directory
- `write()` auto-creates parent directories with `mkdir({ recursive: true })`
- `moveFile()` uses `fs.rename()`; `copyFile()` uses `fs.copyFile()`
- `checksum()` uses `Bun.CryptoHasher` (default algorithm: `sha256`)
- `temporaryUrl()` generates HMAC-signed URLs using `APP_KEY` env var
- `changeVisibility()` is a no-op (Node.js doesn't map to public/private simply)
- `visibility()` always returns `'private'`
- MIME type detection is extension-based (supports txt, html, css, js, json, xml, pdf, zip, jpg, jpeg, png, gif, svg, mp4, mp3, wav)
- `list()` returns an async iterable; supports `deep: true` for recursive listing
### S3 Adapter (`S3StorageAdapter`)
```typescript
import { createS3Storage, S3StorageAdapter } from '@stacksjs/storage'
import { S3Client } from '@stacksjs/ts-cloud'
const client = new S3Client('us-east-1')
const s3 = new S3StorageAdapter(client, { bucket: 'my-bucket', region: 'us-east-1', prefix: 'uploads/' })
```
- Uses `@stacksjs/ts-cloud` S3Client for AWS operations
- Supports key prefix: all paths are prefixed with `config.prefix`
- `write()` auto-detects MIME type from extension and sets `contentType`
- `createDirectory()` is a no-op (S3 directories are implicit)
- `moveFile()` = `copyFile()` + `deleteFile()`
- `deleteDirectory()` lists all objects with prefix and deletes them in bulk
- `temporaryUrl()` uses `client.getSignedUrl()` for pre-signed URLs
- `checksum()` downloads the file content and hashes with `Bun.CryptoHasher`
- `publicUrl()` defaults to `https://<bucket>.s3.<region>.amazonaws.com/<key>`
- `list()` supports pagination via continuation tokens; `deep: true` uses `listAllObjects()`
- `fileExists()` uses `headObject()` and catches 404/NoSuchKey/NotFound errors
Every S3-COMPATIBLE provider is an `s3` disk with a different endpoint, and has
a preset that fills it in: `r2Disk(bucket, accountId)`, `gcsDisk(bucket)`,
`filebaseDisk(bucket)`, `backblazeDisk(bucket, region)`,
`hetznerDisk(bucket, location)`. R2 additionally needs `url` set, because its
API host never serves public objects.
### Azure Adapter (`AzureBlobStorageAdapter`)
```typescript
import { azureDisk, AzureBlobStorageAdapter } from '@stacksjs/storage'
const azure = new AzureBlobStorageAdapter({
account: 'mystorageaccount',
accountKey: process.env.AZURE_STORAGE_ACCOUNT_KEY,
container: 'uploads',
prefix: 'tenant-7',
})
```
Its own driver rather than an `s3` disk with an endpoint, because Azure is the
one provider with no S3-compatible API.
- Speaks the blob REST API over `fetch`; no SDK, and no lazy-client dance
- Shared Key authorization for server-side calls, service SAS for signed URLs,
both in `azure-signing.ts` and tested there directly
- `changeVisibility()` THROWS -- Azure has no per-blob ACL. Public access is a
container property, so `visibility()` reports the container's level, and a
per-object grant is `signedUrl()`
- `putStream()` stages blocks (Put Block) and commits one Put Block List, so the
write is atomic; block ids are zero-padded to a fixed width because Azure
rejects unequal-length ids
- `getStream()` is genuinely incremental, unlike the S3 adapter's buffered one
- `createDirectory()` is a no-op and `deleteDirectory()` deletes by prefix, same
as S3
- `endpoint` defaults to `https://<account>.blob.core.windows.net`; point it at
`http://127.0.0.1:10000/devstoreaccount1` for Azurite
- A `sasToken` disk can read and write but cannot sign URLs -- minting a SAS
needs the account key, and re-serving the configured token would hand out its
full grant
## File Uploads (UploadedFile)
```typescript
import { UploadedFile, uploadedFile, uploadedFiles } from '@stacksjs/storage'
// Create from native File object
const file = uploadedFile(nativeFile)
const files = uploadedFiles([file1, file2])
// Properties
file.name // original filename
file.extension // lowercase extension without dot
file.mimeType // client MIME type
file.size // size in bytes
file.file // underlying File object
// Content access
const buffer = await file.arrayBuffer()
const bytes = await file.bytes() // Uint8Array
const text = await file.text()
// Validation
file.isValid() // size > 0
file.isImage() // MIME starts with 'image/'
file.isVideo() // MIME starts with 'video/'
file.isAudio() // MIME starts with 'audio/'
file.isPdf() // MIME is 'application/pdf'
file.isOneOf(['image/*', 'application/pdf']) // wildcard match
// Name helpers
file.hashName() // UUID-based hash filename: 'a1b2c3d4...f5.jpg'
file.hashName('png') // override extension: 'a1b2c3d4...f5.png'
Ver no GitHub