Skip to content

Commit

Permalink
fix: some metadata changes not follow common unix filesystem lead to …
Browse files Browse the repository at this point in the history
…node.watchFile not trigger

1. nlink default value is 2
2. add subfolder nlink++
3. atime updated
4. ctime updated after uid/gid/atime/mtime/perm/nlink updated

#889
  • Loading branch information
vangie committed Apr 11, 2023
1 parent 8d9e533 commit f0049e3
Show file tree
Hide file tree
Showing 6 changed files with 91 additions and 24 deletions.
28 changes: 27 additions & 1 deletion src/__tests__/node.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Node } from '../node';
import { Node, Link } from '../node';
import { constants } from '../constants';

describe('node.ts', () => {
Expand Down Expand Up @@ -69,5 +69,31 @@ describe('node.ts', () => {
expect(node.perm).toBe(0o600);
expect(node.isFile()).toBe(true);
});
describe.each([['uid'], ['gid'], ['atime'], ['mtime'], ['perm'], ['nlink']])('ctime changes', field => {
it(`set ${field}`, () => {
const node = new Node(1);
const oldCtime = node.ctime;
node[field] = 1;
const newCtime = node.ctime;
expect(newCtime !== oldCtime).toBeTruthy();
});
});

describe.each([
['getString', []],
['getBuffer', []],
['read', [Buffer.alloc(0)]],
])('atime changes', (method, args) => {
it(`${method}()`, () => {
const node = new Node(1);
const oldAtime = node.atime;
const oldCtime = node.ctime;
node[method](...args);
const newAtime = node.atime;
const newCtime = node.ctime;
expect(newAtime !== oldAtime).toBeTruthy();
expect(newCtime !== oldCtime).toBeTruthy();
});
});
});
});
2 changes: 1 addition & 1 deletion src/__tests__/promises.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,7 @@ describe('Promises API', () => {
'/foo/baz': 'baz',
});
it('Read an existing directory', async () => {
expect(await promises.readdir('/foo')).toEqual(['bar', 'baz']);
expect(await promises.readdir('/foo')).toEqual(['.', '..', 'bar', 'baz']);
});
it('Reject when directory does not exist', () => {
return expect(promises.readdir('/bar')).rejects.toBeInstanceOf(Error);
Expand Down
23 changes: 16 additions & 7 deletions src/__tests__/volume.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ describe('volume', () => {
const stat = vol.statSync('/dir');

expect(stat.isDirectory()).toBe(true);
expect(vol.readdirSync('/dir')).toEqual([]);
expect(vol.readdirSync('/dir')).toEqual(['.', '..']);
});
});

