From 0a43a1b586cb551c3ce861f7cfbf0876cabc8852 Mon Sep 17 00:00:00 2001 From: streamich Date: Sat, 17 Jun 2023 18:07:56 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20implement=20access()=20m?= =?UTF-8?q?ethod?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/consts/AMODE.ts | 11 +++ src/fsa-to-node/FsaNodeFs.ts | 84 +++++++++++++++--- src/fsa-to-node/FsaNodeFsOpenFile.ts | 13 ++- src/fsa-to-node/__tests__/FsaNodeFs.test.ts | 94 ++++++++++++++++++++- src/fsa-to-node/util.ts | 15 ++++ 5 files changed, 201 insertions(+), 16 deletions(-) create mode 100644 src/consts/AMODE.ts diff --git a/src/consts/AMODE.ts b/src/consts/AMODE.ts new file mode 100644 index 000000000..bbfd5bde7 --- /dev/null +++ b/src/consts/AMODE.ts @@ -0,0 +1,11 @@ +// Constants used in `access` system call, see [access(2)](http://man7.org/linux/man-pages/man2/faccessat.2.html). +export const enum AMODE { + /* Tests for the existence of the file. */ + F_OK = 0, + /** Tests for Execute or Search permissions. */ + X_OK = 1, + /** Tests for Write permission. */ + W_OK = 2, + /** Tests for Read permission. */ + R_OK = 4, +} diff --git a/src/fsa-to-node/FsaNodeFs.ts b/src/fsa-to-node/FsaNodeFs.ts index 304c6d526..e28b0c671 100644 --- a/src/fsa-to-node/FsaNodeFs.ts +++ b/src/fsa-to-node/FsaNodeFs.ts @@ -23,7 +23,7 @@ import { validateCallback, validateFd, } from '../node/util'; -import { pathToLocation } from './util'; +import { pathToLocation, testDirectoryIsWritable } from './util'; import { ERRSTR, MODE } from '../node/constants'; import { strToEncoding } from '../encoding'; import { FsaToNodeConstants } from './constants'; @@ -31,6 +31,7 @@ import { bufferToEncoding } from '../volume'; import { FsaNodeFsOpenFile } from './FsaNodeFsOpenFile'; import { FsaNodeDirent } from './FsaNodeDirent'; import {FLAG} from '../consts/FLAG'; +import {AMODE} from '../consts/AMODE'; import type { FsCallbackApi, FsPromisesApi } from '../node/types'; import type * as misc from '../node/types/misc'; import type * as opts from '../node/types/options'; @@ -86,6 +87,18 @@ export class FsaNodeFs implements FsCallbackApi { return file; } + private async getFileOrDir(path: string[], name: string, funcName?: string, create?: boolean): Promise { + const dir = await this.getDir(path, false, funcName); + try { + const file = await dir.getFileHandle(name); + return file; + } catch (error) { + if (error && typeof error === 'object' && error.name === 'TypeMismatchError') + return await dir.getDirectoryHandle(name); + throw error; + } + } + private async getFileByFd(fd: number, funcName?: string): Promise { if (!isFd(fd)) throw TypeError(ERRSTR.FD); const file = this.fds.get(fd); @@ -108,6 +121,9 @@ export class FsaNodeFs implements FsCallbackApi { return await dir.getFileHandle(name, { create: true }); } + + // ------------------------------------------------------------ FsCallbackApi + public readonly open: FsCallbackApi['open'] = ( path: misc.PathLike, flags: misc.TFlags, @@ -289,15 +305,58 @@ export class FsaNodeFs implements FsCallbackApi { throw new Error('Not implemented'); } - exists(path: misc.PathLike, callback: (exists: boolean) => void): void { - throw new Error('Not implemented'); - } + public readonly exists: FsCallbackApi['exists'] = (path: misc.PathLike, callback: (exists: boolean) => void): void => { + const filename = pathToFilename(path); + if (typeof callback !== 'function') throw Error(ERRSTR.CB); + const [folder, name] = pathToLocation(filename); + (async () => { + // const stats = await new Promise(); + })().then(() => callback(true), () => callback(false)); + }; - access(path: misc.PathLike, callback: misc.TCallback); - access(path: misc.PathLike, mode: number, callback: misc.TCallback); - access(path: misc.PathLike, a: misc.TCallback | number, b?: misc.TCallback) { - throw new Error('Not implemented'); - } + public readonly access: FsCallbackApi['access'] = (path: misc.PathLike, a: misc.TCallback | number, b?: misc.TCallback) => { + let mode: number = AMODE.F_OK; + let callback: misc.TCallback; + if (typeof a !== 'function') { + mode = a | 0; // cast to number + callback = validateCallback(b); + } else { + callback = a; + } + const filename = pathToFilename(path); + const [folder, name] = pathToLocation(filename); + (async () => { + const node = await this.getFileOrDir(folder, name, 'access'); + const checkIfCanExecute = mode & AMODE.X_OK; + if (checkIfCanExecute) throw createError('EACCESS', 'access', filename); + const checkIfCanWrite = mode & AMODE.W_OK; + switch (node.kind) { + case 'file': { + if (checkIfCanWrite) { + try { + const file = node as fsa.IFileSystemFileHandle; + const writable = await file.createWritable(); + await writable.close(); + } catch { + throw createError('EACCESS', 'access', filename); + } + } + break; + } + case 'directory': { + if (checkIfCanWrite) { + const dir = node as fsa.IFileSystemDirectoryHandle; + const canWrite = await testDirectoryIsWritable(dir); + if (!canWrite) throw createError('EACCESS', 'access', filename); + } + break; + } + default: { + throw createError('EACCESS', 'access', filename); + } + } + })().then(() => callback(null), error => callback(error)); + }; public readonly appendFile: FsCallbackApi['appendFile'] = (id: misc.TFileId, data: misc.TData, a, b?) => { const [opts, callback] = getAppendFileOptsAndCb(a, b); @@ -307,8 +366,11 @@ export class FsaNodeFs implements FsCallbackApi { (async () => { const blob = await file.getFile(); const writable = await file.createWritable({ keepExistingData: true }); - await writable.seek(blob.size); - await writable.write(buffer); + await writable.write({ + type: 'write', + data: buffer, + position: blob.size, + }); await writable.close(); })(), ) diff --git a/src/fsa-to-node/FsaNodeFsOpenFile.ts b/src/fsa-to-node/FsaNodeFsOpenFile.ts index 0b9afa631..0a949529d 100644 --- a/src/fsa-to-node/FsaNodeFsOpenFile.ts +++ b/src/fsa-to-node/FsaNodeFsOpenFile.ts @@ -1,6 +1,11 @@ +import {FLAG} from '../consts/FLAG'; import type * as fsa from '../fsa/types'; import type * as misc from '../node/types/misc'; +/** + * Represents an open file. Stores additional metadata about the open file, such + * as the seek position. + */ export class FsaNodeFsOpenFile { protected seek: number = 0; @@ -10,14 +15,16 @@ export class FsaNodeFsOpenFile { * with which flags the file was opened. On subsequent writes we want to * append to the file. */ - protected keepExistingData: boolean = false; + protected keepExistingData: boolean; public constructor( public readonly fd: number, - public readonly mode: misc.TMode, + public readonly createMode: misc.TMode, public readonly flags: number, public readonly file: fsa.IFileSystemFileHandle, - ) {} + ) { + this.keepExistingData = !!(flags & FLAG.O_APPEND); + } public async close(): Promise {} diff --git a/src/fsa-to-node/__tests__/FsaNodeFs.test.ts b/src/fsa-to-node/__tests__/FsaNodeFs.test.ts index 4be53b421..14254cda4 100644 --- a/src/fsa-to-node/__tests__/FsaNodeFs.test.ts +++ b/src/fsa-to-node/__tests__/FsaNodeFs.test.ts @@ -1,11 +1,12 @@ import { IFsWithVolume, NestedDirectoryJSON, memfs } from '../..'; +import {AMODE} from '../../consts/AMODE'; import { nodeToFsa } from '../../node-to-fsa'; import { IDirent } from '../../node/types/misc'; import { FsaNodeFs } from '../FsaNodeFs'; -const setup = (json: NestedDirectoryJSON | null = null) => { +const setup = (json: NestedDirectoryJSON | null = null, mode: 'read' | 'readwrite' = 'readwrite') => { const mfs = memfs({ mountpoint: json }) as IFsWithVolume; - const dir = nodeToFsa(mfs, '/mountpoint', { mode: 'readwrite', syncHandleAllowed: true }); + const dir = nodeToFsa(mfs, '/mountpoint', { mode, syncHandleAllowed: true }); const fs = new FsaNodeFs(dir); return { fs, mfs, dir }; }; @@ -351,3 +352,92 @@ describe('.write()', () => { expect(mfs.readFileSync('/mountpoint/test.txt', 'utf8')).toBe('abc'); }); }); + +describe('.exists()', () => { + test('can works for folders and files', async () => { + // const { fs, mfs } = setup({ folder: { file: 'test' }, 'empty-folder': null, 'f.html': 'test' }); + // const exists = async (path: string): Promise => { + // return new Promise((resolve) => { + // fs.exists(path, (exists) => resolve(exists)); + // }); + // }; + // expect(await exists('/folder')).toBe(true); + + }); +}); + +describe('.access()', () => { + describe('files', () => { + test('succeeds on file existence check', async () => { + const { fs, mfs } = setup({ folder: { file: 'test' }, 'empty-folder': null, 'f.html': 'test' }); + await fs.promises.access('/folder/file', AMODE.F_OK); + }); + + test('succeeds on file "read" check', async () => { + const { fs, mfs } = setup({ folder: { file: 'test' }, 'empty-folder': null, 'f.html': 'test' }); + await fs.promises.access('/folder/file', AMODE.R_OK); + }); + + test('succeeds on file "write" check, on writable file system', async () => { + const { fs, mfs } = setup({ folder: { file: 'test' }, 'empty-folder': null, 'f.html': 'test' }); + await fs.promises.access('/folder/file', AMODE.W_OK); + }); + + test('fails on file "write" check, on read-only file system', async () => { + const { fs, mfs } = setup({ folder: { file: 'test' }, 'empty-folder': null, 'f.html': 'test' }, 'read'); + try { + await fs.promises.access('/folder/file', AMODE.W_OK); + throw new Error('should not be here') + } catch (error) { + expect(error.code).toBe('EACCESS'); + } + }); + + test('fails on file "execute" check', async () => { + const { fs, mfs } = setup({ folder: { file: 'test' }, 'empty-folder': null, 'f.html': 'test' }); + try { + await fs.promises.access('/folder/file', AMODE.X_OK); + throw new Error('should not be here') + } catch (error) { + expect(error.code).toBe('EACCESS'); + } + }); + }); + + describe('directories', () => { + test('succeeds on folder existence check', async () => { + const { fs, mfs } = setup({ folder: { file: 'test' }, 'empty-folder': null, 'f.html': 'test' }); + await fs.promises.access('/folder', AMODE.F_OK); + }); + + test('succeeds on folder "read" check', async () => { + const { fs, mfs } = setup({ folder: { file: 'test' }, 'empty-folder': null, 'f.html': 'test' }); + await fs.promises.access('/folder', AMODE.R_OK); + }); + + test('succeeds on folder "write" check, on writable file system', async () => { + const { fs, mfs } = setup({ folder: { file: 'test' }, 'empty-folder': null, 'f.html': 'test' }); + await fs.promises.access('/folder', AMODE.W_OK); + }); + + test('fails on folder "write" check, on read-only file system', async () => { + const { fs, mfs } = setup({ folder: { file: 'test' }, 'empty-folder': null, 'f.html': 'test' }, 'read'); + try { + await fs.promises.access('/folder', AMODE.W_OK); + throw new Error('should not be here') + } catch (error) { + expect(error.code).toBe('EACCESS'); + } + }); + + test('fails on folder "execute" check', async () => { + const { fs, mfs } = setup({ folder: { file: 'test' }, 'empty-folder': null, 'f.html': 'test' }); + try { + await fs.promises.access('/folder', AMODE.X_OK); + throw new Error('should not be here') + } catch (error) { + expect(error.code).toBe('EACCESS'); + } + }); + }); +}); diff --git a/src/fsa-to-node/util.ts b/src/fsa-to-node/util.ts index a74794274..47acc0228 100644 --- a/src/fsa-to-node/util.ts +++ b/src/fsa-to-node/util.ts @@ -1,3 +1,4 @@ +import {IFileSystemDirectoryHandle} from '../fsa/types'; import { FsaToNodeConstants } from './constants'; import type { FsLocation } from './types'; @@ -9,3 +10,17 @@ export const pathToLocation = (path: string): FsLocation => { const folder = path.slice(0, lastSlashIndex).split(FsaToNodeConstants.Separator); return [folder, file]; }; + +export const testDirectoryIsWritable = async (dir: IFileSystemDirectoryHandle): Promise => { + const testFileName = '__memfs_writable_test_file_' + Math.random().toString(36).slice(2) + Date.now(); + try { + await dir.getFileHandle(testFileName, { create: true }); + return true; + } catch { + return false; + } finally { + try { + await dir.removeEntry(testFileName); + } catch (e) {} + } +};