You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
lib/src/util/support/path/LocalFilesystem.ts

162 lines
5.3 KiB

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<void> {
// 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<UniversalPath> {
const tempName = this.tempName()
await fs.promises.copyFile(this.storePath(storePath), tempName)
return new UniversalPath(tempName)
}
public getStoreFileAsStream(args: { storePath: string }): fs.ReadStream | Promise<fs.ReadStream> {
return fs.createReadStream(this.storePath(args.storePath))
}
public putStoreFileAsStream(args: { storePath: string }): fs.WriteStream | Promise<fs.WriteStream> {
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<void> {
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<Stat> {
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<void> {
return new Promise<void>((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<void> {
if ( !args.recursive ) {
await fs.promises.unlink(this.storePath(args.storePath))
await fs.promises.unlink(this.metadataPath(args.storePath))
} else {
await new Promise<void>((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<void> {
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')
}
}