From f0049e3cc1c111ba244648847535c5b0bef946af Mon Sep 17 00:00:00 2001 From: Vangie Du Date: Wed, 21 Dec 2022 17:29:21 +0800 Subject: [PATCH] fix: some metadata changes not follow common unix filesystem lead to 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 https://github.com/streamich/memfs/issues/889 --- src/__tests__/node.test.ts | 28 +++++++++++++++- src/__tests__/promises.test.ts | 2 +- src/__tests__/volume.test.ts | 23 +++++++++---- src/__tests__/volume/readdirSync.test.ts | 10 +++--- src/node.ts | 41 ++++++++++++++++++++---- src/volume.ts | 11 ++++--- 6 files changed, 91 insertions(+), 24 deletions(-) diff --git a/src/__tests__/node.test.ts b/src/__tests__/node.test.ts index c88798028..7372a7268 100644 --- a/src/__tests__/node.test.ts +++ b/src/__tests__/node.test.ts @@ -1,4 +1,4 @@ -import { Node } from '../node'; +import { Node, Link } from '../node'; import { constants } from '../constants'; describe('node.ts', () => { @@ -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(); + }); + }); }); }); diff --git a/src/__tests__/promises.test.ts b/src/__tests__/promises.test.ts index e49002799..60267dc75 100644 --- a/src/__tests__/promises.test.ts +++ b/src/__tests__/promises.test.ts @@ -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); diff --git a/src/__tests__/volume.test.ts b/src/__tests__/volume.test.ts index fe7b134cd..2d62525d2 100644 --- a/src/__tests__/volume.test.ts +++ b/src/__tests__/volume.test.ts @@ -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(['.', '..']); }); }); @@ -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 { @@ -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); }); @@ -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(); diff --git a/src/__tests__/volume/readdirSync.test.ts b/src/__tests__/volume/readdirSync.test.ts index 53ff2b999..f0aaf535d 100644 --- a/src/__tests__/volume/readdirSync.test.ts +++ b/src/__tests__/volume/readdirSync.test.ts @@ -7,7 +7,7 @@ describe('readdirSync()', () => { }); const dirs = vol.readdirSync('/'); - expect(dirs).toEqual(['foo']); + expect(dirs).toEqual(['.', '..', 'foo']); }); it('returns multiple directories', () => { @@ -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', () => { @@ -43,7 +43,7 @@ describe('readdirSync()', () => { (dirs as any).sort(); - expect(dirs).toEqual(['a', 'aa']); + expect(dirs).toEqual(['.', '..', 'a', 'aa']); }); it('respects recursive symlinks', () => { @@ -53,6 +53,6 @@ describe('readdirSync()', () => { const dirs = vol.readdirSync('/foo'); - expect(dirs).toEqual(['foo']); + expect(dirs).toEqual(['.', '..', 'foo']); }); }); diff --git a/src/node.ts b/src/node.ts index 30faafaa3..e68b74487 100644 --- a/src/node.ts +++ b/src/node.ts @@ -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); } @@ -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. } @@ -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; @@ -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(); } } @@ -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); @@ -305,12 +320,25 @@ 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--; @@ -318,6 +346,7 @@ export class Link extends EventEmitter { } getChild(name: string): Link | undefined { + this.getNode().mtime = new Date(); if (Object.hasOwnProperty.call(this.children, name)) { return this.children[name]; } diff --git a/src/volume.ts b/src/volume.ts index 2c1520065..31710efdb 100644 --- a/src/volume.ts +++ b/src/volume.ts @@ -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; } @@ -879,6 +879,9 @@ export class Volume { } for (const name in children) { + if (name === '.' || name === '..') { + continue; + } isEmpty = false; const child = link.getChild(name);