@samveys/storage

Versi
1.0.35 Nov 2026
Pasang
pnpm add @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

ts
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

ts
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 with SVS_E104 before 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() signs path and expires with HMAC-SHA256; check an incoming link with disk.verifyTemporaryUrl(path, expires, signature).
  • s3().temporaryUrl() returns a SigV4 presigned GET URL (at most 7 days).
  • s3().put() signs the body hash, so streams are buffered in memory first.

Safe uploads

ts
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:

  1. refuses an empty file or one over maxBytes;
  2. decides the type from the file's first bytes (PNG, JPEG, GIF, WebP, PDF), never from the client's Content-Type or file name, so a .php renamed to .png is refused;
  3. refuses names with an executable part (shell.php.png, x.html, .htaccess, logo.svg) and extensions that do not fit the real type;
  4. stores it as uploads/<random UUID>.<extension from the real type>: the client's name is never part of the path (it is returned as originalName, for display only);
  5. refuses a disk whose visibility is public (a local root with a public path segment, by default) unless you pass allowPublicDisk: 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.

ts
signRequest({ method, url, headers?, payloadHash, date, credentials: { accessKeyId, secretAccessKey, region, service: 's3' } })
presignUrl({ method: 'GET', url, expiresInSeconds, date, credentials })