Files
directus/packages/drive/src/LocalFileSystemStorage.ts
Paul Boudewijn 58964bd1bc Fix localstorage file deletion (#9126)
* excluded VS specific files

* new files after updating directus sources

* case insensitive string matching fix

* Remove accidental commit files

* Cleanup gitignore

Co-authored-by: rijkvanzanten <rijkvanzanten@me.com>
2021-10-25 20:43:57 -04:00

244 lines
5.9 KiB
TypeScript

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<Response> {
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<Response> {
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<DeleteResponse> {
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<ExistsResponse> {
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<ContentResponse<string>> {
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<ContentResponse<Buffer>> {
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<StatResponse> {
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<Response> {
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<Response> {
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<Response> {
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<FileListResponse> {
const fullPrefix = this._fullPath(prefix);
return this._flatDirIterator(fullPrefix, prefix);
}
private async *_flatDirIterator(prefix: string, originalPrefix: string): AsyncIterable<FileListResponse> {
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.toLowerCase().startsWith(prefix.toLowerCase())) {
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;
};