Skip to content

Commit 5370d19

Browse files
committed
feat: 🎸 add FileHandle stub
1 parent 2161af2 commit 5370d19

File tree

2 files changed

+280
-2
lines changed

2 files changed

+280
-2
lines changed
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
import type * as misc from 'memfs/lib/node/types/misc';
2+
import type * as opts from 'memfs/lib/node/types/options';
3+
import type {Nfsv4Client} from './types';
4+
import {EventEmitter} from 'events';
5+
import * as msg from '../messages';
6+
import * as structs from '../structs';
7+
import {Nfsv4Stat, Nfsv4Attr} from '../constants';
8+
import {Reader} from '@jsonjoy.com/buffers/lib/Reader';
9+
import {XdrDecoder} from '../../../xdr/XdrDecoder';
10+
import {NfsFsStats} from './NfsFsStats';
11+
12+
/**
13+
* Implements Node.js-like FileHandle interface for NFS v4 file operations.
14+
*/
15+
export class NfsFsFileHandle extends EventEmitter implements misc.IFileHandle {
16+
public readonly fd: number;
17+
private closed: boolean = false;
18+
19+
constructor(
20+
fd: number,
21+
public readonly path: string,
22+
private readonly nfs: Nfsv4Client,
23+
private readonly stateid: structs.Nfsv4Stateid,
24+
private readonly operations: msg.Nfsv4Request[],
25+
) {
26+
super();
27+
this.fd = fd;
28+
}
29+
30+
getAsyncId(): number {
31+
return this.fd;
32+
}
33+
34+
async close(): Promise<void> {
35+
if (this.closed) return;
36+
this.closed = true;
37+
const {nfs} = require('../builder');
38+
const closeOps: msg.Nfsv4Request[] = [nfs.CLOSE(0, this.stateid)];
39+
const response = await this.nfs.compound(closeOps);
40+
if (response.status !== Nfsv4Stat.NFS4_OK) {
41+
throw new Error(`Failed to close file: ${response.status}`);
42+
}
43+
this.emit('close');
44+
}
45+
46+
async stat(options?: opts.IStatOptions): Promise<misc.IStats> {
47+
if (this.closed) throw new Error('File handle is closed');
48+
const {nfs} = require('../builder');
49+
const operations = [...this.operations];
50+
const attrNums = [
51+
Nfsv4Attr.FATTR4_TYPE,
52+
Nfsv4Attr.FATTR4_SIZE,
53+
Nfsv4Attr.FATTR4_FILEID,
54+
Nfsv4Attr.FATTR4_MODE,
55+
Nfsv4Attr.FATTR4_NUMLINKS,
56+
Nfsv4Attr.FATTR4_SPACE_USED,
57+
Nfsv4Attr.FATTR4_TIME_ACCESS,
58+
Nfsv4Attr.FATTR4_TIME_MODIFY,
59+
Nfsv4Attr.FATTR4_TIME_METADATA,
60+
];
61+
const attrMask: number[] = [];
62+
for (const attrNum of attrNums) {
63+
const wordIndex = Math.floor(attrNum / 32);
64+
const bitIndex = attrNum % 32;
65+
while (attrMask.length <= wordIndex) attrMask.push(0);
66+
attrMask[wordIndex] |= 1 << bitIndex;
67+
}
68+
operations.push(nfs.GETATTR(attrMask));
69+
const response = await this.nfs.compound(operations);
70+
if (response.status !== Nfsv4Stat.NFS4_OK) {
71+
throw new Error(`Failed to stat file: ${response.status}`);
72+
}
73+
const getattrRes = response.resarray[response.resarray.length - 1] as msg.Nfsv4GetattrResponse;
74+
if (getattrRes.status !== Nfsv4Stat.NFS4_OK || !getattrRes.resok) {
75+
throw new Error(`Failed to get attributes: ${getattrRes.status}`);
76+
}
77+
const fattr = getattrRes.resok.objAttributes;
78+
const reader = new Reader();
79+
reader.reset(fattr.attrVals);
80+
const xdr = new XdrDecoder(reader);
81+
const returnedMask = fattr.attrmask.mask;
82+
let fileType = 1;
83+
let size = 0;
84+
let fileid = 0;
85+
let mode = 0;
86+
let nlink = 1;
87+
let spaceUsed = 0;
88+
let atime = new Date(0);
89+
let mtime = new Date(0);
90+
let ctime = new Date(0);
91+
for (let i = 0; i < returnedMask.length; i++) {
92+
const word = returnedMask[i];
93+
if (!word) continue;
94+
for (let bit = 0; bit < 32; bit++) {
95+
if (!(word & (1 << bit))) continue;
96+
const attrNum = i * 32 + bit;
97+
switch (attrNum) {
98+
case Nfsv4Attr.FATTR4_TYPE:
99+
fileType = xdr.readUnsignedInt();
100+
break;
101+
case Nfsv4Attr.FATTR4_SIZE:
102+
size = Number(xdr.readUnsignedHyper());
103+
break;
104+
case Nfsv4Attr.FATTR4_FILEID:
105+
fileid = Number(xdr.readUnsignedHyper());
106+
break;
107+
case Nfsv4Attr.FATTR4_MODE:
108+
mode = xdr.readUnsignedInt();
109+
break;
110+
case Nfsv4Attr.FATTR4_NUMLINKS:
111+
nlink = xdr.readUnsignedInt();
112+
break;
113+
case Nfsv4Attr.FATTR4_SPACE_USED:
114+
spaceUsed = Number(xdr.readUnsignedHyper());
115+
break;
116+
case Nfsv4Attr.FATTR4_TIME_ACCESS: {
117+
const seconds = Number(xdr.readHyper());
118+
const nseconds = xdr.readUnsignedInt();
119+
atime = new Date(seconds * 1000 + nseconds / 1000000);
120+
break;
121+
}
122+
case Nfsv4Attr.FATTR4_TIME_MODIFY: {
123+
const seconds = Number(xdr.readHyper());
124+
const nseconds = xdr.readUnsignedInt();
125+
mtime = new Date(seconds * 1000 + nseconds / 1000000);
126+
break;
127+
}
128+
case Nfsv4Attr.FATTR4_TIME_METADATA: {
129+
const seconds = Number(xdr.readHyper());
130+
const nseconds = xdr.readUnsignedInt();
131+
ctime = new Date(seconds * 1000 + nseconds / 1000000);
132+
break;
133+
}
134+
}
135+
}
136+
}
137+
const blocks = Math.ceil(spaceUsed / 512);
138+
return new NfsFsStats(
139+
0,
140+
0,
141+
0,
142+
4096,
143+
fileid,
144+
size,
145+
blocks,
146+
atime,
147+
mtime,
148+
ctime,
149+
mtime,
150+
atime.getTime(),
151+
mtime.getTime(),
152+
ctime.getTime(),
153+
mtime.getTime(),
154+
0,
155+
mode,
156+
nlink,
157+
fileType,
158+
);
159+
}
160+
161+
appendFile(data: misc.TData, options?: opts.IAppendFileOptions | string): Promise<void> {
162+
throw new Error('Not implemented');
163+
}
164+
165+
chmod(mode: misc.TMode): Promise<void> {
166+
throw new Error('Not implemented');
167+
}
168+
169+
chown(uid: number, gid: number): Promise<void> {
170+
throw new Error('Not implemented');
171+
}
172+
173+
createReadStream(options?: opts.IFileHandleReadStreamOptions): misc.IReadStream {
174+
throw new Error('Not implemented');
175+
}
176+
177+
createWriteStream(options?: opts.IFileHandleWriteStreamOptions): misc.IWriteStream {
178+
throw new Error('Not implemented');
179+
}
180+
181+
datasync(): Promise<void> {
182+
throw new Error('Not implemented');
183+
}
184+
185+
readableWebStream(options?: opts.IReadableWebStreamOptions): ReadableStream {
186+
throw new Error('Not implemented');
187+
}
188+
189+
read(
190+
buffer: Buffer | Uint8Array,
191+
offset: number,
192+
length: number,
193+
position?: number | null,
194+
): Promise<misc.TFileHandleReadResult> {
195+
throw new Error('Not implemented');
196+
}
197+
198+
readv(buffers: ArrayBufferView[], position?: number | null): Promise<misc.TFileHandleReadvResult> {
199+
throw new Error('Not implemented');
200+
}
201+
202+
readFile(options?: opts.IReadFileOptions | string): Promise<misc.TDataOut> {
203+
throw new Error('Not implemented');
204+
}
205+
206+
truncate(len?: number): Promise<void> {
207+
throw new Error('Not implemented');
208+
}
209+
210+
utimes(atime: misc.TTime, mtime: misc.TTime): Promise<void> {
211+
throw new Error('Not implemented');
212+
}
213+
214+
write(
215+
buffer: Buffer | ArrayBufferView | DataView,
216+
offset?: number,
217+
length?: number,
218+
position?: number | null,
219+
): Promise<misc.TFileHandleWriteResult> {
220+
throw new Error('Not implemented');
221+
}
222+
223+
writev(buffers: ArrayBufferView[], position?: number | null): Promise<misc.TFileHandleWritevResult> {
224+
throw new Error('Not implemented');
225+
}
226+
227+
writeFile(data: misc.TData, options?: opts.IWriteFileOptions): Promise<void> {
228+
throw new Error('Not implemented');
229+
}
230+
}

