2021-06-03 03:36:25 +00:00
|
|
|
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'
|
2021-07-08 01:13:23 +00:00
|
|
|
import { Collection } from '../../collection/Collection'
|
2021-06-02 01:59:40 +00:00
|
|
|
|
|
|
|
export interface LocalFilesystemConfig {
|
|
|
|
baseDir: string
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A Filesystem implementation that stores files on the local disk.
|
|
|
|
* @todo walk
|
|
|
|
*/
|
|
|
|
export class LocalFilesystem extends Filesystem {
|
|
|
|
constructor(
|
2021-06-03 03:36:25 +00:00
|
|
|
protected readonly baseConfig: LocalFilesystemConfig,
|
|
|
|
) {
|
|
|
|
super()
|
|
|
|
}
|
2021-06-02 01:59:40 +00:00
|
|
|
|
|
|
|
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 {
|
2021-06-03 03:36:25 +00:00
|
|
|
return 'file://'
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
|
2021-06-03 03:36:25 +00:00
|
|
|
public async putLocalFile({localPath, storePath, ...args}: {localPath: string, storePath: string, mimeType?: string, tags?: string[], tag?: string}): Promise<void> {
|
2021-06-02 01:59:40 +00:00
|
|
|
await fs.promises.copyFile(localPath, this.storePath(storePath))
|
|
|
|
await fs.promises.writeFile(this.metadataPath(storePath), JSON.stringify({
|
|
|
|
mimeType: args.mimeType,
|
2021-06-03 03:36:25 +00:00
|
|
|
tags: this.normalizeTags(args.tag, args.tags),
|
2021-06-02 01:59:40 +00:00
|
|
|
}))
|
|
|
|
}
|
|
|
|
|
|
|
|
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))
|
|
|
|
}
|
|
|
|
|
2021-06-03 03:36:25 +00:00
|
|
|
public async getMetadata(storePath: string): Promise<FileMetadata> {
|
2021-06-02 01:59:40 +00:00
|
|
|
try {
|
|
|
|
const json = (await fs.promises.readFile(this.metadataPath(storePath))).toString('utf-8')
|
|
|
|
return JSON.parse(json)
|
|
|
|
} catch (e) {
|
|
|
|
return {
|
2021-06-03 03:36:25 +00:00
|
|
|
tags: [],
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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,
|
2021-06-03 03:36:25 +00:00
|
|
|
tags: meta.tags ?? [],
|
2021-06-02 01:59:40 +00:00
|
|
|
accessed: stat.atime,
|
|
|
|
modified: stat.mtime,
|
|
|
|
created: stat.ctime,
|
2021-07-08 01:13:23 +00:00
|
|
|
isDirectory: stat.isDirectory(),
|
|
|
|
isFile: stat.isFile(),
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
} catch (e) {
|
2021-10-18 18:03:28 +00:00
|
|
|
if ( (e as any)?.code === 'ENOENT' ) {
|
2021-06-02 01:59:40 +00:00
|
|
|
return {
|
|
|
|
path: new UniversalPath(args.storePath, this),
|
|
|
|
exists: false,
|
|
|
|
sizeInBytes: 0,
|
2021-06-03 03:36:25 +00:00
|
|
|
tags: [],
|
2021-07-08 01:13:23 +00:00
|
|
|
isFile: false,
|
|
|
|
isDirectory: false,
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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) => {
|
2021-06-03 03:36:25 +00:00
|
|
|
if ( err2 ) {
|
|
|
|
return rej(err2)
|
|
|
|
}
|
2021-06-02 01:59:40 +00:00
|
|
|
fs.close(fd, err3 => {
|
2021-06-03 03:36:25 +00:00
|
|
|
if ( err3 ) {
|
|
|
|
return rej(err3)
|
|
|
|
}
|
2021-06-02 01:59:40 +00:00
|
|
|
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 => {
|
2021-06-03 03:36:25 +00:00
|
|
|
if ( err ) {
|
|
|
|
return rej(err)
|
|
|
|
} else {
|
|
|
|
fs.promises.unlink(this.metadataPath(args.storePath)).then(() => res())
|
|
|
|
.catch(rej)
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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')
|
|
|
|
}
|
2021-07-08 01:13:23 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* List all immediate children of the given path.
|
|
|
|
* @param storePath
|
|
|
|
*/
|
|
|
|
public async list(storePath: string): Promise<Collection<string>> {
|
|
|
|
const paths = await fs.promises.readdir(this.storePath(storePath))
|
|
|
|
return Collection.collect<string>(paths)
|
|
|
|
}
|
2021-06-02 01:59:40 +00:00
|
|
|
}
|