Expand Down Expand Up @@ -332,10 +332,13 @@ describe('volume', () => {
describe('.openSync(path, flags[, mode])', () => {
const vol = new Volume();
it('Create new file at root (/test.txt)', () => {
const oldMtime = vol.root.getNode().mtime;
const fd = vol.openSync('/test.txt', 'w');
const newMtime = vol.root.getNode().mtime;
expect(vol.root.getChild('test.txt')).toBeInstanceOf(Link);
expect(typeof fd).toBe('number');
expect(fd).toBeGreaterThan(0);
expect(oldMtime !== newMtime).toBeTruthy();
});
it('Error on file not found', () => {
try {
Expand Down Expand Up @@ -870,20 +873,20 @@ describe('volume', () => {
vol.writeFileSync('/1.js', '123');
vol.writeFileSync('/2.js', '123');
const list = vol.readdirSync('/');
expect(list.length).toBe(2);
expect(list).toEqual(['1.js', '2.js']);
expect(list.length).toBe(4);
expect(list).toEqual(['.', '..', '1.js', '2.js']);
});
it('Returns a Dirent list', () => {
const vol = new Volume();
vol.writeFileSync('/1', '123');
vol.mkdirSync('/2');
const list = vol.readdirSync('/', { withFileTypes: true });
expect(list.length).toBe(2);
expect(list[0]).toBeInstanceOf(Dirent);
const dirent0 = list[0] as Dirent;
expect(list.length).toBe(4);
expect(list[2]).toBeInstanceOf(Dirent);
const dirent0 = list[2] as Dirent;
expect(dirent0.name).toBe('1');
expect(dirent0.isFile()).toBe(true);
const dirent1 = list[1] as Dirent;
const dirent1 = list[3] as Dirent;
expect(dirent1.name).toBe('2');
expect(dirent1.isDirectory()).toBe(true);
});
Expand Down Expand Up @@ -989,10 +992,16 @@ describe('volume', () => {
describe('.mkdirSync(path[, options])', () => {
it('Create dir at root', () => {
const vol = new Volume();
const oldMtime = vol.root.getNode().mtime;
const oldNlink = vol.root.getNode().nlink;
vol.mkdirSync('/test');
const newMtime = vol.root.getNode().mtime;
const newNlink = vol.root.getNode().nlink;
const child = tryGetChild(vol.root, 'test');
expect(child).toBeInstanceOf(Link);
expect(child.getNode().isDirectory()).toBe(true);
expect(oldMtime !== newMtime).toBeTruthy();
expect(newNlink === oldNlink + 1).toBeTruthy;
});
it('Create 2 levels deep folders', () => {
const vol = new Volume();
Expand Down
10 changes: 5 additions & 5 deletions src/__tests__/volume/readdirSync.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ describe('readdirSync()', () => {
});
const dirs = vol.readdirSync('/');

expect(dirs).toEqual(['foo']);
expect(dirs).toEqual(['.', '..', 'foo']);
});

it('returns multiple directories', () => {
Expand All @@ -20,14 +20,14 @@ describe('readdirSync()', () => {

(dirs as any).sort();

expect(dirs).toEqual(['ab', 'foo', 'tro']);
expect(dirs).toEqual(['.', '..', 'ab', 'foo', 'tro']);
});

it('returns empty array when dir empty', () => {
const vol = create({});
const dirs = vol.readdirSync('/');

expect(dirs).toEqual([]);
expect(dirs).toEqual(['.', '..']);
});

it('respects symlinks', () => {
Expand All @@ -43,7 +43,7 @@ describe('readdirSync()', () => {

(dirs as any).sort();

expect(dirs).toEqual(['a', 'aa']);
expect(dirs).toEqual(['.', '..', 'a', 'aa']);
});

it('respects recursive symlinks', () => {
Expand All @@ -53,6 +53,6 @@ describe('readdirSync()', () => {

const dirs = vol.readdirSync('/foo');

expect(dirs).toEqual(['foo']);
expect(dirs).toEqual(['.', '..', 'foo']);
});
});
41 changes: 35 additions & 6 deletions src/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,21 @@ export class Node extends EventEmitter {
this.perm = perm;
this.mode |= perm;
this.ino = ino;

return new Proxy(this, {
set(target, name, value): boolean {
const metadataProps = ['uid', 'gid', 'atime', 'mtime', 'perm', 'nlink'];
if (metadataProps.includes(String(name))) {
target.ctime = new Date();
}
target[name] = value;
return true;
},
});
}

getString(encoding = 'utf8'): string {
this.atime = new Date();
return this.getBuffer().toString(encoding);
}

Expand All @@ -57,6 +69,7 @@ export class Node extends EventEmitter {
}

getBuffer(): Buffer {
this.atime = new Date();
if (!this.buf) this.setBuffer(bufferAllocUnsafe(0));
return bufferFrom(this.buf); // Return a copy.
}
Expand Down Expand Up @@ -122,6 +135,7 @@ export class Node extends EventEmitter {

// Returns the number of bytes read.
read(buf: Buffer | Uint8Array, off: number = 0, len: number = buf.byteLength, pos: number = 0): number {
this.atime = new Date();
if (!this.buf) this.buf = bufferAllocUnsafe(0);

let actualLen = len;
Expand Down Expand Up @@ -262,8 +276,11 @@ export class Link extends EventEmitter {
// Recursively sync children steps, e.g. in case of dir rename
set steps(val) {
this._steps = val;
for (const child of Object.values(this.children)) {
child?.syncSteps();
for (const [child, link] of Object.entries(this.children)) {
if (child === '.' || child === '..') {
continue;
}
link?.syncSteps();
}
}

Expand All @@ -289,10 +306,8 @@ export class Link extends EventEmitter {
link.setNode(node);

if (node.isDirectory()) {
// link.setChild('.', link);
// link.getNode().nlink++;
// link.setChild('..', this);
// this.getNode().nlink++;
link.children['.'] = link;
link.getNode().nlink++;
}

this.setChild(name, link);
Expand All @@ -305,19 +320,33 @@ export class Link extends EventEmitter {
link.parent = this;
this.length++;

const node = link.getNode();
if (node.isDirectory()) {
link.children['..'] = this;
this.getNode().nlink++;
this.getNode().mtime = new Date();
}

this.emit('child:add', link, this);

return link;
}

deleteChild(link: Link) {
const node = link.getNode();
if (node.isDirectory()) {
delete link.children['..'];
this.getNode().nlink--;
this.getNode().mtime = new Date();
}
delete this.children[link.getName()];
this.length--;

this.emit('child:delete', link, this);
}

getChild(name: string): Link | undefined {
this.getNode().mtime = new Date();
if (Object.hasOwnProperty.call(this.children, name)) {
return this.children[name];
}
Expand Down
11 changes: 7 additions & 4 deletions src/volume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -666,11 +666,11 @@ export class Volume {
}
};

// root.setChild('.', root);
// root.getNode().nlink++;
root.setChild('.', root);
root.getNode().nlink++;

// root.setChild('..', root);
// root.getNode().nlink++;
root.setChild('..', root);
root.getNode().nlink++;

this.root = root;
}
Expand Down Expand Up @@ -879,6 +879,9 @@ export class Volume {
}

for (const name in children) {
if (name === '.' || name === '..') {
continue;
}
isEmpty = false;

const child = link.getChild(name);
Expand Down

0 comments on commit f0049e3

Please sign in to comment.