From e772d12f2057e3195bf0aad01dea3b29ae290ce9 Mon Sep 17 00:00:00 2001 From: garrettmills Date: Mon, 29 Mar 2021 11:12:16 -0500 Subject: [PATCH] Create Files unit to manage filesystem implementations defined in config --- src/index.ts | 1 + src/lifecycle/Application.ts | 16 ++++ src/service/Files.ts | 142 +++++++++++++++++++++++++++++++++++ 3 files changed, 159 insertions(+) create mode 100644 src/service/Files.ts diff --git a/src/index.ts b/src/index.ts index 620dd4c..ae41690 100644 --- a/src/index.ts +++ b/src/index.ts @@ -55,6 +55,7 @@ export * from './service/CanonicalStatic' export * from './service/FakeCanonical' export * from './service/Config' export * from './service/Controllers' +export * from './service/Files' export * from './service/HTTPServer' export * from './service/Routing' export * from './service/Middlewares' diff --git a/src/lifecycle/Application.ts b/src/lifecycle/Application.ts index 8f49356..9fad3a3 100644 --- a/src/lifecycle/Application.ts +++ b/src/lifecycle/Application.ts @@ -28,6 +28,22 @@ export function env(key: string, defaultValue?: any): any { return Application.getApplication().env(key, defaultValue) } +/** + * Helper function for fetching a universal path relative to the root of the application. + * @param parts + */ +export function basePath(...parts: PathLike[]): UniversalPath { + return Application.getApplication().path(...parts) +} + +/** + * Helper function for fetching a universal path relative to the `app/` directory. + * @param parts + */ +export function appPath(...parts: PathLike[]): UniversalPath { + return Application.getApplication().appPath(...parts) +} + /** * The main application container. */ diff --git a/src/service/Files.ts b/src/service/Files.ts new file mode 100644 index 0000000..e5bba9c --- /dev/null +++ b/src/service/Files.ts @@ -0,0 +1,142 @@ +import {Unit} from "../lifecycle/Unit" +import {Inject, Singleton} from "@extollo/di" +import {Config} from "./Config" +import {Logging} from "./Logging" +import {Filesystem, ErrorWithContext} from "@extollo/util" + +/** + * Error thrown when a function is called on a filesystem that does not exists in code. + */ +export class FilesystemDoesNotExist extends ErrorWithContext { + constructor( + message: string = 'The specified filesystem does not exist.', + context: {[key: string]: any} = {} + ) { + super(message, context) + } +} + +/** + * Unit service that loads and creates Filesystem drivers from config. The filesystems + * will automatically be opened when the app starts, and closed when it stops. + * + * @example + * Filesystems can be defined in the `server.filesystems` config. For example: + * + * ```typescript + * import {basePath} from "@extollo/lib" + * import {LocalFilesystem, LocalFilesystemConfig} from "@extollo/util" + * + * export default { + * // ... other configs ... + * filesystems: { + * default: { + * driver: LocalFilesystem, + * config: { + * baseDir: basePath('..', 'uploads').toLocal, + * } as LocalFilesystemConfig, + * }, + * }, + * } + * ``` + * + * The `config` key should be an instance of the config interface for the driver + * in question. + * + * @example + * Filesystems can then be accessed from the Files service: + * + * ```typescript + * if ( files.hasFilesystem('default') ) { + * const filesystem = files.getFilesystem('default') + * // ... do something with the filesystem ... + * } + * ``` + * + */ +@Singleton() +export class Files extends Unit { + protected filesystems: {[key: string]: Filesystem} = {} + protected defaultFilesystem?: Filesystem + + @Inject() + protected readonly config!: Config + + @Inject() + protected readonly logging!: Logging + + async up() { + const config = this.config.get('server.filesystems', {}) + const promises = [] + for ( const key in config ) { + if ( !config.hasOwnProperty(key) ) continue; + if ( config[key]?.driver?.prototype instanceof Filesystem ) { + this.logging.verbose(`Registering filesystem '${key}' with driver ${config[key].driver.name}...`) + + const inst = this.make(config[key].driver, config[key].config || {}) + promises.push(inst.open()) + + if ( this.filesystems[key] ) { + this.logging.warn(`Overwriting filesystem with duplicate name: ${key}`) + } + + this.filesystems[key] = inst + + if ( config[key]?.isDefault ) { + this.defaultFilesystem = inst + } + } + } + + await Promise.all(promises) + } + + async down() { + await Promise.all(Object.values(this.filesystems).map(fs => fs.close())) + } + + /** + * Returns true if a filesystem with the given name exists. + * @param key + */ + hasFilesystem(key?: string) { + if ( !key ) { + return !!this.defaultFilesystem + } + + return !!this.filesystems[key] + } + + /** + * Given the name of a filesystem registered in the system, get that filesystem. + * @param key + */ + getFilesystem(key?: string): Filesystem { + if ( !key ) { + if ( !this.defaultFilesystem ) { + throw new FilesystemDoesNotExist() + } + + return this.defaultFilesystem + } + + if ( !this.hasFilesystem(key) ) { + throw new FilesystemDoesNotExist() + } + + return this.filesystems[key] + } + + /** + * Register the given filesystem with this service by name. + * @param key + * @param fs + */ + registerFilesystem(key: string, fs: Filesystem) { + if ( this.hasFilesystem(key) ) { + this.logging.warn(`Overwriting filesystem with duplicate name: ${key}`) + } + + this.filesystems[key] = fs + } +}