Skip to content

Commit 20f8883

Browse files
committed
feat: 🎸 add more methods
1 parent 675e301 commit 20f8883

File tree

2 files changed

+249
-18
lines changed

2 files changed

+249
-18
lines changed

src/nfs/v4/client/Nfsv4FsClient.ts

Lines changed: 134 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -374,47 +374,163 @@ export class Nfsv4FsClient implements NfsFsClient {
374374
return entries;
375375
}
376376

377-
public readonly appendFile = (path: misc.TFileHandle, data: misc.TData, options?: opts.IAppendFileOptions | string): Promise<void> => {
378-
throw new Error('Not implemented.');
379-
};
377+
public async appendFile(path: misc.TFileHandle, data: misc.TData, options?: opts.IAppendFileOptions | string): Promise<void> {
378+
const pathStr = typeof path === 'string' ? path : path.toString();
379+
const parts = this.parsePath(pathStr);
380+
const operations: msg.Nfsv4Request[] = [nfs.PUTROOTFH()];
381+
for (const part of parts.slice(0, -1)) {
382+
operations.push(nfs.LOOKUP(part));
383+
}
384+
const filename = parts[parts.length - 1];
385+
const openOwner = nfs.OpenOwner(BigInt(1), new Uint8Array([1, 2, 3, 4]));
386+
const claim = nfs.OpenClaimNull(filename);
387+
operations.push(
388+
nfs.OPEN(
389+
0,
390+
Nfsv4OpenAccess.OPEN4_SHARE_ACCESS_WRITE,
391+
Nfsv4OpenDeny.OPEN4_SHARE_DENY_NONE,
392+
openOwner,
393+
0,
394+
claim,
395+
),
396+
);
397+
const attrNums = [Nfsv4Attr.FATTR4_SIZE];
398+
const attrMask = this.attrNumsToBitmap(attrNums);
399+
operations.push(nfs.GETATTR(attrMask));
400+
const openResponse = await this.nfs.compound(operations);
401+
if (openResponse.status !== Nfsv4Stat.NFS4_OK) {
402+
throw new Error(`Failed to open file: ${openResponse.status}`);
403+
}
404+
const openRes = openResponse.resarray[openResponse.resarray.length - 2] as msg.Nfsv4OpenResponse;
405+
if (openRes.status !== Nfsv4Stat.NFS4_OK || !openRes.resok) {
406+
throw new Error(`Failed to open file: ${openRes.status}`);
407+
}
408+
const getattrRes = openResponse.resarray[openResponse.resarray.length - 1] as msg.Nfsv4GetattrResponse;
409+
if (getattrRes.status !== Nfsv4Stat.NFS4_OK || !getattrRes.resok) {
410+
throw new Error(`Failed to get attributes: ${getattrRes.status}`);
411+
}
412+
const fattr = getattrRes.resok.objAttributes;
413+
const reader = new Reader();
414+
reader.reset(fattr.attrVals);
415+
const xdr = new XdrDecoder(reader);
416+
const currentSize = Number(xdr.readUnsignedHyper());
417+
const openStateid = openRes.resok.stateid;
418+
const buffer = this.encodeData(data);
419+
const chunkSize = 65536;
420+
try {
421+
let offset = BigInt(currentSize);
422+
for (let i = 0; i < buffer.length; i += chunkSize) {
423+
const chunk = buffer.slice(i, Math.min(i + chunkSize, buffer.length));
424+
const writeResponse = await this.nfs.compound([
425+
nfs.WRITE(openStateid, offset, Nfsv4StableHow.FILE_SYNC4, chunk),
426+
]);
427+
if (writeResponse.status !== Nfsv4Stat.NFS4_OK) {
428+
throw new Error(`Failed to write file: ${writeResponse.status}`);
429+
}
430+
const writeRes = writeResponse.resarray[0] as msg.Nfsv4WriteResponse;
431+
if (writeRes.status !== Nfsv4Stat.NFS4_OK || !writeRes.resok) {
432+
throw new Error(`Failed to write file: ${writeRes.status}`);
433+
}
434+
offset += BigInt(writeRes.resok.count);
435+
}
436+
} finally {
437+
await this.nfs.compound([nfs.CLOSE(0, openStateid)]);
438+
}
439+
}
380440

