Skip to content

Commit 06bd6b1

Browse files
Copilotstreamich
andcommitted
feat: implement queryPermission method in NodeFileSystemHandle using fs.access
Co-authored-by: streamich <[email protected]>
1 parent 904e541 commit 06bd6b1

File tree

4 files changed

+83
-24
lines changed

4 files changed

+83
-24
lines changed

src/fsa-to-node/FsaNodeFs.ts

Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -404,34 +404,25 @@ export class FsaNodeFs extends FsaNodeCore implements FsCallbackApi, FsSynchrono
404404
const [folder, name] = pathToLocation(filename);
405405
(async () => {
406406
const node = folder.length || name ? await this.getFileOrDir(folder, name, 'access') : await this.root;
407+
408+
// Check execute permission - not supported by FSA
407409
const checkIfCanExecute = mode & AMODE.X_OK;
408410
if (checkIfCanExecute) throw util.createError('EACCESS', 'access', filename);
411+
412+
// Use queryPermission to check read/write access
413+
const checkIfCanRead = mode & AMODE.R_OK;
409414
const checkIfCanWrite = mode & AMODE.W_OK;
410-
switch (node.kind) {
411-
case 'file': {
412-
if (checkIfCanWrite) {
413-
try {
414-
const file = node as fsa.IFileSystemFileHandle;
415-
const writable = await file.createWritable();
416-
await writable.close();
417-
} catch {
418-
throw util.createError('EACCESS', 'access', filename);
419-
}
420-
}
421-
break;
422-
}
423-
case 'directory': {
424-
if (checkIfCanWrite) {
425-
const dir = node as fsa.IFileSystemDirectoryHandle;
426-
const canWrite = await testDirectoryIsWritable(dir);
427-
if (!canWrite) throw util.createError('EACCESS', 'access', filename);
428-
}
429-
break;
430-
}
431-
default: {
415+
416+
if (checkIfCanRead || checkIfCanWrite) {
417+
const permissionMode = checkIfCanWrite ? 'readwrite' : 'read';
418+
const permission = node.queryPermission({ mode: permissionMode });
419+
420+
if (permission.state === 'denied') {
432421
throw util.createError('EACCESS', 'access', filename);
433422
}
434423
}
424+
425+
// If only F_OK is requested, we already verified the file exists by getting the node
435426
})().then(
436427
() => callback(null),
437428
error => callback(error),

src/node-to-fsa/NodeFileSystemHandle.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { NodePermissionStatus } from './NodePermissionStatus';
2+
import { AMODE } from '../consts/AMODE';
23
import type { IFileSystemHandle, FileSystemHandlePermissionDescriptor } from '../fsa/types';
4+
import type { NodeFsaFs } from './types';
35

46
/**
57
* Represents a File System Access API file handle `FileSystemHandle` object,
@@ -8,6 +10,9 @@ import type { IFileSystemHandle, FileSystemHandlePermissionDescriptor } from '..
810
* @see [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/API/FileSystemHandle)
911
*/
1012
export abstract class NodeFileSystemHandle implements IFileSystemHandle {
13+
protected abstract readonly fs: NodeFsaFs;
14+
protected abstract readonly __path: string;
15+
1116
constructor(
1217
public readonly kind: 'file' | 'directory',
1318
public readonly name: string,
@@ -30,7 +35,26 @@ export abstract class NodeFileSystemHandle implements IFileSystemHandle {
3035
public queryPermission(
3136
fileSystemHandlePermissionDescriptor: FileSystemHandlePermissionDescriptor,
3237
): NodePermissionStatus {
33-
throw new Error('Not implemented');
38+
const { mode } = fileSystemHandlePermissionDescriptor;
39+
40+
try {
41+
// Use Node.js fs.access() to check permissions synchronously
42+
let accessMode = AMODE.F_OK;
43+
44+
if (mode === 'read') {
45+
accessMode = AMODE.R_OK;
46+
} else if (mode === 'readwrite') {
47+
accessMode = AMODE.R_OK | AMODE.W_OK;
48+
}
49+
50+
// Use synchronous access check
51+
this.fs.accessSync(this.__path, accessMode);
52+
53+
return new NodePermissionStatus(mode, 'granted');
54+
} catch (error) {
55+
// If access check fails, permission is denied
56+
return new NodePermissionStatus(mode, 'denied');
57+
}
3458
}
3559

3660
/**

src/node-to-fsa/__tests__/NodeFileSystemHandle.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,48 @@ onlyOnNode20('NodeFileSystemHandle', () => {
4040
expect(dir.isSameEntry(file)).toBe(false);
4141
});
4242
});
43+
44+
describe('.queryPermission()', () => {
45+
test('grants read permission for existing files', async () => {
46+
const { dir } = setup({
47+
'test.txt': 'content',
48+
});
49+
const file = await dir.getFileHandle('test.txt');
50+
const permission = file.queryPermission({ mode: 'read' });
51+
expect(permission.state).toBe('granted');
52+
expect(permission.name).toBe('read');
53+
});
54+
55+
test('grants readwrite permission for files when context allows', async () => {
56+
const { dir } = setup({
57+
'test.txt': 'content',
58+
});
59+
const file = await dir.getFileHandle('test.txt');
60+
const permission = file.queryPermission({ mode: 'readwrite' });
61+
expect(permission.state).toBe('granted');
62+
expect(permission.name).toBe('readwrite');
63+
});
64+
65+
test('grants read permission for existing directories', async () => {
66+
const { dir } = setup({
67+
subdir: null,
68+
});
69+
const subdir = await dir.getDirectoryHandle('subdir');
70+
const permission = subdir.queryPermission({ mode: 'read' });
71+
expect(permission.state).toBe('granted');
72+
expect(permission.name).toBe('read');
73+
});
74+
75+
test('denies permission for non-existent paths', async () => {
76+
const { fs } = setup();
77+
const nonExistentFile = new (await import('../NodeFileSystemFileHandle')).NodeFileSystemFileHandle(
78+
fs as any,
79+
'/nonexistent.txt',
80+
{ mode: 'readwrite' }
81+
);
82+
const permission = nonExistentFile.queryPermission({ mode: 'read' });
83+
expect(permission.state).toBe('denied');
84+
expect(permission.name).toBe('read');
85+
});
86+
});
4387
});

src/node-to-fsa/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type { FsCommonObjects } from '../node/types/FsCommonObjects';
66
*/
77
export type NodeFsaFs = Pick<FsCommonObjects, 'constants'> & { promises: FsPromisesApi } & Pick<
88
FsSynchronousApi,
9-
'openSync' | 'fsyncSync' | 'statSync' | 'closeSync' | 'readSync' | 'truncateSync' | 'writeSync'
9+
'openSync' | 'fsyncSync' | 'statSync' | 'closeSync' | 'readSync' | 'truncateSync' | 'writeSync' | 'accessSync'
1010
>;
1111

1212
export interface NodeFsaContext {

0 commit comments

Comments
 (0)