| 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 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
- 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
import { Storage, StorageManager } from '@stacksjs/storage'
import { UploadedFile, uploadedFile, uploadedFiles } from '@stacksjs/storage'
import { createLocalStorage, LocalStorageAdapter } from '@stacksjs/storage'
import { createS3Storage, S3StorageAdapter } from '@stacksjs/storage'
import { localDisk, s3Disk, configFromEnv } from '@stacksjs/storage'
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
{ , createDirectoryListing, normalizeExpiryToMilliseconds, normalizeExpiryToDate, isFile, isDirectory }
{ copy, copyFile, copyFolder }
{ move, rename }
{ del, deleteFile, deleteFolder, deleteEmptyFolder, deleteEmptyFolders, deleteGlob, isDirectoryEmpty }
{ readJsonFile, readPackageJson, readTextFile, writeFile, writeJsonFile, writeTextFile, put, get, getFiles, deleteFiles, hasFiles }
{ isFolder, isDir, doesFolderExist, createFolder, getFolders }
{ glob, globSync }
{ hashDirectory, hashPath, hashPaths }
{ zip, unzip, archive, unarchive, compress, decompress, gzipSync, gunzipSync, deflateSync, inflateSync }
* 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
import { Storage } from '@stacksjs/storage'
await Storage.put('file.txt', 'Hello World')
await Storage.put('data.bin', new Uint8Array([1, 2, 3]))
const content = await Storage.get('file.txt')
const exists = await Storage.exists('file.txt')
const missing = await Storage.missing('file.txt')
await Storage.delete('file.txt')
await Storage.copy('source.txt', 'dest.txt')
await Storage.move('old.txt', 'new.txt')
size = .()
modified = .()
mime = .()
hash = .(, )
url = .()
.()
.()
( entry .()) {
.(entry., entry.)
}
( entry .()) {
.(entry.)
}
Using Named Disks
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')
Configuring Disks
Storage.init({
default: 's3',
disks: {
custom: { driver: 'local', root: '/custom/path' },
},
})
Storage.configure('backups', { driver: 's3', bucket: 'my-backups', region: 'eu-west-1' })
Storage.setDefaultDisk('s3')
Storage.getDefaultDisk()
Storage.getConfiguredDisks()
Storage.getDiskConfig('s3')
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:
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: , : ): <>
(: ): <>
(: , ?: ):
(: , : ): <>
(: ): <>
(: ): <>
(: ): <>
(: , ?: ): <>
(: , : ): <>
(: , ?: ): <>
(: , ?: ): <>
(: ): <>
(: ): <>
}
Local Adapter (LocalStorageAdapter)
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)
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
File Uploads (UploadedFile)
import { UploadedFile, uploadedFile, uploadedFiles } from '@stacksjs/storage'
const file = uploadedFile(nativeFile)
const files = uploadedFiles([file1, file2])
file.name
file.extension
file.mimeType
file.size
file.file
const buffer = await file.arrayBuffer()
const bytes = await file.bytes()
const text = await file.text()
file.isValid()
file.isImage()
file.isVideo()
file.isAudio()
file.isPdf()
file.isOneOf(['image/*', 'application/pdf'])
file.()
file.()
file.()
file.()
file.()
file.()
path = file.()
path = file.(, )
path = file.(, )
path = file.(, , )
path = file.()
path = file.(, )
path = file.(, )
store() generates a hash-based filename using crypto.randomUUID(). storeAs() validates the filename: rejects path traversal (..), forward slashes, and backslashes.
Low-Level File Operations
Read/Write Files
import { readJsonFile, readTextFile, readPackageJson, writeFile, writeJsonFile, writeTextFile, put, get } from '@stacksjs/storage'
const jsonFile = await readJsonFile('package.json')
await writeJsonFile(jsonFile)
const pkg = await readPackageJson('package.json')
const textFile = await readTextFile('config.txt')
await writeTextFile({ path: 'out.txt', data: 'content' })
const bytes = await writeFile('output.txt', 'data')
put('file.txt', 'contents')
const content = await get('file.txt')
Copy
import { copy, copyFile, copyFolder } from '@stacksjs/storage'
copy('src.txt', 'dest.txt')
copy(['a.txt', 'b.txt'], 'dest/')
copy('srcDir/', 'destDir/')
copy('srcDir/', 'destDir/', ['node_modules'])
copyFile('src.txt', 'dest.txt')
copyFolder('srcDir/', 'destDir/', ['dist'])
Move/Rename
import { move, rename } from '@stacksjs/storage'
const result = await move('old.txt', 'new.txt')
const result = await move(['a.txt', 'b.txt'], 'dest/')
const result = await move('old.txt', 'new.txt', { overwrite: true })
const result = await rename('old.txt', 'new.txt')
move() creates destination directories if needed. Without overwrite: true, throws if destination exists.
Delete
import { del, deleteFile, deleteFolder, deleteEmptyFolder, deleteEmptyFolders, deleteGlob, isDirectoryEmpty } from '@stacksjs/storage'
await del('path')
await deleteFile('file.txt')
await deleteFolder('dir/')
await deleteEmptyFolder('dir/')
await deleteEmptyFolders('parent/')
await deleteGlob('dist/*')
const result = await isDirectoryEmpty('dir/')
All delete functions return Result<string, Error> using @stacksjs/error-handling.
Glob
import { glob, globSync } from '@stacksjs/storage'
const files = await glob('**/*.ts')
const files = await glob(['src/**/*.ts', 'tests/**/*.ts'])
const files = await glob('**/*.ts', {
cwd: '/project',
absolute: true,
dot: true,
onlyFiles: true,
})
const files = globSync('**/*.ts', { cwd: '/project' })
Uses Bun.Glob internally.
Hash
import { hashDirectory, hashPath, hashPaths } from '@stacksjs/storage'
const hash = hashDirectory('src/')
const hash = hashPath('src/index.ts')
const hash = hashPaths(['src/', 'tests/'])
const hash = hashPaths('src/index.ts')
All use createHash('sha256') from Node.js crypto.
Zip/Compression
import { zip, unzip, archive, unarchive, compress, decompress } from '@stacksjs/storage'
import { gzipSync, gunzipSync, deflateSync, inflateSync } from '@stacksjs/storage'
await zip('src/', 'archive.zip')
await zip(['file1.txt', 'file2.txt'], 'archive.zip')
await zip('src/', 'archive.zip', { cwd: '/project' })
await unzip('archive.zip')
await unzip(['a.zip', 'b.zip'])
archive('src/')
unarchive('a.zip')
compress(['a', 'b'])
decompress('a.zip')
const compressed = gzipSync(data)
const decompressed = gunzipSync(compressed)
deflated = (data)
inflated = (deflated)
Folder Utilities
import { isFolder, isDir, doesFolderExist, createFolder, getFolders } from '@stacksjs/storage'
isFolder('/path')
isDir('/path')
doesFolderExist('/path')
await createFolder('dir/')
const dirs = getFolders('parent/')
File Query Utilities
import { doesExist, doesNotExist, hasFiles, getFiles, deleteFiles } from '@stacksjs/storage'
doesExist('/path')
doesNotExist('/path')
hasFiles('dir/')
const files = getFiles('dir/')
const files = getFiles('dir/', ['node_modules'])
deleteFiles('dir/', ['keep.txt'])
Config File Helper
import { updateConfigFile } from '@stacksjs/storage'
await updateConfigFile('config.json', { key: 'newValue' })
Types
type FileContents = string | Buffer | Uint8Array | ReadableStream
enum Visibility {
PUBLIC = 'public',
PRIVATE = 'private',
}
interface StatEntry {
path: string
type: 'file' | 'directory'
visibility: Visibility
size: number
lastModified: number
mimeType?: string
metadata?: Record<string, any>
}
interface DirectoryEntry {
path: string
type: 'file' | 'directory'
}
interface DirectoryListing extends AsyncIterable<DirectoryEntry> {}
interface ListOptions {
deep?: boolean
}
interface TemporaryUrlOptions {
expiresIn: number |
}
{
?: | |
}
= |
= | S3DiskConfig
{
:
:
?:
?: |
}
S3DiskConfig {
:
:
?:
?:
?:
?:
?:
?: { : ; : }
?: |
}
{
:
: <, >
}
Config Helpers
import { localDisk, s3Disk, configFromEnv } from '@stacksjs/storage'
const disk = localDisk('/storage/app', { visibility: 'public', url: '/storage' })
const disk = s3Disk('my-bucket', { region: 'eu-west-1', prefix: 'uploads/' })
const config = configFromEnv({ default: 'local' })
config/filesystems.ts
{
driver: (env.STORAGE_DRIVER || 'bun') as any,
root: env.STORAGE_ROOT || process.cwd(),
s3: {
bucket: env.AWS_S3_BUCKET || '',
region: env.AWS_REGION || 'us-east-1',
prefix: env.AWS_S3_PREFIX || '',
credentials: env.AWS_ACCESS_KEY_ID && env.AWS_SECRET_ACCESS_KEY
? { accessKeyId: env.AWS_ACCESS_KEY_ID, secretAccessKey: env.AWS_SECRET_ACCESS_KEY }
: undefined,
},
publicUrl: {
domain: env.STORAGE_PUBLIC_URL || env.APP_URL || 'http://localhost',
},
defaultVisibility: 'private',
} satisfies FilesystemsConfig
Gotchas
- Default driver is
'bun' (not 'local'), but the Storage facade only supports 'local' and 's3' drivers -- the buildConfig() in the facade maps the filesystems config to 'local' as the default disk driver
- The
local disk root is <project>/storage/app, not the project root
- The
public disk root is <project>/public with URL prefix <appUrl>/storage
- S3 disk is only added if
s3.bucket is configured
list() returns an AsyncIterable -- use for await to iterate
checksum() defaults to sha256 in both adapters (not md5)
temporaryUrl() on local adapter generates HMAC-signed URLs using APP_KEY env var
temporaryUrl() on S3 adapter uses pre-signed URLs via getSignedUrl()
LocalStorageAdapter prevents path traversal -- throws if resolved path escapes the root
S3StorageAdapter requires a bucket name -- throws on construction if missing
changeVisibility() is a no-op on both local and S3 adapters
visibility() always returns 'private' on both adapters
- The
Storage singleton is pre-instantiated -- use Storage.reset() to clear caches for testing
UploadedFile.storeAs() rejects filenames containing .., /, or \ to prevent path traversal
UploadedFile.hashName() is cached -- calling it multiple times returns the same hash
- Low-level operations (
copy, move, delete*, etc.) use synchronous fs methods, while the Storage facade uses async operations
zip() and unzip() shell out to the zip/unzip commands -- they require these tools to be installed on the system