fs-zoo
Version:
File system abstractions and implementations
339 lines (338 loc) • 13.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.NodeCrud = void 0;
const util_1 = require("memfs/lib/node-to-fsa/util");
const util_2 = require("../crud/util");
const util_3 = require("../fsa-to-crud/util");
class NodeCrud {
constructor(options) {
this.options = options;
this.put = async (path, data, options) => {
const [collection, id] = (0, util_2.parseId)(path);
(0, util_2.assertType)(collection, 'put', 'crudfs');
(0, util_1.assertName)(id, 'put', 'crudfs');
const dir = this.dir + (collection.length ? collection.join(this.separator) + this.separator : '');
const fs = this.fs;
if (dir.length > 1)
await fs.mkdir(dir, { recursive: true });
const filename = dir + id;
if (typeof data === 'undefined') {
const throwIf = options?.throwIf;
// Determine existence and type
let exists = false;
try {
const st = await fs.stat(filename);
exists = st.isDirectory() ? 'dir' : st.isFile() ? 'file' : false;
}
catch (e) {
if (!(e && typeof e === 'object' && e.code === 'ENOENT'))
throw e;
}
switch (throwIf) {
case 'exists': {
if (exists)
throw (0, util_3.newExistsError)();
await fs.mkdir(filename, { recursive: true });
return;
}
case 'missing': {
if (exists !== 'dir')
throw (0, util_3.newMissingError)();
return; // directory already there
}
default: {
if (!exists) {
await fs.mkdir(filename, { recursive: true });
}
else if (exists === 'file') {
// cannot replace file with directory under default behavior
throw (0, util_3.newExistsError)();
}
return;
}
}
}
const throwIf = options?.throwIf;
let pos = options?.pos;
if (pos === void 0) {
if (throwIf) {
try {
const stats = await fs.stat(filename);
if (throwIf === 'exists')
throw (0, util_3.newExistsError)();
if (!stats.isFile())
throw (0, util_1.newNotAllowedError)();
}
catch (error) {
if (error && typeof error === 'object' && error.code === 'ENOENT') {
if (throwIf === 'missing')
throw (0, util_3.newMissingError)();
}
else
throw error;
}
}
await fs.writeFile(filename, data);
return;
}
let handle;
try {
if (typeof pos !== 'number')
throw new Error(`Invalid position: ${pos}`);
// Use string flags compatible with both native fs and memfs.
if (throwIf === 'exists') {
// Create new file exclusively; fail if it already exists.
try {
handle = await fs.open(filename, 'wx');
}
catch (error) {
if (error && typeof error === 'object' && error.code === 'EEXIST')
throw (0, util_3.newExistsError)();
throw error;
}
}
else if (throwIf === 'missing') {
// Open existing file for read/write; fail if missing.
try {
handle = await fs.open(filename, 'r+');
}
catch (error) {
if (error && typeof error === 'object' && error.code === 'ENOENT')
throw (0, util_3.newMissingError)();
throw error;
}
}
else {
// Default: open for read/write; if missing, create then reopen without truncation.
try {
handle = await fs.open(filename, 'r+');
}
catch (error) {
if (error && typeof error === 'object' && error.code === 'ENOENT') {
await fs.writeFile(filename, new Uint8Array());
handle = await fs.open(filename, 'r+');
}
else {
throw error;
}
}
}
if (pos === -1) {
// Append: compute size without changing file content or position semantics
const stats = await handle.stat();
pos = stats.size;
}
await handle.write(data, 0, data.byteLength, pos);
}
finally {
await handle?.close();
}
};
this.getStream = async (path) => {
const [collection, id] = (0, util_2.parseId)(path);
try {
const handle = await this._file(collection, id, 0 /* FLAG.O_RDONLY */);
const stream = handle.readableWebStream();
// const reader = await stream.getReader();
// console.log(await reader.read());
// console.log(stream);
return stream;
}
catch (error) {
if (error && typeof error === 'object') {
switch (error.code) {
case 'ENOENT':
throw (0, util_3.newFile404Error)(collection, id);
}
}
throw error;
}
};
this.del = async (path, silent) => {
const [collection, id] = (0, util_2.parseId)(path);
(0, util_2.assertType)(collection, 'del', 'crudfs');
(0, util_1.assertName)(id, 'del', 'crudfs');
try {
const dir = await this._dir(collection);
const filename = dir + id;
await this.fs.unlink(filename);
}
catch (error) {
if (!!silent)
return;
if (error && typeof error === 'object') {
switch (error.code) {
case 'ENOENT':
throw (0, util_3.newFile404Error)(collection, id);
}
}
throw error;
}
};
this.info = async (path) => {
const [collection, id] = (0, util_2.parseId)(path);
const isRootPath = !collection.length && !id;
if (!isRootPath) {
(0, util_2.assertType)(collection, 'info', 'crudfs');
(0, util_1.assertName)(id, 'info', 'crudfs');
}
await this._dir(collection);
if (isRootPath) {
return {
type: 'collection',
id: '',
readable: true,
};
}
try {
// Build base dir path without introducing a double slash when collection is empty
const base = this.dir + (collection.length ? collection.join(this.separator) + this.separator : '');
const fullPath = base + id;
const stats = await this.fs.stat(fullPath);
// Access mode constants (Node: F_OK=0, X_OK=1, W_OK=2, R_OK=4)
const R_OK = 4;
const check = async (mode) => {
try {
await this.fs.access(fullPath, mode);
return true;
}
catch {
return false;
}
};
if (stats.isFile()) {
// Only perform a non-destructive readability check; some adapters may
// implement write-access checks in a destructive way.
const readable = await check(R_OK);
return {
type: 'resource',
id,
size: stats.size,
modified: stats.mtimeMs,
readable,
};
}
else if (stats.isDirectory()) {
const readable = await check(R_OK);
return {
type: 'collection',
id: '',
readable,
};
}
else {
throw (0, util_3.newMissingError)();
}
}
catch (error) {
if (error && typeof error === 'object') {
switch (error.code) {
case 'ENOENT':
throw (0, util_3.newFile404Error)(collection, id);
}
}
throw error;
}
};
this.drop = async (path, silent) => {
const collection = (0, util_2.parseParts)(path);
(0, util_2.assertType)(collection, 'drop', 'crudfs');
try {
const dir = await this._dir(collection);
const isRoot = dir === this.dir;
if (isRoot) {
const list = (await this.fs.readdir(dir));
for (const entry of list)
await this.fs.rmdir(dir + entry, { recursive: true });
}
else {
await this.fs.rmdir(dir, { recursive: true });
}
}
catch (error) {
if (!silent)
throw error;
}
};
this.scan = async function* (path) {
const collection = (0, util_2.parseParts)(path);
(0, util_2.assertType)(collection, 'scan', 'crudfs');
const dir = await this._dir(collection);
const dirents = (await this.fs.readdir(dir, { withFileTypes: true }));
for (const entry of dirents) {
if (entry.isFile()) {
yield {
type: 'resource',
id: '' + entry.name,
};
}
else if (entry.isDirectory()) {
yield {
type: 'collection',
id: '' + entry.name,
};
}
}
};
this.list = async (path) => {
const entries = [];
for await (const entry of this.scan(path))
entries.push(entry);
return entries;
};
this.from = async (path) => {
const collection = (0, util_2.parseParts)(path);
(0, util_2.assertType)(collection, 'from', 'crudfs');
const dir = this.dir + (collection.length ? collection.join(this.separator) + this.separator : '');
const fs = this.fs;
if (dir.length > 1)
await fs.mkdir(dir, { recursive: true });
await this._dir(collection);
return new NodeCrud({
dir,
fs: this.fs,
separator: this.separator,
});
};
this.separator = options.separator ?? '/';
let dir = options.dir;
const last = dir[dir.length - 1];
if (last !== this.separator)
dir = dir + this.separator;
this.dir = dir;
this.fs = options.fs;
}
async _dir(collection) {
const dir = this.dir + (collection.length ? collection.join(this.separator) + this.separator : '');
// Avoid statting the root directory when collection is empty; some adapters
// (e.g., FSA-backed) don't accept '/'
if (!collection.length)
return dir;
const fs = this.fs;
try {
const stats = await fs.stat(dir);
if (!stats.isDirectory())
throw (0, util_3.newFolder404Error)(collection);
return dir;
}
catch (error) {
if (error && typeof error === 'object') {
switch (error.code) {
case 'ENOENT':
case 'ENOTDIR':
throw (0, util_3.newFolder404Error)(collection);
}
}
throw error;
}
}
async _file(collection, id, flags) {
(0, util_2.assertType)(collection, 'get', 'crudfs');
(0, util_1.assertName)(id, 'get', 'crudfs');
const dir = await this._dir(collection);
const filename = dir + id;
const fs = this.fs;
return await fs.open(filename, flags);
}
}
exports.NodeCrud = NodeCrud;