Storage
هذا المحتوى غير متوفر بلغتك بعد.
Storage
Section titled “Storage”@framework/storage gives every module one StorageDisk contract for reading
and writing files: put/get/stream, copy/move, bounded listing and public
URLs. Every file lives on a named disk whose visibility (private or
public) is fixed at configuration time, and every path is guarded against
traversal before it touches a filesystem. The local driver ships with the
framework; every other driver is constructed through the storagekit engine.
Register the provider — it reads the storage section of your configuration
directly, so it takes no arguments:
import { defineProvider } from '@framework/core';import { createStorageProvider } from '@framework/storage';import type { AppConfig } from '../../config';
export const storageProvider = defineProvider<AppConfig>(createStorageProvider<AppConfig>());Each entry of storage.disks becomes a singleton under storageDiskToken(name),
and the storage.default disk is additionally bound to STORAGE:
import { STORAGE, storageDiskToken } from '@framework/storage';
const disk = scope.resolve(STORAGE); // the default diskconst publicDisk = scope.resolve(storageDiskToken('public')); // a named diskConfiguration
Section titled “Configuration”Environment variables:
STORAGE_DEFAULT_DISK=local# STORAGE_ROOT overrides the local disk's root (absolute path)The storage section itself:
storage: { default: 'local', disks: { local: { driver: 'local', root: './storage/app/private', visibility: 'private' }, public: { driver: 'local', root: './storage/app/public', visibility: 'public', publicBaseUrl: '/storage', // or 'https://cdn.example.com/files' }, },},Rules enforced at boot:
storage.defaultmust name an existing disk.- Public disks must declare
publicBaseUrl; private disks never resolve a public URL. rootmay be absolute or relative to the process cwd and is created on first use.
const disk = scope.resolve(STORAGE);
// Write text or bytesawait disk.put('docs/hello.txt', 'hello storage');await disk.put('uploads/avatar.bin', new Uint8Array([0, 1, 2]));
// Read content plus metadataconst file = await disk.get('docs/hello.txt');new TextDecoder().decode(file.content); // 'hello storage'file.path; // 'docs/hello.txt', exactly as passed infile.size; // 13file.contentType; // MIME type derived from the extension, when derivablefile.lastModified; // Date
// Stream large files — a web ReadableStream that also supports for-awaitconst stream = await disk.stream('exports/big.csv');for await (const chunk of stream) { /* … */}
// Inspect, reorganize, deleteawait disk.exists('docs/hello.txt'); // trueawait disk.size('docs/hello.txt'); // 13await disk.copy('docs/hello.txt', 'backup/hello.txt');await disk.move('backup/hello.txt', 'archive/2026/hello.txt');await disk.delete('docs/hello.txt'); // deleting a missing path is a no-op
// Bounded recursive listing under a prefixconst paths = await disk.list('docs'); // sorted; default limit 100const first50 = await disk.list('docs', { limit: 50 });Listing is bounded by design: entries beyond the limit are dropped and there
is no pagination — narrow the prefix instead (limits outside 1–1000 reject
with STORAGE_LIST_LIMIT_INVALID). Reads, sizes, moves and copies of missing
paths reject with STORAGE_FILE_NOT_FOUND; an unknown prefix lists empty.
Uploads
Section titled “Uploads”Validate size and extension before persisting anything:
import { assertUploadAllowed } from '@framework/storage';
assertUploadAllowed({ size: bytes.byteLength, maxBytes: 5 * 1024 * 1024, allowedExtensions: ['png', 'jpg'], // case-insensitive: photo.PNG passes filename: upload.name,});await disk.put(`uploads/${upload.name}`, bytes);Oversize uploads throw STORAGE_FILE_TOO_LARGE; disallowed extensions throw
STORAGE_EXTENSION_FORBIDDEN. Extension rules require the original
filename. For upload staging areas, pruneTemp(root, maxAgeMs) removes
files older than the cutoff and drops the emptied directories.
Advanced
Section titled “Advanced”Visibility
Section titled “Visibility”A disk’s visibility is a hard boundary, not a per-file hint:
publicUrl(path)joins the configured base on a public disk (/storage/img/logo.png) and throwsSTORAGE_VISIBILITY_FORBIDDENon a private one — a private file never resolves to a URL.- Per-write overrides cannot cross the split:
put(…, { visibility: 'public' })on a private disk rejects withSTORAGE_VISIBILITY_FORBIDDEN, and so does the reverse. A private file can never be promoted by a call-site flag.
const publicDisk = scope.resolve(storageDiskToken('public'));const url = publicDisk.publicUrl('img/logo.png'); // '/storage/img/logo.png'Path safety
Section titled “Path safety”Every path argument is validated on every operation. Traversal attempts —
../escape.txt, absolute paths (/etc/passwd), URL-encoded dots
(%2e%2e/escape), backslashes, double slashes — reject with
STORAGE_PATH_FORBIDDEN before any file access, on writes, reads, deletes,
lists, moves, copies and publicUrl alike. The same guards are exported so
your own tools validate exactly like the disks:
import { resolveInsideRoot, validateStoragePath } from '@framework/storage';
const canonical = validateStoragePath('docs/hello.txt'); // 'docs/hello.txt'const absolute = resolveInsideRoot(disk.root, 'docs/hello.txt'); // strictly inside the rootCapabilities and multi-driver
Section titled “Capabilities and multi-driver”Disks declare what they can do; local disks stream but never sign URLs:
disk.capabilities; // { streaming: true, signedUrls: false }await disk.signedUrl('docs/hello.txt'); // rejects STORAGE_CAPABILITY_UNSUPPORTEDCloud drivers (s3, r2, minio, azure, gcs, oracle, rustfs, …)
answer through the storagekit engine instead. Add the disk to storage.disks
and install that driver’s peer SDK — for example @aws-sdk/client-s3 for
s3. Only driver and the visibility fields are validated by the framework;
the options bag is forwarded verbatim to storagekit:
disks: { local: { driver: 'local', root: './storage/app/private', visibility: 'private' }, assets: { driver: 's3', visibility: 'private', options: { bucket: 'my-bucket', region: 'eu-central-1' }, },},Boot creates every configured disk eagerly, so a missing peer SDK or invalid
kit options fail startup with STORAGE_DRIVER_UNAVAILABLE instead of failing
on the first file operation.
Testing
Section titled “Testing”Point a LocalStorageDisk at a temp directory for round-trip tests — the
root is created on demand:
import { join } from 'node:path';import { mkdtemp } from 'node:fs/promises';import { tmpdir } from 'node:os';import { LocalStorageDisk } from '@framework/storage';
const disk = new LocalStorageDisk({ name: 'test', root: await mkdtemp(join(tmpdir(), 'disk-')), visibility: 'private',});To test wiring, boot the Application with a storage config section and
resolve STORAGE from the container. Boot failures are assertions too: a bad
configuration (unknown default disk, empty root, public disk without a base
URL) fails with ConfigError, and a cloud driver whose peer SDK is missing
fails with STORAGE_DRIVER_UNAVAILABLE.
Related: background jobs that produce artifacts run on the job system — see Queues — and Notifications covers outbound messaging.