@samveys/storage
Dari packages/storage/README.md di repository framework.
Halaman ini belum diterjemahkan. Kamu sedang membaca versi bahasa Inggris.
Disks for files: a local directory or any S3-compatible bucket (AWS SigV4 with node:crypto, no SDK), plus safe uploads. No runtime dependencies.
Every disk has the same API: put, get, stream, exists, delete, url, temporaryUrl.
Set up
import { local, s3, storage } from '@samveys/storage'
export const plugin = storage({
default: 'private',
disks: {
private: local({ root: 'storage/private' }),
media: s3({
endpoint: 'https://s3.eu-west-1.amazonaws.com',
bucket: 'my-app-media',
region: 'eu-west-1',
accessKeyId: process.env.S3_KEY ?? '',
secretAccessKey: process.env.S3_SECRET ?? '',
forcePathStyle: false, // true for MinIO and most local S3 servers
}),
},
})The plugin binds 'storage'; container.make('storage').disk('media') returns a disk (the default one without a name).
Read and write
import { local } from '@samveys/storage'
export async function example() {
const disk = local({ root: 'storage/private', baseUrl: 'https://files.example.com' })
await disk.put('reports/2026/q1.txt', 'hello')
const bytes = await disk.get('reports/2026/q1.txt') // SVS_E105 when missing
const exists = await disk.exists('reports/2026/q1.txt')
const link = await disk.temporaryUrl('reports/2026/q1.txt', 300) // expires in 5 minutes
await disk.delete('reports/2026/q1.txt')
return { size: bytes.byteLength, exists, link }
}- Paths are keys, not file-system paths.
.., absolute paths (/x,C:\x), control characters and NUL bytes are refused withSVS_E104before anything happens. The local disk also refuses to follow a symlink out of its root. - The local disk writes to a temporary file and renames it, so a reader never sees half a file.
local().temporaryUrl()signspathandexpireswith HMAC-SHA256; check an incoming link withdisk.verifyTemporaryUrl(path, expires, signature).s3().temporaryUrl()returns a SigV4 presignedGETURL (at most 7 days).s3().put()signs the body hash, so streams are buffered in memory first.
Safe uploads
import type { HttpContext } from '@samveys/core'
import { local, storeUpload } from '@samveys/storage'
const disk = local({ root: 'storage/private' }) // not under a public folder
export async function uploadAvatar(ctx: HttpContext) {
const stored = await storeUpload(ctx, 'avatar', {
disk,
maxBytes: 2 * 1024 * 1024,
allowedTypes: ['image/png', 'image/jpeg', 'image/webp'],
})
return { path: stored.path, size: stored.size, type: stored.mimeType }
}storeUpload reads multipart/form-data and then:
- refuses an empty file or one over
maxBytes; - decides the type from the file's first bytes (PNG, JPEG, GIF, WebP, PDF), never from the client's
Content-Typeor file name, so a.phprenamed to.pngis refused; - refuses names with an executable part (
shell.php.png,x.html,.htaccess,logo.svg) and extensions that do not fit the real type; - stores it as
uploads/<random UUID>.<extension from the real type>: the client's name is never part of the path (it is returned asoriginalName, for display only); - refuses a disk whose
visibilityispublic(alocalroot with apublicpath segment, by default) unless you passallowPublicDisk: true.
Every refusal is SVS_E104 (422). Serve stored files through a route that checks permissions (disk.stream(path)), not through a public folder.
S3 request signing
signRequest() and presignUrl() are exported, and tested against the examples in the AWS Signature Version 4 documentation, so you can sign requests for other S3 calls yourself.
signRequest({ method, url, headers?, payloadHash, date, credentials: { accessKeyId, secretAccessKey, region, service: 's3' } })
presignUrl({ method: 'GET', url, expiresInSeconds, date, credentials })