381-
public readonly access = (path: misc.PathLike, mode?: number): Promise<void> => {
382-
throw new Error('Not implemented.');
383-
};
441+
public async truncate(path: misc.PathLike, len: number = 0): Promise<void> {
442+
const pathStr = typeof path === 'string' ? path : path.toString();
443+
const parts = this.parsePath(pathStr);
444+
const operations: msg.Nfsv4Request[] = [nfs.PUTROOTFH()];
445+
for (const part of parts) {
446+
operations.push(nfs.LOOKUP(part));
447+
}
448+
const writer = new Writer(16);
449+
const xdr = new XdrEncoder(writer);
450+
xdr.writeUnsignedHyper(BigInt(len));
451+
const attrVals = writer.flush();
452+
const sizeAttrs = nfs.Fattr([Nfsv4Attr.FATTR4_SIZE], attrVals);
453+
const stateid = nfs.Stateid(0, new Uint8Array(12));
454+
operations.push(nfs.SETATTR(stateid, sizeAttrs));
455+
const response = await this.nfs.compound(operations);
456+
if (response.status !== Nfsv4Stat.NFS4_OK) {
457+
throw new Error(`Failed to truncate file: ${response.status}`);
458+
}
459+
const setattrRes = response.resarray[response.resarray.length - 1] as msg.Nfsv4SetattrResponse;
460+
if (setattrRes.status !== Nfsv4Stat.NFS4_OK) {
461+
throw new Error(`Failed to truncate file: ${setattrRes.status}`);
462+
}
463+
}
384464

385-
public readonly copyFile = (src: misc.PathLike, dest: misc.PathLike, flags?: misc.TFlagsCopy): Promise<void> => {
386-
throw new Error('Not implemented.');
387-
};
465+
public async unlink(path: misc.PathLike): Promise<void> {
466+
const pathStr = typeof path === 'string' ? path : path.toString();
467+
const parts = this.parsePath(pathStr);
468+
if (parts.length === 0) {
469+
throw new Error('Cannot unlink root directory');
470+
}
471+
const operations: msg.Nfsv4Request[] = [nfs.PUTROOTFH()];
472+
for (const part of parts.slice(0, -1)) {
473+
operations.push(nfs.LOOKUP(part));
474+
}
475+
const filename = parts[parts.length - 1];
476+
operations.push(nfs.REMOVE(filename));
477+
const response = await this.nfs.compound(operations);
478+
if (response.status !== Nfsv4Stat.NFS4_OK) {
479+
throw new Error(`Failed to unlink file: ${response.status}`);
480+
}
481+
const removeRes = response.resarray[response.resarray.length - 1] as msg.Nfsv4RemoveResponse;
482+
if (removeRes.status !== Nfsv4Stat.NFS4_OK) {
483+
throw new Error(`Failed to unlink file: ${removeRes.status}`);
484+
}
485+
}
388486

389-
public readonly link = (existingPath: misc.PathLike, newPath: misc.PathLike): Promise<void> => {
390-
throw new Error('Not implemented.');
391-
};
487+
public async rmdir(path: misc.PathLike, options?: opts.IRmdirOptions): Promise<void> {
488+
const pathStr = typeof path === 'string' ? path : path.toString();
489+
const parts = this.parsePath(pathStr);
490+
if (parts.length === 0) {
491+
throw new Error('Cannot remove root directory');
492+
}
493+
const operations: msg.Nfsv4Request[] = [nfs.PUTROOTFH()];
494+
for (const part of parts.slice(0, -1)) {
495+
operations.push(nfs.LOOKUP(part));
496+
}
497+
const dirname = parts[parts.length - 1];
498+
operations.push(nfs.REMOVE(dirname));
499+
const response = await this.nfs.compound(operations);
500+
if (response.status !== Nfsv4Stat.NFS4_OK) {
501+
throw new Error(`Failed to remove directory: ${response.status}`);
502+
}
503+
const removeRes = response.resarray[response.resarray.length - 1] as msg.Nfsv4RemoveResponse;
504+
if (removeRes.status !== Nfsv4Stat.NFS4_OK) {
505+
throw new Error(`Failed to remove directory: ${removeRes.status}`);
506+
}
507+
}
392508

