Digging deeper
File storage
Oriel's Storage reads and writes files on disks: folders on your server, or an Amazon S3 bucket (or any S3-compatible
service), through one API.
Configuration#
Your disks are in config/. Each disk names its driver, local or s3, and its settings; default
names the disk Storage uses when you don't name one (FILESYSTEM_DISK, else local). A new app has three disks:
local, public and s3.
The local driver#
A local disk's paths are relative to its root, an absolute folder. The local disk's root is
storage/:
import { Storage } from '@orieljs/framework'await Storage.disk('local').put('example.txt', 'Contents')A path that climbs out of the root with ../ throws.
The public disk#
The public disk is for files anyone may download: its root is storage/, its files are written public, and
its url is APP_URL with /storage. To serve those files, link public/ to storage/:
node oriel storage:linkIt always makes that one link. --relative makes it relative, and --force makes it again. The links entry of a new
app's config/ isn't read yet.
The S3 driver#
The s3 disk reads AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION, AWS_BUCKET, AWS_URL,
AWS_ENDPOINT and AWS_USE_PATH_STYLE_ENDPOINT from your .env. For an S3-compatible service, such as MinIO or
Cloudflare R2, set endpoint, and usePathStyleEndpoint if it wants the bucket in the path.
Obtaining disk instances#
Storage works on the default disk; Storage.disk(name) on another:
import { Storage } from '@orieljs/framework'await Storage.put('avatars/1.txt', 'Contents')await Storage.disk('s3').put('avatars/1.txt', 'Contents')Retrieving files#
Every method is async. get() gives a file's contents as a string, getBytes() as a Uint8Array, and readStream()
as a web ReadableStream. A missing file throws.
import { Storage } from '@orieljs/framework'const contents = await Storage.get('file.txt')const bytes = await Storage.getBytes('photo.jpg')const stream = await Storage.readStream('video.mp4')if (await Storage.exists('file.txt')) { // ...}missing() is exists()'s opposite.
File URLs#
url() gives a file's public URL: the disk's url with the path. A local disk with no url, such as local,
throws.
import { Storage } from '@orieljs/framework'const url = Storage.disk('public').url('avatars/1.jpg')On an S3 disk, url() encodes the key, segment by segment, and joins it to the configured url (a CDN's address, for
example). Without one, it uses the endpoint, else https:// (us-east-1 when no region is
set), with the bucket as a subdomain, or in the path when usePathStyleEndpoint is on.
Temporary URLs#
On an S3 disk, temporaryUrl() gives a signed URL that works until the date you give:
import { Storage } from '@orieljs/framework'const url = await Storage.disk('s3').temporaryUrl('report.pdf', new Date(Date.now() + 5 * 60_000))A local disk can't sign URLs, and throws.
File metadata#
size() gives a file's size in bytes, lastModified() a Date, and mimeType() its type, or undefined. On a
local disk, path() gives the file's absolute path.
Storing files#
put() writes a string, a Uint8Array or a web ReadableStream, creating the folders it needs:
import { Storage } from '@orieljs/framework'await Storage.put('file.txt', 'Contents')await Storage.put('file.txt', 'Contents', { visibility: 'public' })copy(from, to) and move(from, to) copy and move a file on the same disk.
File uploads#
An uploaded file's store() saves it in a folder under a random name, with the extension its contents show, and gives
back the path. storeAs() takes the name. Both take a disk's name last:
import { request, Route } from '@orieljs/framework'Route.post('/avatar', async () => { const file = request().file('avatar') const path = await file!.store('avatars') await file!.storeAs('avatars', 'user-1.jpg', 's3') return { path }})Storage.putFile(directory, file) and Storage.putFileAs(directory, file, name) do the same from the disk's side.
File visibility#
A file is public or private. On a local disk that is the file's mode: 0644 or 0600 for files, and 0755 or
0700 for the folders makeDirectory() makes; a folder put() makes has the default mode. A disk's visibility
setting is its default, and put()'s visibility option sets it for one file.
Deleting files#
delete() takes a path or an array of paths:
import { Storage } from '@orieljs/framework'await Storage.delete('file.txt')await Storage.delete(['file.jpg', 'file2.jpg'])Directories#
files(directory) lists a folder's files, and files(directory, true) its subfolders' too; directories() does the
same for folders. makeDirectory() creates a folder and deleteDirectory() deletes one with everything in it.
Live uploads#
A Live component's upload is first stored as a temporary file, under live-tmp on your default disk, with a random
name. Your component's store() moves it to its place. When an upload finishes, temporary files older than a day are
deleted, except on an S3 disk, where listing the folder on every upload would be slow: give the bucket a lifecycle rule
for live-tmp instead. live.temporary_file_upload.disk and directory choose another place.
Testing#
Storage.fake() swaps a disk for one in memory, so a test writes no files:
import { Storage } from '@orieljs/framework'const disk = Storage.fake('avatars')// Upload the avatar...await disk.assertExists('avatar.jpg')Fakes covers fakes in full.
Custom filesystems#
Storage.extend(driver, factory) adds a driver, in a provider's boot() method: the factory takes a disk's settings
and returns an object with the Filesystem contract's methods.