Skip to content

Commit 23d8abd

Browse files
committed
feat: 🎸 support reporting back file system stats
1 parent f49123f commit 23d8abd

File tree

4 files changed

+123
-7
lines changed

4 files changed

+123
-7
lines changed

src/nfs/v4/attributes.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,15 @@ export const STAT_ATTRS = new Set<Nfsv4Attr>([
200200
Nfsv4Attr.FATTR4_TIME_MODIFY,
201201
]);
202202

203+
export const FS_ATTRS = new Set<Nfsv4Attr>([
204+
Nfsv4Attr.FATTR4_FILES_AVAIL,
205+
Nfsv4Attr.FATTR4_FILES_FREE,
206+
Nfsv4Attr.FATTR4_FILES_TOTAL,
207+
Nfsv4Attr.FATTR4_SPACE_AVAIL,
208+
Nfsv4Attr.FATTR4_SPACE_FREE,
209+
Nfsv4Attr.FATTR4_SPACE_TOTAL,
210+
]);
211+
203212
/**
204213
* Extract attribute numbers from a bitmap mask.
205214
*
@@ -232,6 +241,8 @@ export const containsSetOnlyAttr = (requestedAttrs: Set<number>): boolean => ove
232241
*/
233242
export const requiresLstat = (requestedAttrs: Set<number>): boolean => overlaps(requestedAttrs, STAT_ATTRS);
234243