393-
public readonly realpath = (path: misc.PathLike, options?: opts.IRealpathOptions | string): Promise<misc.TDataOut> => {
509+
public readonly access = (path: misc.PathLike, mode?: number): Promise<void> => {
394510
throw new Error('Not implemented.');
395511
};
396512

397513
public readonly rename = (oldPath: misc.PathLike, newPath: misc.PathLike): Promise<void> => {
398514
throw new Error('Not implemented.');
399515
};
400516

401-
public readonly rmdir = (path: misc.PathLike, options?: opts.IRmdirOptions): Promise<void> => {
517+
public readonly copyFile = (src: misc.PathLike, dest: misc.PathLike, flags?: misc.TFlagsCopy): Promise<void> => {
402518
throw new Error('Not implemented.');
403519
};
404520

405-
public readonly truncate = (path: misc.PathLike, len?: number): Promise<void> => {
521+
public readonly realpath = (path: misc.PathLike, options?: opts.IRealpathOptions | string): Promise<misc.TDataOut> => {
406522
throw new Error('Not implemented.');
407523
};
408524

409-
public readonly unlink = (path: misc.PathLike): Promise<void> => {
525+
public readonly link = (existingPath: misc.PathLike, newPath: misc.PathLike): Promise<void> => {
410526
throw new Error('Not implemented.');
411527
};
412528

413-
public readonly utimes = (path: misc.PathLike, atime: misc.TTime, mtime: misc.TTime): Promise<void> => {
529+
public readonly symlink = (target: misc.PathLike, path: misc.PathLike, type?: misc.symlink.Type): Promise<void> => {
414530
throw new Error('Not implemented.');
415531
};
416532

417-
public readonly symlink = (target: misc.PathLike, path: misc.PathLike, type?: misc.symlink.Type): Promise<void> => {
533+
public readonly utimes = (path: misc.PathLike, atime: misc.TTime, mtime: misc.TTime): Promise<void> => {
418534
throw new Error('Not implemented.');
419535
};
420536

src/nfs/v4/client/__tests__/Nfsv4FsClient.spec.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,3 +165,118 @@ describe('.readdir()', () => {
165165
await stop();
166166
});
167167
});
168+
169+
describe('.truncate()', () => {
170+
test('can truncate file to zero', async () => {
171+
const {client, stop} = await setupNfsClientServerTestbed();
172+
const fs = new Nfsv4FsClient(client);
173+
await fs.truncate('file.txt', 0);
174+
const stats = await fs.stat('file.txt');
175+
expect(stats.size).toBe(0);
176+
await stop();
177+
});
178+
179+
test('can truncate file to specific size', async () => {
180+
const {client, stop} = await setupNfsClientServerTestbed();
181+
const fs = new Nfsv4FsClient(client);
182+
await fs.truncate('file.txt', 5);
183+
const stats = await fs.stat('file.txt');
184+
expect(stats.size).toBe(5);
185+
const content = await fs.readFile('file.txt', 'utf8');
186+
expect(content).toBe('Hello');
187+
await stop();
188+
});
189+
190+
test('can truncate nested file', async () => {
191+
const {client, stop} = await setupNfsClientServerTestbed();
192+
const fs = new Nfsv4FsClient(client);
193+
await fs.truncate('subdir/nested.dat', 6);
194+
const stats = await fs.stat('subdir/nested.dat');
195+
expect(stats.size).toBe(6);
196+
await stop();
197+
});
198+
});
199+
200+
describe('.appendFile()', () => {
201+
test('can append text to file', async () => {
202+
const {client, stop} = await setupNfsClientServerTestbed();
203+
const fs = new Nfsv4FsClient(client);
204+
await fs.appendFile('file.txt', ' Appended!');
205+
const content = await fs.readFile('file.txt', 'utf8');
206+
expect(content).toBe('Hello, NFS v4!\n Appended!');
207+
await stop();
208+
});
209+
210+
test('can append buffer to file', async () => {
211+
const {client, stop} = await setupNfsClientServerTestbed();
212+
const fs = new Nfsv4FsClient(client);
213+
const data = Buffer.from(' More data');
214+
await fs.appendFile('file.txt', data);
215+
const content = await fs.readFile('file.txt', 'utf8');
216+
expect(content).toBe('Hello, NFS v4!\n More data');
217+
await stop();
218+
});
219+
220+
test('can append to nested file', async () => {
221+
const {client, stop} = await setupNfsClientServerTestbed();
222+
const fs = new Nfsv4FsClient(client);
223+
await fs.appendFile('subdir/nested.dat', '+++');
224+
const content = await fs.readFile('subdir/nested.dat', 'utf8');
225+
expect(content).toBe('nested data+++');
226+
await stop();
227+
});
228+
});
229+
230+
describe('.unlink()', () => {
231+
test('can delete a file', async () => {
232+
const {client, stop, vol} = await setupNfsClientServerTestbed();
233+
const fs = new Nfsv4FsClient(client);
234+
await fs.unlink('file.txt');
235+
expect(vol.existsSync('/export/file.txt')).toBe(false);
236+
await stop();
237+
});
238+
239+
test('can delete nested file', async () => {
240+
const {client, stop, vol} = await setupNfsClientServerTestbed();
241+
const fs = new Nfsv4FsClient(client);
242+
await fs.unlink('subdir/nested.dat');
243+
expect(vol.existsSync('/export/subdir/nested.dat')).toBe(false);
244+
await stop();
245+
});
246+
247+
test('throws error when deleting non-existent file', async () => {
248+
const {client, stop} = await setupNfsClientServerTestbed();
249+
const fs = new Nfsv4FsClient(client);
250+
await expect(fs.unlink('nonexistent.txt')).rejects.toThrow();
251+
await stop();
252+
});
253+
});
254+
255+
describe('.rmdir()', () => {
256+
test('can remove empty directory', async () => {
257+
const {client, stop, vol} = await setupNfsClientServerTestbed();
258+
const fs = new Nfsv4FsClient(client);
259+
expect(vol.existsSync('/export/emptydir')).toBe(false);
260+
await fs.mkdir('emptydir');
261+
expect(vol.existsSync('/export/emptydir')).toBe(true);
262+
await fs.rmdir('emptydir');
263+
expect(vol.existsSync('/export/emptydir')).toBe(false);
264+
await stop();
265+
});
266+
267+
test('can remove nested directory', async () => {
268+
const {client, stop, vol} = await setupNfsClientServerTestbed();
269+
const fs = new Nfsv4FsClient(client);
270+
await fs.mkdir('subdir/newsubdir');
271+
await fs.rmdir('subdir/newsubdir');
272+
expect(vol.existsSync('/export/subdir/newsubdir')).toBe(false);
273+
await stop();
274+
});
275+
276+
test('throws error when removing non-empty directory', async () => {
277+
const {client, stop} = await setupNfsClientServerTestbed();
278+
const fs = new Nfsv4FsClient(client);
279+
await expect(fs.rmdir('subdir')).rejects.toThrow();
280+
await stop();
281+
});
282+
});

0 commit comments

Comments
 (0)