src/nfs/v4/client/Nfsv4FsClient.ts

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {XdrDecoder} from '../../../xdr/XdrDecoder';
2121
import {NfsFsStats} from './NfsFsStats';
2222
import {NfsFsDir} from './NfsFsDir';
2323
import {NfsFsDirent} from './NfsFsDirent';
24+
import {NfsFsFileHandle} from './NfsFsFileHandle';
2425

2526
export class Nfsv4FsClient implements NfsFsClient {
2627
constructor(public readonly fs: Nfsv4Client) {}
@@ -773,8 +774,55 @@ export class Nfsv4FsClient implements NfsFsClient {
773774
return this.utimes(path, atime, mtime);
774775
};
775776

776-
public readonly open = (path: misc.PathLike, flags?: misc.TFlags, mode?: misc.TMode): Promise<misc.IFileHandle> => {
777-
throw new Error('Not implemented.');
777+
public readonly open = async (
778+
path: misc.PathLike,
779+
flags?: misc.TFlags,
780+
mode?: misc.TMode,
781+
): Promise<misc.IFileHandle> => {
782+
const pathStr = typeof path === 'string' ? path : path.toString();
783+
const parts = this.parsePath(pathStr);
784+
const operations = this.navigateToParent(parts);
785+
const filename = parts[parts.length - 1];
786+
const openOwner = nfs.OpenOwner(BigInt(1), new Uint8Array([1, 2, 3, 4]));
787+
const claim = nfs.OpenClaimNull(filename);
788+
let access = Nfsv4OpenAccess.OPEN4_SHARE_ACCESS_READ;
789+
let openFlags = 0;
790+
if (typeof flags === 'string') {
791+
if (flags.includes('r') && flags.includes('+')) {
792+
access = Nfsv4OpenAccess.OPEN4_SHARE_ACCESS_BOTH;
793+
} else if (flags.includes('w') || flags.includes('a')) {
794+
access = Nfsv4OpenAccess.OPEN4_SHARE_ACCESS_WRITE;
795+
if (flags.includes('+')) {
796+
access = Nfsv4OpenAccess.OPEN4_SHARE_ACCESS_BOTH;
797+
}
798+
}
799+
} else if (typeof flags === 'number') {
800+
const O_RDONLY = 0;
801+
const O_WRONLY = 1;
802+
const O_RDWR = 2;
803+
const O_ACCMODE = 3;
804+
const accessMode = flags & O_ACCMODE;
805+
if (accessMode === O_RDONLY) {
806+
access = Nfsv4OpenAccess.OPEN4_SHARE_ACCESS_READ;
807+
} else if (accessMode === O_WRONLY) {
808+
access = Nfsv4OpenAccess.OPEN4_SHARE_ACCESS_WRITE;
809+
} else if (accessMode === O_RDWR) {
810+
access = Nfsv4OpenAccess.OPEN4_SHARE_ACCESS_BOTH;
811+
}
812+
}
813+
operations.push(nfs.OPEN(openFlags, access, Nfsv4OpenDeny.OPEN4_SHARE_DENY_NONE, openOwner, 0, claim));
814+
const openResponse = await this.fs.compound(operations);
815+
if (openResponse.status !== Nfsv4Stat.NFS4_OK) {
816+
throw new Error(`Failed to open file: ${openResponse.status}`);
817+
}
818+
const openRes = openResponse.resarray[openResponse.resarray.length - 1] as msg.Nfsv4OpenResponse;
819+
if (openRes.status !== Nfsv4Stat.NFS4_OK || !openRes.resok) {
820+
throw new Error(`Failed to open file: ${openRes.status}`);
821+
}
822+
const stateid = openRes.resok.stateid;
823+
const fd = Math.floor(Math.random() * 1000000);
824+
const fileOperations = this.navigateToPath(parts);
825+
return new NfsFsFileHandle(fd, pathStr, this.fs, stateid, fileOperations);
778826
};
779827

780828
public readonly statfs = (path: misc.PathLike, options?: opts.IStatOptions): Promise<misc.IStatFs> => {

0 commit comments

Comments
 (0)