import * as fse from 'fs-extra'; //import { promises as fs } from 'fs'; import { dirname, join, resolve, relative, sep } from 'path'; import Storage from './Storage'; import { isReadableStream, pipeline } from './utils'; import { FileNotFound, UnknownException, PermissionMissing } from './exceptions'; import { Response, ExistsResponse, ContentResponse, StatResponse, FileListResponse, DeleteResponse, Range, } from './types'; function handleError(err: Error & { code: string; path?: string }, location: string): Error { switch (err.code) { case 'ENOENT': return new FileNotFound(err, location); case 'EPERM': return new PermissionMissing(err, location); default: return new UnknownException(err, err.code, location); } } export class LocalFileSystemStorage extends Storage { private $root: string; constructor(config: LocalFileSystemStorageConfig) { super(); this.$root = resolve(config.root); } /** * Returns full path relative to the storage's root directory. */ private _fullPath(relativePath: string): string { return join(this.$root, join(sep, relativePath)); } /** * Appends content to a file. */ public async append(location: string, content: Buffer | string): Promise { try { const result = await fse.appendFile(this._fullPath(location), content); return { raw: result }; } catch (e: any) { throw handleError(e, location); } } /** * Copy a file to a location. */ public async copy(src: string, dest: string): Promise { try { const result = await fse.copy(this._fullPath(src), this._fullPath(dest)); return { raw: result }; } catch (e: any) { throw handleError(e, `${src} -> ${dest}`); } } /** * Delete existing file. */ public async delete(location: string): Promise { try { const result = await fse.unlink(this._fullPath(location)); return { raw: result, wasDeleted: true }; } catch (e: any) { const error = handleError(e, location); if (error instanceof FileNotFound) { return { raw: undefined, wasDeleted: false }; } throw error; } } /** * Returns the driver. */ public driver(): typeof fse { return fse; } /** * Determines if a file or folder already exists. */ public async exists(location: string): Promise { try { const result = await fse.pathExists(this._fullPath(location)); return { exists: result, raw: result }; } catch (e: any) { throw handleError(e, location); } } /** * Returns the file contents as string. */ public async get(location: string, encoding = 'utf-8'): Promise> { try { const result = await fse.readFile(this._fullPath(location), encoding); return { content: result, raw: result }; } catch (e: any) { throw handleError(e, location); } } /** * Returns the file contents as Buffer. */ public async getBuffer(location: string): Promise> { try { const result = await fse.readFile(this._fullPath(location)); return { content: result, raw: result }; } catch (e: any) { throw handleError(e, location); } } /** * Returns file size in bytes. */ public async getStat(location: string): Promise { try { const stat = await fse.stat(this._fullPath(location)); return { size: stat.size, modified: stat.mtime, raw: stat, }; } catch (e: any) { throw handleError(e, location); } } /** * Returns a read stream for a file location. */ public getStream(location: string, range?: Range): NodeJS.ReadableStream { return fse.createReadStream(this._fullPath(location), { start: range?.start, end: range?.end, }); } /** * Move file to a new location. */ public async move(src: string, dest: string): Promise { try { const result = await fse.move(this._fullPath(src), this._fullPath(dest)); return { raw: result }; } catch (e: any) { throw handleError(e, `${src} -> ${dest}`); } } /** * Prepends content to a file. */ public async prepend(location: string, content: Buffer | string): Promise { try { const { content: actualContent } = await this.get(location, 'utf-8'); return this.put(location, `${content}${actualContent}`); } catch (e: any) { if (e instanceof FileNotFound) { return this.put(location, content); } throw e; } } /** * Creates a new file. * This method will create missing directories on the fly. */ public async put(location: string, content: Buffer | NodeJS.ReadableStream | string): Promise { const fullPath = this._fullPath(location); try { if (isReadableStream(content)) { const dir = dirname(fullPath); await fse.ensureDir(dir); const ws = fse.createWriteStream(fullPath); await pipeline(content, ws); return { raw: undefined }; } const result = await fse.outputFile(fullPath, content); return { raw: result }; } catch (e: any) { throw handleError(e, location); } } /** * List files with a given prefix. */ public flatList(prefix = ''): AsyncIterable { const fullPrefix = this._fullPath(prefix); return this._flatDirIterator(fullPrefix, prefix); } private async *_flatDirIterator(prefix: string, originalPrefix: string): AsyncIterable { const prefixDirectory = prefix[prefix.length - 1] === sep ? prefix : dirname(prefix); try { const dir = await fse.opendir(prefixDirectory); for await (const file of dir) { const fileName = join(prefixDirectory, file.name); if (fileName.startsWith(prefix)) { if (file.isDirectory()) { yield* this._flatDirIterator(join(fileName, sep), originalPrefix); } else if (file.isFile()) { const path = relative(this.$root, fileName); yield { raw: null, path, }; } } } } catch (e: any) { if (e.code !== 'ENOENT') { throw handleError(e, originalPrefix); } } } } export type LocalFileSystemStorageConfig = { root: string; };