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" 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 'local://' } public async putLocalFile({localPath, storePath, ...args}: {localPath: string, storePath: string, mimeType?: string, tags?: string[], tag?: string}) { 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) { 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, } } catch (e) { if ( e?.code === 'ENOENT' ) { return { path: new UniversalPath(args.storePath, this), exists: false, sizeInBytes: 0, tags: [] } } 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') } }