244+
export const requiresFsStats = (requestedAttrs: Set<number>): boolean => overlaps(requestedAttrs, FS_ATTRS);
245+
235246
export const setBit = (mask: number[], attrNum: Nfsv4Attr): void => {
236247
const wordIndex = Math.floor(attrNum / 32);
237248
const bitIndex = attrNum % 32;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* Filesystem statistics for NFSv4 space and file count attributes.
3+
*/
4+
export class FilesystemStats {
5+
constructor(
6+
/** Available space in bytes for unprivileged users */
7+
public readonly spaceAvail: bigint,
8+
/** Free space in bytes on the filesystem */
9+
public readonly spaceFree: bigint,
10+
/** Total space in bytes on the filesystem */
11+
public readonly spaceTotal: bigint,
12+
/** Available file slots (inodes) */
13+
public readonly filesAvail: bigint,
14+
/** Free file slots (inodes) */
15+
public readonly filesFree: bigint,
16+
/** Total file slots (inodes) */
17+
public readonly filesTotal: bigint,
18+
) {}
19+
}

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

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,12 @@ import {OpenFileState} from '../OpenFileState';
2222
import {OpenOwnerState} from '../OpenOwnerState';
2323
import {LockOwnerState} from '../LockOwnerState';
2424
import {ByteRangeLock} from '../ByteRangeLock';
25+
import {FilesystemStats} from '../FilesystemStats';
2526
import {FileHandleMapper, ROOT_FH} from './fh';
2627
import {isErrCode, normalizeNodeFsError} from './util';
2728
import {Nfsv4StableHow, Nfsv4Attr} from '../../../constants';
2829
import {encodeAttrs} from './attrs';
29-
import {parseBitmask, requiresLstat, attrNumsToBitmap} from '../../../attributes';
30+
import {parseBitmask, requiresLstat, attrNumsToBitmap, requiresFsStats} from '../../../attributes';
3031
import {Writer} from '@jsonjoy.com/buffers/lib/Writer';
3132
import {XdrEncoder} from '../../../../../xdr/XdrEncoder';
3233
import {XdrDecoder} from '../../../../../xdr/XdrDecoder';
@@ -52,6 +53,12 @@ export interface Nfsv4OperationsNodeOpts {
5253
* @default 1000
5354
*/
5455
maxPendingClients?: number;
56+
57+
/**
58+
* Optional function to provide filesystem statistics.
59+
* If not provided, defaults to 2TB available space and 2M available inodes.
60+
*/
61+
fsStats?: () => Promise<FilesystemStats>;
5562
}
5663

5764
/**
@@ -104,15 +111,30 @@ export class Nfsv4OperationsNode implements Nfsv4Operations {
104111
*/
105112
protected changeCounter: bigint = 0n;
106113

114+
/**
115+
* Function to retrieve filesystem statistics.
116+
*/
117+
protected fsStats: () => Promise<FilesystemStats>;
118+
107119
constructor(opts: Nfsv4OperationsNodeOpts) {
108120
this.fs = opts.fs;
109121
this.promises = this.fs.promises;
110122
this.dir = opts.dir;
111123
this.fh = new FileHandleMapper(this.bootStamp, this.dir);
112124
this.maxClients = opts.maxClients ?? 1000;
113125
this.maxPendingClients = opts.maxPendingClients ?? 1000;
126+
this.fsStats = opts.fsStats ?? this.defaultFsStats;
114127
}
115128

129+
/**
130+
* Default filesystem statistics: 2TB available space, 2M available inodes.
131+
*/
132+
protected defaultFsStats = async (): Promise<FilesystemStats> => {
133+
const twoTB = BigInt(2 * 1024 * 1024 * 1024 * 1024); // 2TB
134+
const twoM = BigInt(2 * 1000 * 1000); // 2M inodes
135+
return new FilesystemStats(twoTB, twoTB, twoTB * 2n, twoM, twoM, twoM * 2n);
136+
};
137+
116138
protected findClientByIdString(
117139
map: Map<bigint, ClientRecord>,
118140
clientIdString: Uint8Array,
@@ -474,7 +496,15 @@ export class Nfsv4OperationsNode implements Nfsv4Operations {
474496
throw normalizeNodeFsError(error, ctx.connection.logger);
475497
}
476498
}
477-
const attrs = encodeAttrs(request.attrRequest, stats, currentPath, ctx.cfh!, this.leaseTime);
499+
let fsStats: FilesystemStats | undefined;
500+
if (requiresFsStats(requestedAttrNums)) {
501+
try {
502+
fsStats = await this.fsStats();
503+
} catch (error: unknown) {
504+
ctx.connection.logger.error(error);
505+
}
506+
}
507+
const attrs = encodeAttrs(request.attrRequest, stats, currentPath, ctx.cfh!, this.leaseTime, fsStats);
478508
return new msg.Nfsv4GetattrResponse(Nfsv4Stat.NFS4_OK, new msg.Nfsv4GetattrResOk(attrs));
479509
}
480510

