mirror of
https://github.com/directus/directus.git
synced 2026-02-05 13:45:36 -05:00
* 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>
244 lines
5.9 KiB
TypeScript
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;
|
|
};
|