From 60a65c1d835dd49a18ef8dfafa75cedb62f05f5a Mon Sep 17 00:00:00 2001 From: streamich Date: Thu, 15 Jun 2023 20:50:22 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20add=20read/write=20mode?= =?UTF-8?q?=20separation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/fsa.md | 2 +- .../NodeFileSystemDirectoryHandle.ts | 4 ++ src/node-to-fsa/NodeFileSystemFileHandle.ts | 3 +- .../NodeFileSystemSyncAccessHandle.ts | 5 ++ .../NodeFileSystemDirectoryHandle.test.ts | 54 ++++++++++++++++++- .../NodeFileSystemFileHandle.test.ts | 16 +++++- .../__tests__/NodeFileSystemHandle.test.ts | 2 +- .../NodeFileSystemSyncAccessHandle.test.ts | 2 +- src/node-to-fsa/__tests__/scenarios.test.ts | 2 +- src/node-to-fsa/types.ts | 2 + src/node-to-fsa/util.ts | 6 +++ 11 files changed, 91 insertions(+), 7 deletions(-) diff --git a/docs/fsa.md b/docs/fsa.md index a85f2b738..df7282022 100644 --- a/docs/fsa.md +++ b/docs/fsa.md @@ -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, diff --git a/src/node-to-fsa/NodeFileSystemDirectoryHandle.ts b/src/node-to-fsa/NodeFileSystemDirectoryHandle.ts index ee7e758ab..45499fc19 100644 --- a/src/node-to-fsa/NodeFileSystemDirectoryHandle.ts +++ b/src/node-to-fsa/NodeFileSystemDirectoryHandle.ts @@ -1,5 +1,6 @@ import { NodeFileSystemHandle } from './NodeFileSystemHandle'; import { + assertCanWrite, assertName, basename, ctx as createCtx, @@ -84,6 +85,7 @@ export class NodeFileSystemDirectoryHandle extends NodeFileSystemHandle implemen switch (error.code) { case 'ENOENT': { if (options?.create) { + assertCanWrite(this.ctx.mode!); await this.fs.promises.mkdir(filename); return new NodeFileSystemDirectoryHandle(this.fs, filename, this.ctx); } @@ -120,6 +122,7 @@ export class NodeFileSystemDirectoryHandle extends NodeFileSystemHandle implemen switch (error.code) { case 'ENOENT': { if (options?.create) { + assertCanWrite(this.ctx.mode!); await this.fs.promises.writeFile(filename, ''); return new NodeFileSystemFileHandle(this.fs, filename, this.ctx); } @@ -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 { + assertCanWrite(this.ctx.mode!); assertName(name, 'removeEntry', 'FileSystemDirectoryHandle'); const filename = this.__path + this.ctx.separator! + name; const promises = this.fs.promises; diff --git a/src/node-to-fsa/NodeFileSystemFileHandle.ts b/src/node-to-fsa/NodeFileSystemFileHandle.ts index 3c608421e..29cb22cfd 100644 --- a/src/node-to-fsa/NodeFileSystemFileHandle.ts +++ b/src/node-to-fsa/NodeFileSystemFileHandle.ts @@ -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'; @@ -55,6 +55,7 @@ export class NodeFileSystemFileHandle extends NodeFileSystemHandle implements IF public async createWritable( { keepExistingData = false }: CreateWritableOptions = { keepExistingData: false }, ): Promise { + assertCanWrite(this.ctx.mode); return new NodeFileSystemWritableFileStream(this.fs, this.__path, keepExistingData); } } diff --git a/src/node-to-fsa/NodeFileSystemSyncAccessHandle.ts b/src/node-to-fsa/NodeFileSystemSyncAccessHandle.ts index 0111f931c..ad7738936 100644 --- a/src/node-to-fsa/NodeFileSystemSyncAccessHandle.ts +++ b/src/node-to-fsa/NodeFileSystemSyncAccessHandle.ts @@ -1,3 +1,4 @@ +import {assertCanWrite} from './util'; import type {FileSystemReadWriteOptions, IFileSystemSyncAccessHandle} from '../fsa/types'; import type { NodeFsaContext, NodeFsaFs } from './types'; @@ -19,6 +20,7 @@ export class NodeFileSystemSyncAccessHandle implements IFileSystemSyncAccessHand * @see https://developer.mozilla.org/en-US/docs/Web/API/FileSystemSyncAccessHandle/close */ public async close(): Promise { + assertCanWrite(this.ctx.mode); this.fs.closeSync(this.fd); } @@ -26,6 +28,7 @@ export class NodeFileSystemSyncAccessHandle implements IFileSystemSyncAccessHand * @see https://developer.mozilla.org/en-US/docs/Web/API/FileSystemSyncAccessHandle/flush */ public async flush(): Promise { + assertCanWrite(this.ctx.mode); this.fs.fsyncSync(this.fd); } @@ -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 { + assertCanWrite(this.ctx.mode); this.fs.truncateSync(this.fd, newSize); } @@ -77,6 +81,7 @@ export class NodeFileSystemSyncAccessHandle implements IFileSystemSyncAccessHand buffer: ArrayBuffer | ArrayBufferView | DataView, options: FileSystemReadWriteOptions = {}, ): Promise { + 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); diff --git a/src/node-to-fsa/__tests__/NodeFileSystemDirectoryHandle.test.ts b/src/node-to-fsa/__tests__/NodeFileSystemDirectoryHandle.test.ts index ca203bca0..703e555cf 100644 --- a/src/node-to-fsa/__tests__/NodeFileSystemDirectoryHandle.test.ts +++ b/src/node-to-fsa/__tests__/NodeFileSystemDirectoryHandle.test.ts @@ -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 }; }; @@ -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 = [ '.', '..', @@ -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 = [ '.', '..', @@ -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 = [ '.', '..', diff --git a/src/node-to-fsa/__tests__/NodeFileSystemFileHandle.test.ts b/src/node-to-fsa/__tests__/NodeFileSystemFileHandle.test.ts index 640b5fbde..d927619cb 100644 --- a/src/node-to-fsa/__tests__/NodeFileSystemFileHandle.test.ts +++ b/src/node-to-fsa/__tests__/NodeFileSystemFileHandle.test.ts @@ -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 }; }; @@ -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({ diff --git a/src/node-to-fsa/__tests__/NodeFileSystemHandle.test.ts b/src/node-to-fsa/__tests__/NodeFileSystemHandle.test.ts index 6a8d66495..bc782fea1 100644 --- a/src/node-to-fsa/__tests__/NodeFileSystemHandle.test.ts +++ b/src/node-to-fsa/__tests__/NodeFileSystemHandle.test.ts @@ -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 }; }; diff --git a/src/node-to-fsa/__tests__/NodeFileSystemSyncAccessHandle.test.ts b/src/node-to-fsa/__tests__/NodeFileSystemSyncAccessHandle.test.ts index 29bf5f173..1b776f329 100644 --- a/src/node-to-fsa/__tests__/NodeFileSystemSyncAccessHandle.test.ts +++ b/src/node-to-fsa/__tests__/NodeFileSystemSyncAccessHandle.test.ts @@ -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 }; }; diff --git a/src/node-to-fsa/__tests__/scenarios.test.ts b/src/node-to-fsa/__tests__/scenarios.test.ts index 4d70a3daa..dc8cd8744 100644 --- a/src/node-to-fsa/__tests__/scenarios.test.ts +++ b/src/node-to-fsa/__tests__/scenarios.test.ts @@ -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'); diff --git a/src/node-to-fsa/types.ts b/src/node-to-fsa/types.ts index 8bb68c51e..95c903a41 100644 --- a/src/node-to-fsa/types.ts +++ b/src/node-to-fsa/types.ts @@ -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'; } diff --git a/src/node-to-fsa/util.ts b/src/node-to-fsa/util.ts index 0a48b9962..2f23b8518 100644 --- a/src/node-to-fsa/util.ts +++ b/src/node-to-fsa/util.ts @@ -7,6 +7,7 @@ export const ctx = (partial: Partial = {}): NodeFsaContext => { return { separator: '/', syncHandleAllowed: false, + mode: 'read', ...partial, }; }; @@ -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.',