Skip to content

Commit

Permalink
feat: 🎸 add read/write mode separation
Browse files Browse the repository at this point in the history
  • Loading branch information
streamich committed Jun 15, 2023
1 parent b9c4edd commit e00165f
Show file tree
Hide file tree
Showing 11 changed files with 91 additions and 7 deletions.
2 changes: 1 addition & 1 deletion docs/fsa.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ of any folder on your filesystem:
```js
import { nodeToFsa } from 'memfs/lib/node-to-fsa';

const dir = nodeToFsa(fs, '/path/to/folder');
const dir = nodeToFsa(fs, '/path/to/folder', {mode: 'readwrite'});
```

The `fs` Node filesystem API can be the real `fs` module or any, for example,
Expand Down
4 changes: 4 additions & 0 deletions src/node-to-fsa/NodeFileSystemDirectoryHandle.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { NodeFileSystemHandle } from './NodeFileSystemHandle';
import {
assertCanWrite,
assertName,
basename,
ctx as createCtx,
Expand Down Expand Up @@ -84,6 +85,7 @@ export class NodeFileSystemDirectoryHandle extends NodeFileSystemHandle implemen
switch (error.code) {
case 'ENOENT': {
if (options && options.create) {
assertCanWrite(this.ctx.mode!);
await this.fs.promises.mkdir(filename);
return new NodeFileSystemDirectoryHandle(this.fs, filename, this.ctx);
}
Expand Down Expand Up @@ -120,6 +122,7 @@ export class NodeFileSystemDirectoryHandle extends NodeFileSystemHandle implemen
switch (error.code) {
case 'ENOENT': {
if (options && options.create) {
assertCanWrite(this.ctx.mode!);
await this.fs.promises.writeFile(filename, '');
return new NodeFileSystemFileHandle(this.fs, filename, this.ctx);
}
Expand All @@ -144,6 +147,7 @@ export class NodeFileSystemDirectoryHandle extends NodeFileSystemHandle implemen
* @param options An optional object containing options.
*/
public async removeEntry(name: string, { recursive = false }: RemoveEntryOptions = {}): Promise<void> {
assertCanWrite(this.ctx.mode!);
assertName(name, 'removeEntry', 'FileSystemDirectoryHandle');
const filename = this.__path + this.ctx.separator! + name;
const promises = this.fs.promises;
Expand Down
3 changes: 2 additions & 1 deletion src/node-to-fsa/NodeFileSystemFileHandle.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NodeFileSystemHandle } from './NodeFileSystemHandle';
import { NodeFileSystemSyncAccessHandle } from './NodeFileSystemSyncAccessHandle';
import { basename, ctx as createCtx, newNotAllowedError } from './util';
import { assertCanWrite, basename, ctx as createCtx, newNotAllowedError } from './util';
import { NodeFileSystemWritableFileStream } from './NodeFileSystemWritableFileStream';
import type { NodeFsaContext, NodeFsaFs } from './types';
import type {IFileSystemFileHandle, IFileSystemSyncAccessHandle} from '../fsa/types';
Expand Down Expand Up @@ -55,6 +55,7 @@ export class NodeFileSystemFileHandle extends NodeFileSystemHandle implements IF
public async createWritable(
{ keepExistingData = false }: CreateWritableOptions = { keepExistingData: false },
): Promise<NodeFileSystemWritableFileStream> {
assertCanWrite(this.ctx.mode);
return new NodeFileSystemWritableFileStream(this.fs, this.__path, keepExistingData);
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/node-to-fsa/NodeFileSystemSyncAccessHandle.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {assertCanWrite} from './util';
import type {FileSystemReadWriteOptions, IFileSystemSyncAccessHandle} from '../fsa/types';
import type { NodeFsaContext, NodeFsaFs } from './types';

Expand All @@ -19,13 +20,15 @@ export class NodeFileSystemSyncAccessHandle implements IFileSystemSyncAccessHand
* @see https://developer.mozilla.org/en-US/docs/Web/API/FileSystemSyncAccessHandle/close
*/
public async close(): Promise<void> {
assertCanWrite(this.ctx.mode);
this.fs.closeSync(this.fd);
}

/**
* @see https://developer.mozilla.org/en-US/docs/Web/API/FileSystemSyncAccessHandle/flush
*/
public async flush(): Promise<void> {
assertCanWrite(this.ctx.mode);
this.fs.fsyncSync(this.fd);
}

Expand Down Expand Up @@ -62,6 +65,7 @@ export class NodeFileSystemSyncAccessHandle implements IFileSystemSyncAccessHand
* @param newSize The number of bytes to resize the file to.
*/
public async truncate(newSize: number): Promise<void> {
assertCanWrite(this.ctx.mode);
this.fs.truncateSync(this.fd, newSize);
}

Expand All @@ -77,6 +81,7 @@ export class NodeFileSystemSyncAccessHandle implements IFileSystemSyncAccessHand
buffer: ArrayBuffer | ArrayBufferView | DataView,
options: FileSystemReadWriteOptions = {},
): Promise<number> {
assertCanWrite(this.ctx.mode);
const buf: Buffer | ArrayBufferView = buffer instanceof ArrayBuffer ? Buffer.from(buffer) : buffer;
try {
return this.fs.writeSync(this.fd, buf, 0, buffer.byteLength, options.at || 0);
Expand Down
54 changes: 53 additions & 1 deletion src/node-to-fsa/__tests__/NodeFileSystemDirectoryHandle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { maybe } from './util';

const setup = (json: DirectoryJSON = {}) => {
const fs = memfs(json, '/');
const dir = new NodeFileSystemDirectoryHandle(fs as any, '/');
const dir = new NodeFileSystemDirectoryHandle(fs as any, '/', {mode: 'readwrite'});
return { dir, fs };
};

Expand Down Expand Up @@ -159,6 +159,19 @@ maybe('NodeFileSystemDirectoryHandle', () => {
}
});

test('throws if not in "readwrite" mode and attempting to create a directory', async () => {
const fs = memfs({}, '/');
const dir = new NodeFileSystemDirectoryHandle(fs as any, '/', {mode: 'read'});
try {
await dir.getDirectoryHandle('test', { create: true });
throw new Error('Not this error');
} catch (error) {
expect(error).toBeInstanceOf(DOMException);
expect(error.name).toBe('NotAllowedError');
expect(error.message).toBe('The request is not allowed by the user agent or the platform in the current context.');
}
});

const invalidNames = [
'.',
'..',
Expand Down Expand Up @@ -239,6 +252,19 @@ maybe('NodeFileSystemDirectoryHandle', () => {
}
});

test('throws if not in "readwrite" mode and attempting to create a file', async () => {
const fs = memfs({}, '/');
const dir = new NodeFileSystemDirectoryHandle(fs as any, '/', {mode: 'read'});
try {
await dir.getFileHandle('test', { create: true });
throw new Error('Not this error');
} catch (error) {
expect(error).toBeInstanceOf(DOMException);
expect(error.name).toBe('NotAllowedError');
expect(error.message).toBe('The request is not allowed by the user agent or the platform in the current context.');
}
});

const invalidNames = [
'.',
'..',
Expand Down Expand Up @@ -307,6 +333,32 @@ maybe('NodeFileSystemDirectoryHandle', () => {
}
});

test('throws if not in "readwrite" mode and attempting to remove a file', async () => {
const fs = memfs({ a: 'b'}, '/');
const dir = new NodeFileSystemDirectoryHandle(fs as any, '/', {mode: 'read'});
try {
await dir.removeEntry('a');
throw new Error('Not this error');
} catch (error) {
expect(error).toBeInstanceOf(DOMException);
expect(error.name).toBe('NotAllowedError');
expect(error.message).toBe('The request is not allowed by the user agent or the platform in the current context.');
}
});

test('throws if not in "readwrite" mode and attempting to remove a folder', async () => {
const fs = memfs({ a: null}, '/');
const dir = new NodeFileSystemDirectoryHandle(fs as any, '/', {mode: 'read'});
try {
await dir.removeEntry('a');
throw new Error('Not this error');
} catch (error) {
expect(error).toBeInstanceOf(DOMException);
expect(error.name).toBe('NotAllowedError');
expect(error.message).toBe('The request is not allowed by the user agent or the platform in the current context.');
}
});

const invalidNames = [
'.',
'..',
Expand Down
16 changes: 15 additions & 1 deletion src/node-to-fsa/__tests__/NodeFileSystemFileHandle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { maybe } from './util';

const setup = (json: DirectoryJSON = {}) => {
const fs = memfs(json, '/') as IFsWithVolume;
const dir = new NodeFileSystemDirectoryHandle(fs as any, '/');
const dir = new NodeFileSystemDirectoryHandle(fs as any, '/', {mode: 'readwrite'});
return { dir, fs };
};

Expand All @@ -26,6 +26,20 @@ maybe('NodeFileSystemFileHandle', () => {
});

describe('.createWritable()', () => {
test('throws if not in "readwrite" mode', async () => {
const fs = memfs({ 'file.txt': 'abc' }, '/') as IFsWithVolume;
const dir = new NodeFileSystemDirectoryHandle(fs as any, '/', {mode: 'read'});
const entry = await dir.getFileHandle('file.txt');
try {
await entry.createWritable();
throw new Error('Not this error');
} catch (error) {
expect(error).toBeInstanceOf(DOMException);
expect(error.name).toBe('NotAllowedError');
expect(error.message).toBe('The request is not allowed by the user agent or the platform in the current context.');
}
});

describe('.truncate()', () => {
test('can truncate file', async () => {
const { dir, fs } = setup({
Expand Down
2 changes: 1 addition & 1 deletion src/node-to-fsa/__tests__/NodeFileSystemHandle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { maybe } from './util';

const setup = (json: DirectoryJSON = {}) => {
const fs = memfs(json, '/');
const dir = new NodeFileSystemDirectoryHandle(fs as any, '/');
const dir = new NodeFileSystemDirectoryHandle(fs as any, '/', {mode: 'readwrite'});
return { dir, fs };
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { maybe } from './util';

const setup = (json: DirectoryJSON = {}) => {
const fs = memfs(json, '/');
const dir = new NodeFileSystemDirectoryHandle(fs as any, '/', { syncHandleAllowed: true });
const dir = new NodeFileSystemDirectoryHandle(fs as any, '/', { syncHandleAllowed: true, mode: 'readwrite' });
return { dir, fs };
};

Expand Down
2 changes: 1 addition & 1 deletion src/node-to-fsa/__tests__/scenarios.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ maybe('scenarios', () => {
'/bin': null,
'/Users/kasper/Documents/shopping-list.txt': 'Milk, Eggs, Bread',
}) as IFsWithVolume;
const dir = nodeToFsa(fs, '/Users/kasper/Documents');
const dir = nodeToFsa(fs, '/Users/kasper/Documents', {mode: 'readwrite'});
const shoppingListFile = await dir.getFileHandle('shopping-list.txt');
const shoppingList = await shoppingListFile.getFile();
expect(await shoppingList.text()).toBe('Milk, Eggs, Bread');
Expand Down
2 changes: 2 additions & 0 deletions src/node-to-fsa/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,6 @@ export interface NodeFsaContext {
separator: '/' | '\\';
/** Whether synchronous file handles are allowed. */
syncHandleAllowed: boolean;
/** Whether writes are allowed, defaults to `read`. */
mode: 'read' | 'readwrite';
}
6 changes: 6 additions & 0 deletions src/node-to-fsa/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const ctx = (partial: Partial<NodeFsaContext> = {}): NodeFsaContext => {
return {
separator: '/',
syncHandleAllowed: false,
mode: 'read',
...partial,
};
};
Expand All @@ -23,6 +24,11 @@ export const assertName = (name: string, method: string, klass: string) => {
if (isInvalid) throw new TypeError(`Failed to execute '${method}' on '${klass}': Name is not allowed.`);
};

export const assertCanWrite = (mode: 'read' | 'readwrite') => {
if (mode !== 'readwrite')
throw new DOMException('The request is not allowed by the user agent or the platform in the current context.', 'NotAllowedError');
};

export const newNotFoundError = () =>
new DOMException(
'A requested file or directory could not be found at the time an operation was processed.',
Expand Down

0 comments on commit e00165f

Please sign in to comment.