import {Unit} from '../lifecycle/Unit' import {Inject, Singleton} from '../di' import {Config} from './Config' import {Logging} from './Logging' import {Filesystem, ErrorWithContext} from '../util' /** * Error thrown when a function is called on a filesystem that does not exists in code. */ export class FilesystemDoesNotExist extends ErrorWithContext { constructor( message = '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(): Promise { const config = this.config.get('server.filesystems', {}) const promises = [] for ( const key in config ) { if ( !Object.prototype.hasOwnProperty.call(config, 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) // Once they have initialized, register the DI token for the default filesystem this.app().registerProducer(Filesystem, () => { // This will throw FilesystemDoesNotExistError if no default filesystem was created // Such behavior is desired as it is clearer than an invalid injection error, e.g. return this.getFilesystem() }) // If file uploads are enabled, ensure that the default upload prefix exists if ( this.defaultFilesystem ) { const upload = this.config.get('server.uploads', {}) if ( upload?.enable && upload?.filesystemPrefix ) { await this.defaultFilesystem.mkdir({ storePath: upload.filesystemPrefix, }) } } } async down(): Promise { 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): boolean { if ( !key ) { return Boolean(this.defaultFilesystem) } return Boolean(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): void { if ( this.hasFilesystem(key) ) { this.logging.warn(`Overwriting filesystem with duplicate name: ${key}`) } this.filesystems[key] = fs } }