@flystorage/local-fs
Version:
<img src="https://raw.githubusercontent.com/duna-oss/flystorage/main/flystorage.svg" width="50px" height="50px" />
242 lines (241 loc) • 9.34 kB
JavaScript
import { checksumFromStream, PathPrefixer, } from '@flystorage/file-storage';
import { lookup } from "mime-types";
import { createReadStream, createWriteStream, Dirent } from 'node:fs';
import { chmod, mkdir, opendir, rm, stat, rename, copyFile } from 'node:fs/promises';
import { posix, extname } from 'node:path';
import { pipeline } from 'stream/promises';
import { PortableUnixVisibilityConversion } from './unix-visibility.js';
import { dynamicallyImport } from '@flystorage/dynamic-import';
import { PreparedUploadsAreNotSupported } from '@flystorage/file-storage';
export class BaseUrlLocalPublicUrlGenerator {
async publicUrl(path, options) {
if (options.baseUrl === undefined) {
throw new Error('No base URL defined for public URL generation');
}
const base = options.baseUrl.endsWith('/') ? options.baseUrl : `${options.baseUrl}/`;
if (posix.sep === '\\' && path.includes(posix.sep)) {
path = path.replace(posix.sep, '/');
}
return `${base}${path}`;
}
}
export class FailingLocalTemporaryUrlGenerator {
async temporaryUrl() {
throw new Error('No temporary URL generator provided');
}
}
let fileTypeImport;
let fileTypes = undefined;
export class LocalStorageAdapter {
rootDir;
options;
visibilityConversion;
publicUrlGenerator;
temporaryUrlGenerator;
uploadPreparer;
prefixer;
constructor(rootDir, options = {}, visibilityConversion = new PortableUnixVisibilityConversion(), publicUrlGenerator = new BaseUrlLocalPublicUrlGenerator(), temporaryUrlGenerator = new FailingLocalTemporaryUrlGenerator(), uploadPreparer = new PreparedUploadsAreNotSupported()) {
this.rootDir = rootDir;
this.options = options;
this.visibilityConversion = visibilityConversion;
this.publicUrlGenerator = publicUrlGenerator;
this.temporaryUrlGenerator = temporaryUrlGenerator;
this.uploadPreparer = uploadPreparer;
this.rootDir = posix.join(this.rootDir, posix.sep);
this.prefixer = new PathPrefixer(this.rootDir, posix.sep, posix.join);
}
async copyFile(from, to, options) {
await this.ensureRootDirectoryExists();
await this.ensureParentDirectoryExists(to, options);
await copyFile(this.prefixer.prefixFilePath(from), this.prefixer.prefixFilePath(to));
}
async moveFile(from, to, options) {
await this.ensureRootDirectoryExists();
await this.ensureParentDirectoryExists(to, options);
await rename(this.prefixer.prefixFilePath(from), this.prefixer.prefixFilePath(to));
}
prepareUpload(path, options) {
return this.uploadPreparer.prepareUpload(path, options);
}
temporaryUrl(path, options) {
return this.temporaryUrlGenerator.temporaryUrl(path, { ...this.options.temporaryUrlOptions, ...options });
}
publicUrl(path, options) {
return this.publicUrlGenerator.publicUrl(path, { ...this.options.publicUrlOptions, ...options });
}
async mimeType(path, options) {
if (fileTypeImport === undefined) {
fileTypeImport = import('file-type');
}
if (fileTypes === undefined) {
fileTypes = await fileTypeImport;
}
const { fileTypeFromFile, supportedExtensions } = fileTypes;
const extension = extname(path);
if (!supportedExtensions.has(extension)) {
const mimetype = lookup(extension);
if (mimetype === false) {
throw new Error('Unable to resolve mime-type');
}
return mimetype;
}
const location = this.prefixer.prefixFilePath(path);
const result = await fileTypeFromFile(location);
if (result === undefined) {
throw new Error('Unable to resolve mime-type');
}
return result.mime;
}
async fileSize(path) {
const stat = await this.stat(path);
if (!stat.isFile) {
throw new Error(`Path ${path} is not a file.`);
}
if (stat.size === undefined) {
throw new Error('Stat unexpectedly did not return file size.');
}
return stat.size;
}
async lastModified(path) {
const stat = await this.stat(path);
if (!stat.isFile) {
throw new Error(`Path ${path} is not a file.`);
}
if (stat.lastModifiedMs === undefined) {
throw new Error('Stat unexpectedly did not return last modified.');
}
return stat.lastModifiedMs;
}
async *list(path, { deep }) {
let entries = await opendir(this.prefixer.prefixDirectoryPath(path), {
recursive: deep,
});
for await (const item of entries) {
const itemPath = posix.join(item.parentPath, item.name);
yield this.mapStatToEntry(item, item.isFile()
? this.prefixer.stripFilePath(itemPath)
: this.prefixer.stripDirectoryPath(itemPath));
}
}
async read(path) {
return createReadStream(this.prefixer.prefixFilePath(path));
}
async write(path, contents, options) {
await this.ensureRootDirectoryExists();
await this.ensureParentDirectoryExists(path, options);
const writeStream = createWriteStream(this.prefixer.prefixFilePath(path), {
flags: 'w+',
mode: options.visibility
? this.visibilityConversion.visibilityToFilePermissions(options.visibility)
: undefined,
});
await pipeline(contents, writeStream);
}
async deleteFile(path) {
await rm(this.prefixer.prefixFilePath(path), {
force: true,
});
}
async createDirectory(path, options) {
await mkdir(this.prefixer.prefixDirectoryPath(path), {
recursive: true,
mode: options.directoryVisibility
? this.visibilityConversion.visibilityToDirectoryPermissions(options.directoryVisibility)
: undefined,
});
}
async stat(path, type = 'file') {
return this.mapStatToEntry(await stat(type === 'file'
? this.prefixer.prefixFilePath(path)
: this.prefixer.prefixDirectoryPath(path)), path);
}
async fileExists(path) {
try {
const stat = await this.stat(path);
return stat.isFile;
}
catch (e) {
if (typeof e === 'object' && e.code === 'ENOENT') {
return false;
}
throw e;
}
}
async deleteDirectory(path) {
await rm(this.prefixer.prefixDirectoryPath(path), {
recursive: true,
force: true,
});
}
mapStatToEntry(info, path) {
if (!info.isFile() && !info.isDirectory()) {
throw new Error('Unsupported file entry encountered...');
}
const isDirent = info instanceof Dirent;
return info.isFile() ? {
path,
type: 'file',
isFile: true,
isDirectory: false,
visibility: isDirent ? undefined : this.visibilityConversion.filePermissionsToVisibility(info.mode & 0o777),
lastModifiedMs: isDirent ? undefined : info.mtimeMs,
size: isDirent ? undefined : info.size,
} : {
path,
type: 'directory',
isFile: false,
isDirectory: true,
visibility: isDirent ? undefined : this.visibilityConversion.directoryPermissionsToVisibility(info.mode & 0o777),
lastModifiedMs: isDirent ? undefined : info.mtimeMs,
};
}
async changeVisibility(path, visibility) {
await chmod(this.prefixer.prefixFilePath(path), this.visibilityConversion.visibilityToFilePermissions(visibility));
}
async visibility(path) {
const stat = await this.stat(path);
if (!stat.visibility) {
throw new Error('Unable to determine visibility');
}
return stat.visibility;
}
async directoryExists(path) {
try {
const stat = await this.stat(path, 'directory');
return stat.isDirectory;
}
catch (e) {
if (typeof e === 'object' && ['ENOTDIR', 'ENOENT'].includes(e.code)) {
return false;
}
throw e;
}
}
rootDirectoryCreation = undefined;
async ensureRootDirectoryExists() {
if (this.rootDirectoryCreation === undefined) {
this.rootDirectoryCreation = this.createDirectory('', {
directoryVisibility: this.options.rootDirectoryVisibility ?? this.visibilityConversion.defaultDirectoryVisibility,
});
}
return await this.rootDirectoryCreation;
}
async ensureParentDirectoryExists(path, options) {
const directoryName = posix.dirname(path);
if (directoryName !== '.' && directoryName !== '/') {
await this.createDirectory(directoryName, {
directoryVisibility: options.directoryVisibility,
});
}
}
async checksum(path, options) {
return checksumFromStream(await this.read(path), options);
}
}
/**
* BC export
*
* @deprecated
*/
export class LocalFileStorage extends LocalStorageAdapter {
}