Skip to content

Commit de20869

Browse files
committed
feat: 🎸 imlement chown and chmod operations
1 parent 8151dff commit de20869

File tree

3 files changed

+195
-19
lines changed

3 files changed

+195
-19
lines changed

src/nfs/v4/client/Nfsv4FsClient.ts

Lines changed: 55 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,10 @@ export class Nfsv4FsClient implements NfsFsClient {
294294
);
295295
}
296296

297+
public async lstat(path: misc.PathLike, options?: opts.IStatOptions): Promise<misc.IStats> {
298+
return this.stat(path, options);
299+
}
300+
297301
public async mkdir(path: misc.PathLike, options?: misc.TMode | opts.IMkdirOptions): Promise<string | undefined> {
298302
const pathStr = typeof path === 'string' ? path : path.toString();
299303
const parts = this.parsePath(pathStr);
@@ -713,31 +717,63 @@ export class Nfsv4FsClient implements NfsFsClient {
713717
return Buffer.from(dirName, 'utf8');
714718
}
715719

716-
public async lstat(path: misc.PathLike, options?: opts.IStatOptions): Promise<misc.IStats> {
717-
return this.stat(path, options);
720+
public async chmod(path: misc.PathLike, mode: misc.TMode): Promise<void> {
721+
const pathStr = typeof path === 'string' ? path : path.toString();
722+
const parts = this.parsePath(pathStr);
723+
const operations = this.navigateToPath(parts);
724+
const modeValue = typeof mode === 'number' ? mode : parseInt(mode.toString(), 8);
725+
const writer = new Writer(8);
726+
const xdr = new XdrEncoder(writer);
727+
xdr.writeUnsignedInt(modeValue);
728+
const attrVals = writer.flush();
729+
const attrs = nfs.Fattr([Nfsv4Attr.FATTR4_MODE], attrVals);
730+
const stateid = nfs.Stateid(0, new Uint8Array(12));
731+
operations.push(nfs.SETATTR(stateid, attrs));
732+
const response = await this.nfs.compound(operations);
733+
if (response.status !== Nfsv4Stat.NFS4_OK) {
734+
throw new Error(`Failed to chmod: ${response.status}`);
735+
}
736+
const setattrRes = response.resarray[response.resarray.length - 1] as msg.Nfsv4SetattrResponse;
737+
if (setattrRes.status !== Nfsv4Stat.NFS4_OK) {
738+
throw new Error(`Failed to chmod: ${setattrRes.status}`);
739+
}
718740
}
719741

720-
public readonly open = (path: misc.PathLike, flags?: misc.TFlags, mode?: misc.TMode): Promise<misc.IFileHandle> => {
721-
throw new Error('Not implemented.');
722-
};
723-
724-
public readonly chmod = (path: misc.PathLike, mode: misc.TMode): Promise<void> => {
725-
throw new Error('Not implemented.');
726-
};
742+
public async chown(path: misc.PathLike, uid: number, gid: number): Promise<void> {
743+
const pathStr = typeof path === 'string' ? path : path.toString();
744+
const parts = this.parsePath(pathStr);
745+
const operations = this.navigateToPath(parts);
746+
const writer = new Writer(64);
747+
const xdr = new XdrEncoder(writer);
748+
xdr.writeStr(uid.toString());
749+
xdr.writeStr(gid.toString());
750+
const attrVals = writer.flush();
751+
const attrs = nfs.Fattr([Nfsv4Attr.FATTR4_OWNER, Nfsv4Attr.FATTR4_OWNER_GROUP], attrVals);
752+
const stateid = nfs.Stateid(0, new Uint8Array(12));
753+
operations.push(nfs.SETATTR(stateid, attrs));
754+
const response = await this.nfs.compound(operations);
755+
if (response.status !== Nfsv4Stat.NFS4_OK) {
756+
throw new Error(`Failed to chown: ${response.status}`);
757+
}
758+
const setattrRes = response.resarray[response.resarray.length - 1] as msg.Nfsv4SetattrResponse;
759+
if (setattrRes.status !== Nfsv4Stat.NFS4_OK) {
760+
throw new Error(`Failed to chown: ${setattrRes.status}`);
761+
}
762+
}
727763

728-
public readonly chown = (path: misc.PathLike, uid: number, gid: number): Promise<void> => {
729-
throw new Error('Not implemented.');
730-
};
764+
public async lchmod(path: misc.PathLike, mode: misc.TMode): Promise<void> {
765+
return this.chmod(path, mode);
766+
}
731767

732-
public readonly lchmod = (path: misc.PathLike, mode: misc.TMode): Promise<void> => {
733-
throw new Error('Not implemented.');
734-
};
768+
public async lchown(path: misc.PathLike, uid: number, gid: number): Promise<void> {
769+
return this.chown(path, uid, gid);
770+
}
735771

736-
public readonly lchown = (path: misc.PathLike, uid: number, gid: number): Promise<void> => {
737-
throw new Error('Not implemented.');
738-
};
772+
public async lutimes(path: misc.PathLike, atime: misc.TTime, mtime: misc.TTime): Promise<void> {
773+
return this.utimes(path, atime, mtime);
774+
}
739775

740-
public readonly lutimes = (path: misc.PathLike, atime: misc.TTime, mtime: misc.TTime): Promise<void> => {
776+
public readonly open = (path: misc.PathLike, flags?: misc.TFlags, mode?: misc.TMode): Promise<misc.IFileHandle> => {
741777
throw new Error('Not implemented.');
742778
};
743779

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

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -705,3 +705,119 @@ describe('.opendir()', () => {
705705
await stop();
706706
});
707707
});
708+
709+
describe('.chmod()', () => {
710+
test('can change file mode', async () => {
711+
const {client, stop} = await setupNfsClientServerTestbed();
712+
const fs = new Nfsv4FsClient(client);
713+
await fs.chmod('file.txt', 0o644);
714+
const stats = await fs.stat('file.txt');
715+
expect(Number(stats.mode) & 0o777).toBe(0o644);
716+
await stop();
717+
});
718+
719+
test('can change directory mode', async () => {
720+
const {client, stop} = await setupNfsClientServerTestbed();
721+
const fs = new Nfsv4FsClient(client);
722+
await fs.chmod('subdir', 0o755);
723+
const stats = await fs.stat('subdir');
724+
expect(Number(stats.mode) & 0o777).toBe(0o755);
725+
await stop();
726+
});
727+
728+
test('can change nested file mode', async () => {
729+
const {client, stop} = await setupNfsClientServerTestbed();
730+
const fs = new Nfsv4FsClient(client);
731+
await fs.chmod('subdir/nested.dat', 0o600);
732+
const stats = await fs.stat('subdir/nested.dat');
733+
expect(Number(stats.mode) & 0o777).toBe(0o600);
734+
await stop();
735+
});
736+
});
737+
738+
describe('.chown()', () => {
739+
test('can change file owner', async () => {
740+
const {client, stop} = await setupNfsClientServerTestbed();
741+
const fs = new Nfsv4FsClient(client);
742+
await fs.chown('file.txt', 1001, 1001);
743+
await stop();
744+
});
745+
746+
test('can change directory owner', async () => {
747+
const {client, stop} = await setupNfsClientServerTestbed();
748+
const fs = new Nfsv4FsClient(client);
749+
await fs.chown('subdir', 1002, 1002);
750+
await stop();
751+
});
752+
753+
test('can change nested file owner', async () => {
754+
const {client, stop} = await setupNfsClientServerTestbed();
755+
const fs = new Nfsv4FsClient(client);
756+
await fs.chown('subdir/nested.dat', 1003, 1003);
757+
await stop();
758+
});
759+
});
760+
761+
describe('.lchmod()', () => {
762+
test('can change file mode without following symlinks', async () => {
763+
const {client, stop, vol} = await setupNfsClientServerTestbed();
764+
const fs = new Nfsv4FsClient(client);
765+
vol.symlinkSync('file.txt', '/export/link.txt');
766+
await fs.lchmod('link.txt', 0o777);
767+
const stats = await fs.lstat('link.txt');
768+
expect(stats.isSymbolicLink()).toBe(true);
769+
await stop();
770+
});
771+
772+
test('can change regular file mode with lchmod', async () => {
773+
const {client, stop} = await setupNfsClientServerTestbed();
774+
const fs = new Nfsv4FsClient(client);
775+
await fs.lchmod('file.txt', 0o666);
776+
const stats = await fs.stat('file.txt');
777+
expect(Number(stats.mode) & 0o777).toBe(0o666);
778+
await stop();
779+
});
780+
});
781+
782+
describe('.lchown()', () => {
783+
test('can change file owner without following symlinks', async () => {
784+
const {client, stop, vol} = await setupNfsClientServerTestbed();
785+
const fs = new Nfsv4FsClient(client);
786+
vol.symlinkSync('file.txt', '/export/link.txt');
787+
await fs.lchown('link.txt', 2001, 2001);
788+
await stop();
789+
});
790+
791+
test('can change regular file owner with lchown', async () => {
792+
const {client, stop} = await setupNfsClientServerTestbed();
793+
const fs = new Nfsv4FsClient(client);
794+
await fs.lchown('file.txt', 2002, 2002);
795+
await stop();
796+
});
797+
});
798+
799+
describe('.lutimes()', () => {
800+
test('can update symlink times without following', async () => {
801+
const {client, stop, vol} = await setupNfsClientServerTestbed();
802+
const fs = new Nfsv4FsClient(client);
803+
vol.symlinkSync('file.txt', '/export/link.txt');
804+
const now = Date.now();
805+
const atime = new Date(now - 10000);
806+
const mtime = new Date(now - 5000);
807+
await fs.lutimes('link.txt', atime, mtime);
808+
await stop();
809+
});
810+
811+
test('can update regular file times with lutimes', async () => {
812+
const {client, stop} = await setupNfsClientServerTestbed();
813+
const fs = new Nfsv4FsClient(client);
814+
const now = Date.now();
815+
const atime = new Date(now - 10000);
816+
const mtime = new Date(now - 5000);
817+
await fs.lutimes('file.txt', atime, mtime);
818+
const stats = await fs.stat('file.txt');
819+
expect(Math.abs(Number(stats.atimeMs) - atime.getTime())).toBeLessThan(2000);
820+
expect(Math.abs(Number(stats.mtimeMs) - mtime.getTime())).toBeLessThan(2000);
821+
await stop();
822+
});
823+
});

src/nfs/v4/server/operations/node/Nfsv4OperationsNode.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1079,6 +1079,8 @@ export class Nfsv4OperationsNode implements Nfsv4Operations {
10791079
const mask = inFattr.attrmask.mask;
10801080
let atime: Date | undefined;
10811081
let mtime: Date | undefined;
1082+
let uid: number | undefined;
1083+
let gid: number | undefined;
10821084
for (let i = 0; i < mask.length; i++) {
10831085
const word = mask[i];
10841086
for (let bit = 0; bit < 32; bit++) {
@@ -1091,6 +1093,22 @@ export class Nfsv4OperationsNode implements Nfsv4Operations {
10911093
await this.promises.chmod(currentPathAbsolute, mode & 0o7777);
10921094
break;
10931095
}
1096+
case Nfsv4Attr.FATTR4_OWNER: {
1097+
const owner = dec.readString();
1098+
const parsedUid = parseInt(owner, 10);
1099+
if (!isNaN(parsedUid)) {
1100+
uid = parsedUid;
1101+
}
1102+
break;
1103+
}
1104+
case Nfsv4Attr.FATTR4_OWNER_GROUP: {
1105+
const group = dec.readString();
1106+
const parsedGid = parseInt(group, 10);
1107+
if (!isNaN(parsedGid)) {
1108+
gid = parsedGid;
1109+
}
1110+
break;
1111+
}
10941112
case Nfsv4Attr.FATTR4_SIZE: {
10951113
const size = dec.readUnsignedHyper();
10961114
await this.promises.truncate(currentPathAbsolute, Number(size));
@@ -1147,6 +1165,12 @@ export class Nfsv4OperationsNode implements Nfsv4Operations {
11471165
}
11481166
}
11491167
}
1168+
if (uid !== undefined || gid !== undefined) {
1169+
const stats = await this.promises.lstat(currentPathAbsolute);
1170+
const uidToSet = uid !== undefined ? uid : stats.uid;
1171+
const gidToSet = gid !== undefined ? gid : stats.gid;
1172+
await this.promises.chown(currentPathAbsolute, uidToSet, gidToSet);
1173+
}
11501174
if (atime || mtime) {
11511175
const stats = await this.promises.lstat(currentPathAbsolute);
11521176
const atimeToSet = atime || stats.atime;

0 commit comments

Comments
 (0)