@@ -570,6 +600,7 @@ export class Nfsv4OperationsNode implements Nfsv4Operations {
570600
if (startIndex > dirents.length) startIndex = dirents.length;
571601
}
572602
let eof = true;
603+
const fsStats = await this.fsStats();
573604
for (let i = startIndex; i < dirents.length; i++) {
574605
const dirent = dirents[i];
575606
const name = dirent.name;
@@ -582,7 +613,7 @@ export class Nfsv4OperationsNode implements Nfsv4Operations {
582613
continue;
583614
}
584615
const entryFh = fh.encode(entryPath);
585-
const attrs = encodeAttrs(attrRequest, entryStats, entryPath, entryFh, this.leaseTime);
616+
const attrs = encodeAttrs(attrRequest, entryStats, entryPath, entryFh, this.leaseTime, fsStats);
586617
const nameBytes = Buffer.byteLength(name, 'utf8');
587618
const attrBytes = attrs.attrVals.length;
588619
const entryBytes = overheadPerEntry + nameBytes + attrBytes;
@@ -1380,10 +1411,18 @@ export class Nfsv4OperationsNode implements Nfsv4Operations {
13801411
const currentPathAbsolute = this.absolutePath(currentPath);
13811412
try {
13821413
const stats = await this.promises.lstat(currentPathAbsolute);
1414+
const fsStats = await this.fsStats();
13831415
// request.objAttributes is a Nfsv4Fattr: use its attrmask when asking
13841416
// encodeAttrs to serialize the server's current attributes and compare
13851417
// raw attrVals bytes.
1386-
const attrs = encodeAttrs(request.objAttributes.attrmask, stats, currentPathAbsolute, ctx.cfh!, this.leaseTime);
1418+
const attrs = encodeAttrs(
1419+
request.objAttributes.attrmask,
1420+
stats,
1421+
currentPathAbsolute,
1422+
ctx.cfh!,
1423+
this.leaseTime,
1424+
fsStats,
1425+
);
13871426
if (cmpUint8Array(attrs.attrVals, request.objAttributes.attrVals))
13881427
return new msg.Nfsv4NverifyResponse(Nfsv4Stat.NFS4ERR_NOT_SAME);
13891428
return new msg.Nfsv4NverifyResponse(Nfsv4Stat.NFS4_OK);
@@ -1503,9 +1542,10 @@ export class Nfsv4OperationsNode implements Nfsv4Operations {
15031542
}
15041543
const stats = await this.promises.lstat(currentPathAbsolute);
15051544
const fh = this.fh.encode(currentPath);
1545+
const fsStats = await this.fsStats();
15061546
// Return updated mode and size attributes
15071547
const returnMask = new struct.Nfsv4Bitmap(attrNumsToBitmap([Nfsv4Attr.FATTR4_MODE, Nfsv4Attr.FATTR4_SIZE]));
1508-
const fattr = encodeAttrs(returnMask, stats, currentPath, fh, this.leaseTime);
1548+
const fattr = encodeAttrs(returnMask, stats, currentPath, fh, this.leaseTime, fsStats);
15091549
const resok = new msg.Nfsv4SetattrResOk(returnMask);
15101550
return new msg.Nfsv4SetattrResponse(Nfsv4Stat.NFS4_OK, resok);
15111551
} catch (err: unknown) {
@@ -1519,7 +1559,8 @@ export class Nfsv4OperationsNode implements Nfsv4Operations {
15191559
const currentPathAbsolute = this.absolutePath(currentPath);
15201560
try {
15211561
const stats = await this.promises.lstat(currentPathAbsolute);
1522-
const attrs = encodeAttrs(request.objAttributes.attrmask, stats, currentPath, ctx.cfh!, this.leaseTime);
1562+
const fsStats = await this.fsStats();
1563+
const attrs = encodeAttrs(request.objAttributes.attrmask, stats, currentPath, ctx.cfh!, this.leaseTime, fsStats);
15231564
if (cmpUint8Array(attrs.attrVals, request.objAttributes.attrVals))
15241565
return new msg.Nfsv4VerifyResponse(Nfsv4Stat.NFS4_OK);
15251566
return new msg.Nfsv4VerifyResponse(Nfsv4Stat.NFS4ERR_NOT_SAME);

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

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import {Writer} from '@jsonjoy.com/buffers/lib/Writer';
77
import {XdrEncoder} from '../../../../../xdr/XdrEncoder';
88
import {Nfsv4Attr, Nfsv4FType, Nfsv4FhExpireType, Nfsv4Stat} from '../../../constants';
99
import * as struct from '../../../structs';
10-
import {REQUIRED_ATTRS, RECOMMENDED_ATTRS, SET_ONLY_ATTRS, setBit} from '../../../attributes';
10+
import {SET_ONLY_ATTRS, setBit} from '../../../attributes';
11+
import type {FilesystemStats} from '../FilesystemStats';
1112

1213
/**
1314
* Encodes file attributes based on the requested bitmap.
@@ -17,13 +18,15 @@ import {REQUIRED_ATTRS, RECOMMENDED_ATTRS, SET_ONLY_ATTRS, setBit} from '../../.
1718
* @param path File path (for context)
1819
* @param fh Optional file handle (required only if FATTR4_FILEHANDLE is requested)
1920
* @param leaseTime Optional lease time in seconds (required only if FATTR4_LEASE_TIME is requested)
21+
* @param fsStats Optional filesystem statistics (required for space/files attributes)
2022
*/
2123
export const encodeAttrs = (
2224
requestedAttrs: struct.Nfsv4Bitmap,
2325
stats: Stats | undefined,
2426
path: string,
2527
fh?: Uint8Array,
2628
leaseTime?: number,
29+
fsStats?: FilesystemStats,
2730
): struct.Nfsv4Fattr => {
2831
const writer = new Writer(512);
2932
const xdr = new XdrEncoder(writer);
@@ -56,6 +59,12 @@ export const encodeAttrs = (
5659
setBit(implementedAttrs, Nfsv4Attr.FATTR4_MODE);
5760
setBit(implementedAttrs, Nfsv4Attr.FATTR4_NUMLINKS);
5861
setBit(implementedAttrs, Nfsv4Attr.FATTR4_SPACE_USED);
62+
setBit(implementedAttrs, Nfsv4Attr.FATTR4_SPACE_AVAIL);
63+
setBit(implementedAttrs, Nfsv4Attr.FATTR4_SPACE_FREE);
64+
setBit(implementedAttrs, Nfsv4Attr.FATTR4_SPACE_TOTAL);
65+
setBit(implementedAttrs, Nfsv4Attr.FATTR4_FILES_AVAIL);
66+
setBit(implementedAttrs, Nfsv4Attr.FATTR4_FILES_FREE);
67+
setBit(implementedAttrs, Nfsv4Attr.FATTR4_FILES_TOTAL);
5968
setBit(implementedAttrs, Nfsv4Attr.FATTR4_TIME_ACCESS);
6069
setBit(implementedAttrs, Nfsv4Attr.FATTR4_TIME_METADATA);
6170
setBit(implementedAttrs, Nfsv4Attr.FATTR4_TIME_MODIFY);
@@ -111,6 +120,42 @@ export const encodeAttrs = (
111120
setBit(supportedMask, attrNum);
112121
break;
113122
}
123+
case Nfsv4Attr.FATTR4_SPACE_AVAIL: {
124+
if (!fsStats) break;
125+
xdr.writeUnsignedHyper(fsStats.spaceAvail);
126+
setBit(supportedMask, attrNum);
127+
break;
128+
}
129+
case Nfsv4Attr.FATTR4_SPACE_FREE: {
130+
if (!fsStats) break;
131+
xdr.writeUnsignedHyper(fsStats.spaceFree);
132+
setBit(supportedMask, attrNum);
133+
break;
134+
}
135+
case Nfsv4Attr.FATTR4_SPACE_TOTAL: {
136+
if (!fsStats) break;
137+
xdr.writeUnsignedHyper(fsStats.spaceTotal);
138+
setBit(supportedMask, attrNum);
139+
break;
140+
}
141+
case Nfsv4Attr.FATTR4_FILES_AVAIL: {
142+
if (!fsStats) break;
143+
xdr.writeUnsignedHyper(fsStats.filesAvail);
144+
setBit(supportedMask, attrNum);
145+
break;
146+
}
147+
case Nfsv4Attr.FATTR4_FILES_FREE: {
148+
if (!fsStats) break;
149+
xdr.writeUnsignedHyper(fsStats.filesFree);
150+
setBit(supportedMask, attrNum);
151+
break;
152+
}
153+
case Nfsv4Attr.FATTR4_FILES_TOTAL: {
154+
if (!fsStats) break;
155+
xdr.writeUnsignedHyper(fsStats.filesTotal);
156+
setBit(supportedMask, attrNum);
157+
break;
158+
}
114159
case Nfsv4Attr.FATTR4_TIME_ACCESS: {
115160
if (!stats) break;
116161
const atime = stats.atimeMs;

0 commit comments

Comments
 (0)