import {FileMetadata, FileNotFoundError, Filesystem, Stat} from './Filesystem' import * as fs from 'fs' import * as path from 'path' import {UniversalPath} from '../path' import * as rimraf from 'rimraf' import * as mkdirp from 'mkdirp' import { Collection } from '../../collection/Collection' export interface LocalFilesystemConfig { baseDir: string } /** * A Filesystem implementation that stores files on the local disk. * @todo walk */ export class LocalFilesystem extends Filesystem { constructor( protected readonly baseConfig: LocalFilesystemConfig, ) { super() } async open(): Promise { // Make sure the base directory exists await mkdirp(this.baseConfig.baseDir) await mkdirp(this.storePath('')) await mkdirp(this.metadataPath('')) } public getPrefix(): string { return 'file://' } public async putLocalFile({localPath, storePath, ...args}: {localPath: string, storePath: string, mimeType?: string, tags?: string[], tag?: string}): Promise { await fs.promises.copyFile(localPath, this.storePath(storePath)) await fs.promises.writeFile(this.metadataPath(storePath), JSON.stringify({ mimeType: args.mimeType, tags: this.normalizeTags(args.tag, args.tags), })) } public async getStoreFileAsTemp({ storePath }: {storePath: string}): Promise { const tempName = this.tempName() await fs.promises.copyFile(this.storePath(storePath), tempName) return new UniversalPath(tempName) } public getStoreFileAsStream(args: { storePath: string }): fs.ReadStream | Promise { return fs.createReadStream(this.storePath(args.storePath)) } public putStoreFileAsStream(args: { storePath: string }): fs.WriteStream | Promise { return fs.createWriteStream(this.storePath(args.storePath)) } public async getMetadata(storePath: string): Promise { try { const json = (await fs.promises.readFile(this.metadataPath(storePath))).toString('utf-8') return JSON.parse(json) } catch (e) { return { tags: [], } } } async setMetadata(storePath: string, meta: FileMetadata): Promise { if ( !(await this.stat({storePath})).exists ) { throw new FileNotFoundError(storePath) } const metaPath = this.metadataPath(storePath) await fs.promises.writeFile(metaPath, JSON.stringify(meta)) } public async stat(args: { storePath: string }): Promise { try { const stat = await fs.promises.stat(this.storePath(args.storePath)) const meta = await this.getMetadata(args.storePath) return { path: new UniversalPath(args.storePath, this), exists: true, sizeInBytes: stat.size, mimeType: meta.mimeType, tags: meta.tags ?? [], accessed: stat.atime, modified: stat.mtime, created: stat.ctime, isDirectory: stat.isDirectory(), isFile: stat.isFile(), } } catch (e) { if ( (e as any)?.code === 'ENOENT' ) { return { path: new UniversalPath(args.storePath, this), exists: false, sizeInBytes: 0, tags: [], isFile: false, isDirectory: false, } } throw e } } public async touch(args: {storePath: string}): Promise { return new Promise((res, rej) => { const storePath = this.storePath(args.storePath) const time = new Date() fs.utimes(storePath, time, time, err => { if ( err ) { fs.open(storePath, 'w', (err2, fd) => { if ( err2 ) { return rej(err2) } fs.close(fd, err3 => { if ( err3 ) { return rej(err3) } res() }) }) } else { res() } }) }) } public async remove(args: { storePath: string; recursive?: boolean }): Promise { if ( !args.recursive ) { await fs.promises.unlink(this.storePath(args.storePath)) await fs.promises.unlink(this.metadataPath(args.storePath)) } else { await new Promise((res, rej) => { rimraf(this.storePath(args.storePath), err => { if ( err ) { return rej(err) } else { fs.promises.unlink(this.metadataPath(args.storePath)).then(() => res()) .catch(rej) } }) }) } } public async mkdir(args: {storePath: string}): Promise { await mkdirp(this.storePath(args.storePath)) } /** * Given a relative path in the store, resolve it to an absolute path. * @param storePath * @protected */ protected storePath(storePath: string): string { return path.resolve(this.baseConfig.baseDir, 'data', storePath) } /** * Given a relative path in the store, resolve it to an absolute path to the metadata JSON * file for that path. * @param storePath * @protected */ protected metadataPath(storePath: string): string { return path.resolve(this.baseConfig.baseDir, 'meta', storePath + '.json') } /** * List all immediate children of the given path. * @param storePath */ public async list(storePath: string): Promise> { const paths = await fs.promises.readdir(this.storePath(storePath)) return Collection.collect(paths) } }