Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions packages/fs-core/src/Node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export class Node {
private _atime = new Date();
private _mtime = new Date();
private _ctime = new Date();
private _btime = new Date();

buf: Buffer = EMPTY_BUFFER;

Expand Down Expand Up @@ -68,6 +69,19 @@ export class Node {
return this._ctime;
}

/**
* Birth (creation) time of this node. Unlike `ctime`, it is set once when the
* node is created and is never advanced by writes or metadata changes. The
* setter exists only for restore paths, like snapshot deserialization.
*/
public set btime(btime: Date) {
this._btime = btime;
}

public get btime(): Date {
return this._btime;
}

public set uid(uid: number) {
this._uid = uid;
this.ctime = new Date();
Expand Down Expand Up @@ -342,6 +356,7 @@ export class Node {
atime: this.atime.getTime(),
mtime: this.mtime.getTime(),
ctime: this.ctime.getTime(),
btime: this.btime.getTime(),
perm: this.perm,
mode: this.mode,
nlink: this.nlink,
Expand Down
42 changes: 42 additions & 0 deletions packages/fs-core/src/__tests__/Node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,4 +146,46 @@ describe('Node', () => {
expect(result.toString('utf8', 10, 15)).toBe('hello');
});
});

describe('btime (birth time)', () => {
const advance = () => new Promise(resolve => setTimeout(resolve, 10));
it('is set on construction', () => {
const before = Date.now();
const node = new Node(1, 0o666);
const after = Date.now();
expect(node.btime.getTime()).toBeGreaterThanOrEqual(before);
expect(node.btime.getTime()).toBeLessThanOrEqual(after);
});

it('is not advanced by writes, while ctime is', async () => {
const node = new Node(1, 0o666);
const btime = node.btime.getTime();
const ctime = node.ctime.getTime();
await advance();
node.write(bufferFrom('hello'), 0, 5, 0);
expect(node.btime.getTime()).toBe(btime);
expect(node.ctime.getTime()).toBeGreaterThan(ctime);
});

it('is not advanced by truncate, chmod, chown or nlink changes', async () => {
const node = new Node(1, 0o666);
node.write(bufferFrom('hello'), 0, 5, 0);
const btime = node.btime.getTime();
await advance();
node.truncate(2);
node.chmod(0o600);
node.chown(1, 2);
node.nlink = 2;
expect(node.btime.getTime()).toBe(btime);
expect(node.ctime.getTime()).toBeGreaterThan(btime);
});

it('can be restored through the setter', () => {
const node = new Node(1, 0o666);
const btime = new Date(1234567890000);
node.btime = btime;
expect(node.btime).toBe(btime);
expect(node.toJSON().btime).toBe(1234567890000);
});
});
});
14 changes: 6 additions & 8 deletions packages/fs-node/src/Stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export class Stats<T = TStatNumber> {
static build(node: Node, bigint?: boolean): Stats<TStatNumber>;
static build(node: Node, bigint: boolean = false): Stats<TStatNumber> {
const stats = new Stats<TStatNumber>();
const { uid, gid, atime, mtime, ctime } = node;
const { uid, gid, atime, mtime, ctime, btime } = node;

const getStatNumber = !bigint ? number => number : number => BigInt(number);

Expand All @@ -34,20 +34,18 @@ export class Stats<T = TStatNumber> {
stats.atime = atime;
stats.mtime = mtime;
stats.ctime = ctime;
stats.birthtime = ctime;
stats.birthtime = btime;

stats.atimeMs = getStatNumber(atime.getTime());
stats.mtimeMs = getStatNumber(mtime.getTime());
const ctimeMs = getStatNumber(ctime.getTime());
stats.ctimeMs = ctimeMs;
stats.birthtimeMs = ctimeMs;
stats.ctimeMs = getStatNumber(ctime.getTime());
stats.birthtimeMs = getStatNumber(btime.getTime());

if (bigint) {
stats.atimeNs = BigInt(atime.getTime()) * BigInt(1000000);
stats.mtimeNs = BigInt(mtime.getTime()) * BigInt(1000000);
const ctimeNs = BigInt(ctime.getTime()) * BigInt(1000000);
stats.ctimeNs = ctimeNs;
stats.birthtimeNs = ctimeNs;
stats.ctimeNs = BigInt(ctime.getTime()) * BigInt(1000000);
stats.birthtimeNs = BigInt(btime.getTime()) * BigInt(1000000);
}

stats.dev = getStatNumber(0);
Expand Down
78 changes: 78 additions & 0 deletions packages/fs-node/src/__tests__/birthtime.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { createFs } from './util';

describe('birthtime', () => {
const advance = () => new Promise(resolve => setTimeout(resolve, 10));

it('stays stable across writes to a file', async () => {
const fs = createFs();
await fs.promises.writeFile('/file.txt', 'one');
const { birthtime, birthtimeMs } = await fs.promises.stat('/file.txt');
await advance();
await fs.promises.writeFile('/file.txt', 'two');
await fs.promises.appendFile('/file.txt', 'three');
const stats = await fs.promises.stat('/file.txt');
expect(stats.birthtimeMs).toBe(birthtimeMs);
expect(stats.birthtime.getTime()).toBe(birthtime.getTime());
expect(stats.mtimeMs).toBeGreaterThan(birthtimeMs);
expect(stats.ctimeMs).toBeGreaterThan(birthtimeMs);
});

it('stays stable across truncate, chmod and chown', async () => {
const fs = createFs();
await fs.promises.writeFile('/file.txt', 'hello');
const { birthtimeMs } = await fs.promises.stat('/file.txt');
await advance();
await fs.promises.truncate('/file.txt', 2);
await fs.promises.chmod('/file.txt', 0o600);
await fs.promises.chown('/file.txt', process.getuid?.() ?? 0, process.getgid?.() ?? 0);
const stats = await fs.promises.stat('/file.txt');
expect(stats.birthtimeMs).toBe(birthtimeMs);
expect(stats.ctimeMs).toBeGreaterThan(birthtimeMs);
});

it('stays stable across utimes', async () => {
const fs = createFs();
await fs.promises.writeFile('/file.txt', 'hello');
const { birthtimeMs } = await fs.promises.stat('/file.txt');
await advance();
await fs.promises.utimes('/file.txt', new Date(0), new Date(0));
const stats = await fs.promises.stat('/file.txt');
expect(stats.birthtimeMs).toBe(birthtimeMs);
expect(stats.mtimeMs).toBe(0);
});

it('stays stable for a directory when its contents change', async () => {
const fs = createFs();
await fs.promises.mkdir('/dir');
const { birthtimeMs } = await fs.promises.stat('/dir');
await advance();
await fs.promises.writeFile('/dir/file.txt', 'hello');
await fs.promises.unlink('/dir/file.txt');
const stats = await fs.promises.stat('/dir');
expect(stats.birthtimeMs).toBe(birthtimeMs);
expect(stats.mtimeMs).toBeGreaterThan(birthtimeMs);
});

it('is reported for bigint stats', async () => {
const fs = createFs();
fs.writeFileSync('/file.txt', 'one');
const { birthtimeMs, birthtimeNs } = fs.statSync('/file.txt', { bigint: true });
await advance();
fs.writeFileSync('/file.txt', 'two');
const stats = fs.statSync('/file.txt', { bigint: true });
expect(stats.birthtimeMs).toBe(birthtimeMs);
expect(stats.birthtimeNs).toBe(birthtimeNs);
expect(stats.birthtimeNs).toBe(birthtimeMs * BigInt(1000000));
expect(stats.ctimeMs).toBeGreaterThan(birthtimeMs);
});

it('is distinct per file', async () => {
const fs = createFs();
await fs.promises.writeFile('/first.txt', 'one');
await advance();
await fs.promises.writeFile('/second.txt', 'two');
const first = await fs.promises.stat('/first.txt');
const second = await fs.promises.stat('/second.txt');
expect(second.birthtimeMs).toBeGreaterThan(first.birthtimeMs);
});
});
Loading