diff --git a/README.md b/README.md index 4f11cc407..1ced4060d 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,9 @@ This package depends on the following Node modules: `buffer`, `events`, It also uses `process` and `setImmediate` globals, but mocks them, if not available. +It uses `Promise` when available and throws when `promises` property is +accessed in an environment that do not support this ES2015 feature. + [npm-url]: https://www.npmjs.com/package/memfs [npm-badge]: https://img.shields.io/npm/v/memfs.svg [travis-url]: https://travis-ci.org/streamich/memfs diff --git a/docs/api-status.md b/docs/api-status.md index bf6811c6f..dee149d9b 100644 --- a/docs/api-status.md +++ b/docs/api-status.md @@ -4,7 +4,7 @@ All of the [Node's `fs` API](https://nodejs.org/api/fs.html) is implemented. Some error messages may be inaccurate. File permissions are currently not implemented (you have access to any file), basically `fs.access()` is a no-op. - - [ ] Promises + - [x] Promises - [x] Constants - [x] `FSWatcher` - [x] `ReadStream` diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts index 06ccfca2e..f62654a5f 100644 --- a/src/__tests__/index.test.ts +++ b/src/__tests__/index.test.ts @@ -35,4 +35,7 @@ describe('memfs', () => { expect(typeof memfs[method]).toBe('function'); } }); + it('Exports promises API', () => { + expect(typeof memfs.promises).toBe('object'); + }); }); diff --git a/src/__tests__/promises.test.ts b/src/__tests__/promises.test.ts new file mode 100644 index 000000000..8978e3110 --- /dev/null +++ b/src/__tests__/promises.test.ts @@ -0,0 +1,629 @@ +import { Volume } from '../volume'; + +describe('Promises API', () => { + describe('FileHandle', () => { + it('API should have a FileHandle property', () => { + const vol = new Volume; + const { promises } = vol; + expect(typeof promises.FileHandle).toBe('function'); + }); + describe('fd', () => { + it('FileHandle should have a fd property', async () => { + const vol = new Volume; + const { promises } = vol; + vol.fromJSON({ + '/foo': 'bar', + }); + const fileHandle = await promises.open('/foo', 'r'); + expect(typeof fileHandle.fd).toEqual('number'); + await fileHandle.close(); + }); + }); + describe('appendFile(data[, options])', () => { + const vol = new Volume; + const { promises } = vol; + vol.fromJSON({ + '/foo': 'bar', + }); + it('Append data to an existing file', async () => { + const fileHandle = await promises.open('/foo', 'a'); + await fileHandle.appendFile('baz'); + expect(vol.readFileSync('/foo').toString()).toEqual('barbaz'); + await fileHandle.close(); + }); + it('Reject when the file handle was closed', async () => { + const fileHandle = await promises.open('/foo', 'a'); + await fileHandle.close(); + return expect(fileHandle.appendFile('/foo', 'baz')).rejects.toBeInstanceOf(Error); + }); + }); + describe('chmod(mode)', () => { + const vol = new Volume; + const { promises } = vol; + vol.fromJSON({ + '/foo': 'bar', + }); + it('Change mode of existing file', async () => { + const fileHandle = await promises.open('/foo', 'a'); + await fileHandle.chmod(0o444); + expect(vol.statSync('/foo').mode & 0o777).toEqual(0o444); + await fileHandle.close(); + }); + it('Reject when the file handle was closed', async () => { + const fileHandle = await promises.open('/foo', 'a'); + await fileHandle.close(); + return expect(fileHandle.chmod(0o666)).rejects.toBeInstanceOf(Error); + }); + }); + describe('chown(uid, gid)', () => { + const vol = new Volume; + const { promises } = vol; + vol.fromJSON({ + '/foo': 'bar', + }); + const { uid, gid } = vol.statSync('/foo'); + it('Change uid and gid of existing file', async () => { + const fileHandle = await promises.open('/foo', 'a'); + await fileHandle.chown(uid + 1, gid + 1); + const stats = vol.statSync('/foo'); + expect(stats.uid).toEqual(uid + 1); + expect(stats.gid).toEqual(gid + 1); + await fileHandle.close(); + }); + it('Reject when the file handle was closed', async () => { + const fileHandle = await promises.open('/foo', 'a'); + await fileHandle.close(); + return expect(fileHandle.chown(uid + 2, gid + 2)).rejects.toBeInstanceOf(Error); + }); + }); + // close(): covered by all other tests + describe('datasync()', () => { + const vol = new Volume; + const { promises } = vol; + vol.fromJSON({ + '/foo': 'bar', + }); + it('Synchronize data with an existing file', async () => { + const fileHandle = await promises.open('/foo', 'r+'); + await fileHandle.datasync(); + expect(vol.readFileSync('/foo').toString()).toEqual('bar'); + await fileHandle.close(); + }); + it('Reject when the file handle was closed', async () => { + const fileHandle = await promises.open('/foo', 'r+'); + await fileHandle.close(); + return expect(fileHandle.datasync()).rejects.toBeInstanceOf(Error); + }); + }); + describe('read(buffer, offset, length, position)', () => { + const vol = new Volume; + const { promises } = vol; + vol.fromJSON({ + '/foo': 'bar', + }); + it('Read data from an existing file', async () => { + const fileHandle = await promises.open('/foo', 'r+'); + const buff = Buffer.from('foo'); + const { bytesRead, buffer } = await fileHandle.read(buff, 0, 42, 0); + expect(bytesRead).toEqual(3); + expect(buffer).toBe(buff); + await fileHandle.close(); + }); + it('Reject when the file handle was closed', async () => { + const fileHandle = await promises.open('/foo', 'r+'); + await fileHandle.close(); + return expect(fileHandle.read(Buffer.from('foo'), 0, 42, 0)).rejects.toBeInstanceOf(Error); + }); + }); + describe('readFile([options])', () => { + const vol = new Volume; + const { promises } = vol; + vol.fromJSON({ + '/foo': 'bar', + }); + it('Read data from an existing file', async () => { + const fileHandle = await promises.open('/foo', 'r+'); + expect((await fileHandle.readFile()).toString()).toEqual('bar'); + await fileHandle.close(); + }); + it('Reject when the file handle was closed', async () => { + const fileHandle = await promises.open('/foo', 'r+'); + await fileHandle.close(); + return expect(fileHandle.readFile()).rejects.toBeInstanceOf(Error); + }); + }); + describe('stat()', () => { + const vol = new Volume; + const { promises } = vol; + vol.fromJSON({ + '/foo': 'bar', + }); + it('Return stats of an existing file', async () => { + const fileHandle = await promises.open('/foo', 'r+'); + expect((await fileHandle.stat()).isFile()).toEqual(true); + await fileHandle.close(); + }); + it('Reject when the file handle was closed', async () => { + const fileHandle = await promises.open('/foo', 'r+'); + await fileHandle.close(); + return expect(fileHandle.stat()).rejects.toBeInstanceOf(Error); + }); + }); + describe('truncate([len])', () => { + const vol = new Volume; + const { promises } = vol; + vol.fromJSON({ + '/foo': '0123456789', + }); + it('Truncate an existing file', async () => { + const fileHandle = await promises.open('/foo', 'r+'); + await fileHandle.truncate(5); + expect(vol.readFileSync('/foo').toString()).toEqual('01234'); + await fileHandle.close(); + }); + it('Reject when the file handle was closed', async () => { + const fileHandle = await promises.open('/foo', 'r+'); + await fileHandle.close(); + return expect(fileHandle.truncate(5)).rejects.toBeInstanceOf(Error); + }); + }); + describe('utimes(atime, mtime)', () => { + const vol = new Volume; + const { promises } = vol; + vol.fromJSON({ + '/foo': '0123456789', + }); + const fttDeparture = new Date(1985, 9, 26, 1, 21); // ftt stands for "first time travel" :-) + const fttArrival = new Date(fttDeparture.getTime() + 60000); + it('Changes times of an existing file', async () => { + const fileHandle = await promises.open('/foo', 'r+'); + await fileHandle.utimes(fttArrival, fttDeparture); + const stats = vol.statSync('/foo'); + expect(stats.atime).toEqual(new Date(fttArrival)); + expect(stats.mtime).toEqual(new Date(fttDeparture)); + await fileHandle.close(); + }); + it('Reject when the file handle was closed', async () => { + const fileHandle = await promises.open('/foo', 'r+'); + await fileHandle.close(); + return expect(fileHandle.utimes(fttArrival, fttDeparture)).rejects.toBeInstanceOf(Error); + }); + }); + describe('write(buffer[, offset[, length[, position]]])', () => { + const vol = new Volume; + const { promises } = vol; + vol.fromJSON({ + '/foo': 'bar', + }); + it('Write data to an existing file', async () => { + const fileHandle = await promises.open('/foo', 'w'); + await fileHandle.write(Buffer.from('foo')); + expect(vol.readFileSync('/foo').toString()).toEqual('foo'); + await fileHandle.close(); + }); + it('Reject when the file handle was closed', async () => { + const fileHandle = await promises.open('/foo', 'w'); + await fileHandle.close(); + return expect(fileHandle.write(Buffer.from('foo'))).rejects.toBeInstanceOf(Error); + }); + }); + describe('writeFile(data[, options])', () => { + const vol = new Volume; + const { promises } = vol; + vol.fromJSON({ + '/foo': 'bar', + }); + it('Write data to an existing file', async () => { + const fileHandle = await promises.open('/foo', 'w'); + await fileHandle.writeFile('foo'); + expect(vol.readFileSync('/foo').toString()).toEqual('foo'); + await fileHandle.close(); + }); + it('Reject when the file handle was closed', async () => { + const fileHandle = await promises.open('/foo', 'w'); + await fileHandle.close(); + return expect(fileHandle.writeFile('foo')).rejects.toBeInstanceOf(Error); + }); + }); + }); + describe('access(path[, mode])', () => { + const vol = new Volume; + const { promises } = vol; + vol.fromJSON({ + '/foo': 'bar', + }); + it('Resolve when file exists', () => { + return expect(promises.access('/foo')).resolves.toBeUndefined(); + }); + it('Reject when file does not exist', () => { + return expect(promises.access('/bar')).rejects.toBeInstanceOf(Error); + }); + }); + describe('appendFile(path, data[, options])', () => { + it('Append data to existing file', async () => { + const vol = new Volume; + const { promises } = vol; + vol.fromJSON({ + '/foo': 'bar', + }); + await promises.appendFile('/foo', 'baz'); + expect(vol.readFileSync('/foo').toString()).toEqual('barbaz'); + }); + it('Append data to existing file using FileHandle', async () => { + const vol = new Volume; + const { promises } = vol; + vol.fromJSON({ + '/foo': 'bar', + }); + const fileHandle = await promises.open('/foo', 'a'); + await promises.appendFile(fileHandle, 'baz'); + await fileHandle.close(); + expect(vol.readFileSync('/foo').toString()).toEqual('barbaz'); + }); + it('Reject when trying to write on a directory', () => { + const vol = new Volume; + const { promises } = vol; + vol.fromJSON({ + '/foo': null, + }); + return expect(promises.appendFile('/foo', 'bar')).rejects.toBeInstanceOf(Error); + }); + }); + describe('chmod(path, mode)', () => { + const vol = new Volume; + const { promises } = vol; + vol.fromJSON({ + '/foo': 'bar', + }); + it('Change mode of existing file', async () => { + await promises.chmod('/foo', 0o444); + expect(vol.statSync('/foo').mode & 0o777).toEqual(0o444); + }); + it ('Reject when file does not exist', () => { + return expect(promises.chmod('/bar', 0o444)).rejects.toBeInstanceOf(Error); + }); + }); + describe('chown(path, uid, gid)', () => { + const vol = new Volume; + const { promises } = vol; + vol.fromJSON({ + '/foo': 'bar', + }); + it('Change uid and gid of existing file', async () => { + const { uid, gid } = vol.statSync('/foo'); + await promises.chown('/foo', uid + 1, gid + 1); + const stats = vol.statSync('/foo'); + expect(stats.uid).toEqual(uid + 1); + expect(stats.gid).toEqual(gid + 1); + }); + it ('Reject when file does not exist', () => { + return expect(promises.chown('/bar', 0, 0)).rejects.toBeInstanceOf(Error); + }); + }); + describe('copyFile(src, dest[, flags])', () => { + const vol = new Volume; + const { promises } = vol; + vol.fromJSON({ + '/foo': 'bar', + }); + it('Copy existing file', async () => { + await promises.copyFile('/foo', '/bar'); + expect(vol.readFileSync('/bar').toString()).toEqual('bar'); + }); + it('Reject when file does not exist', () => { + return expect(promises.copyFile('/baz', '/qux')).rejects.toBeInstanceOf(Error); + }); + }); + describe('lchmod(path, mode)', () => { + const vol = new Volume; + const { promises } = vol; + vol.fromJSON({ + '/foo': 'bar', + }); + vol.symlinkSync('/foo', '/bar'); + it('Change mode of existing file', async () => { + await promises.lchmod('/bar', 0o444); + expect(vol.statSync('/foo').mode & 0o777).toEqual(0o666); + expect(vol.lstatSync('/bar').mode & 0o777).toEqual(0o444); + }); + it ('Reject when file does not exist', () => { + return expect(promises.lchmod('/baz', 0o444)).rejects.toBeInstanceOf(Error); + }); + }); + describe('lchown(path, uid, gid)', () => { + const vol = new Volume; + const { promises } = vol; + vol.fromJSON({ + '/foo': 'bar', + }); + vol.symlinkSync('/foo', '/bar'); + it('Change uid and gid of existing file', async () => { + const fooStatsBefore = vol.statSync('/foo'); + const { uid, gid } = vol.statSync('/bar'); + await promises.lchown('/bar', uid + 1, gid + 1); + const fooStatsAfter = vol.statSync('/foo'); + expect(fooStatsAfter.uid).toEqual(fooStatsBefore.uid); + expect(fooStatsAfter.gid).toEqual(fooStatsBefore.gid); + const stats = vol.lstatSync('/bar'); + expect(stats.uid).toEqual(uid + 1); + expect(stats.gid).toEqual(gid + 1); + }); + it ('Reject when file does not exist', () => { + return expect(promises.lchown('/baz', 0, 0)).rejects.toBeInstanceOf(Error); + }); + }); + describe('link(existingPath, newPath)', () => { + const vol = new Volume; + const { promises } = vol; + vol.fromJSON({ + '/foo': 'bar', + }); + it('Create hard link on existing file', async () => { + await promises.link('/foo', '/bar'); + expect(vol.existsSync('/bar')).toEqual(true); + }); + it('Reject when file does not exist', () => { + return expect(promises.link('/baz', '/qux')).rejects.toBeInstanceOf(Error); + }); + }); + describe('lstat(path)', () => { + const vol = new Volume; + const { promises } = vol; + vol.fromJSON({ + '/foo': 'bar', + }); + vol.symlinkSync('/foo', '/bar'); + it('Get stats on an existing symbolic link', async () => { + const stats = await promises.lstat('/bar'); + expect(stats.isSymbolicLink()).toEqual(true); + }); + it('Reject when symbolic link does not exist', () => { + return expect(promises.lstat('/baz')).rejects.toBeInstanceOf(Error); + }); + }); + describe('mkdir(path[, options])', () => { + const vol = new Volume; + const { promises } = vol; + it('Creates a directory', async () => { + await promises.mkdir('/foo'); + expect(vol.statSync('/foo').isDirectory()).toEqual(true); + }); + it('Reject when a file already exists', () => { + vol.writeFileSync('/bar', 'bar'); + return expect(promises.mkdir('/bar')).rejects.toBeInstanceOf(Error); + }); + }); + describe('mkdtemp(prefix[, options])', () => { + const vol = new Volume; + const { promises } = vol; + it('Creates a temporary directory', async () => { + const tmp = await promises.mkdtemp('/foo'); + expect(vol.statSync(tmp).isDirectory()).toEqual(true); + }); + it('Reject when parent directory does not exist', () => { + return expect(promises.mkdtemp('/foo/bar')).rejects.toBeInstanceOf(Error); + }); + }); + describe('open(path, flags[, mode])', () => { + const vol = new Volume; + const { promises } = vol; + vol.fromJSON({ + '/foo': 'bar', + }); + it('Open an existing file', async () => { + expect(await promises.open('/foo', 'r')).toBeInstanceOf(promises.FileHandle); + }); + it('Reject when file does not exist', () => { + return expect(promises.open('/bar', 'r')).rejects.toBeInstanceOf(Error); + }); + }); + describe('readdir(path[, options])', () => { + const vol = new Volume; + const { promises } = vol; + vol.fromJSON({ + '/foo': null, + '/foo/bar': 'bar', + '/foo/baz': 'baz', + }); + it('Read an existing directory', async () => { + expect(await promises.readdir('/foo')).toEqual([ 'bar', 'baz' ]); + }); + it('Reject when directory does not exist', () => { + return expect(promises.readdir('/bar')).rejects.toBeInstanceOf(Error); + }); + }); + describe('readFile(id[, options])', () => { + it('Read existing file', async () => { + const vol = new Volume; + const { promises } = vol; + vol.fromJSON({ + '/foo': 'bar', + }); + expect((await promises.readFile('/foo')).toString()).toEqual('bar'); + }); + it('Read existing file using FileHandle', async () => { + const vol = new Volume; + const { promises } = vol; + vol.fromJSON({ + '/foo': 'bar', + }); + const fileHandle = await promises.open('/foo', 'r'); + expect((await promises.readFile(fileHandle)).toString()).toEqual('bar'); + await fileHandle.close(); + }); + it('Reject when file does not exist', () => { + const vol = new Volume; + const { promises } = vol; + return expect(promises.readFile('/foo')).rejects.toBeInstanceOf(Error); + }); + }); + describe('readlink(path[, options])', () => { + const vol = new Volume; + const { promises } = vol; + vol.symlinkSync('/foo', '/bar'); + it('Read an existing symbolic link', async () => { + expect((await promises.readlink('/bar')).toString()).toEqual('/foo'); + }); + it('Reject when symbolic link does not exist', () => { + return expect(promises.readlink('/foo')).rejects.toBeInstanceOf(Error); + }); + }); + describe('realpath(path[, options])', () => { + const vol = new Volume; + const { promises } = vol; + vol.fromJSON({ + '/foo': null, + '/foo/bar': null, + '/foo/baz': 'baz', + }); + vol.symlinkSync('/foo/baz', '/foo/qux'); + it('Return real path of existing file', async () => { + console.log((await promises.realpath('/foo/bar/../qux')).toString()); + expect((await promises.realpath('/foo/bar/../qux')).toString()).toEqual('/foo/baz'); + }); + it('Reject when file does not exist', () => { + return expect(promises.realpath('/bar')).rejects.toBeInstanceOf(Error); + }); + }); + describe('rename(oldPath, newPath)', () => { + it('Rename existing file', async () => { + const vol = new Volume; + const { promises } = vol; + vol.fromJSON({ + '/foo': 'bar', + }); + await promises.rename('/foo', '/bar'); + expect(vol.readFileSync('/bar').toString()).toEqual('bar'); + }); + it('Reject when file does not exist', () => { + const vol = new Volume; + const { promises } = vol; + vol.fromJSON({ + '/foo': 'bar', + }); + return expect(promises.rename('/bar', '/baz')).rejects.toBeInstanceOf(Error); + }); + }); + describe('rmdir(path)', () => { + const vol = new Volume; + const { promises } = vol; + vol.fromJSON({ + '/foo': null, + }); + it('Remove an existing directory', async () => { + await promises.rmdir('/foo'); + expect(vol.existsSync('/foo')).toEqual(false); + }); + it('Reject when directory does not exist', () => { + return expect(promises.rmdir('/bar')).rejects.toBeInstanceOf(Error); + }); + }); + describe('stat(path)', () => { + const vol = new Volume; + const { promises } = vol; + vol.fromJSON({ + '/foo': null, + }); + it('Return stats of an existing directory', async () => { + expect((await promises.stat('/foo')).isDirectory()).toEqual(true); + }); + it('Reject when directory does not exist', () => { + return expect(promises.stat('/bar')).rejects.toBeInstanceOf(Error); + }); + }); + describe('symlink(target, path[, type])', () => { + it('Create symbolic link', async () => { + const vol = new Volume; + const { promises } = vol; + vol.fromJSON({ + '/foo': 'bar', + }); + await promises.symlink('/foo', '/bar'); + expect(vol.lstatSync('/bar').isSymbolicLink()).toEqual(true); + }); + it('Reject when file already exists', () => { + const vol = new Volume; + const { promises } = vol; + vol.fromJSON({ + '/foo': 'bar', + }); + return expect(promises.symlink('/bar', '/foo')).rejects.toBeInstanceOf(Error); + }); + }); + describe('truncate(path[, len])', () => { + const vol = new Volume; + const { promises } = vol; + vol.fromJSON({ + '/foo': '0123456789', + }); + it('Truncate an existing file', async () => { + await promises.truncate('/foo', 5); + expect(vol.readFileSync('/foo').toString()).toEqual('01234'); + }); + it('Reject when file does not exist', () => { + return expect(promises.truncate('/bar')).rejects.toBeInstanceOf(Error); + }); + }); + describe('unlink(path)', () => { + const vol = new Volume; + const { promises } = vol; + vol.fromJSON({ + '/foo': 'bar', + }); + it('Unlink an existing file', async () => { + await promises.unlink('/foo'); + expect(vol.existsSync('/foo')).toEqual(false); + }); + it('Reject when file does not exist', () => { + return expect(promises.unlink('/bar')).rejects.toBeInstanceOf(Error); + }); + }); + describe('utimes(path, atime, mtime)', () => { + const vol = new Volume; + const { promises } = vol; + vol.fromJSON({ + '/foo': 'bar', + }); + const fttDeparture = new Date(1985, 9, 26, 1, 21); + const fttArrival = new Date(fttDeparture.getTime() + 60000); + it('Changes times of an existing file', async () => { + await promises.utimes('/foo', fttArrival, fttDeparture); + const stats = vol.statSync('/foo'); + expect(stats.atime).toEqual(new Date(fttArrival)); + expect(stats.mtime).toEqual(new Date(fttDeparture)); + }); + it('Reject when file does not exist', () => { + return expect(promises.utimes('/bar', fttArrival, fttDeparture)).rejects.toBeInstanceOf(Error); + }); + }); + describe('writeFile(id, data[, options])', () => { + it('Write data to an existing file', async () => { + const vol = new Volume; + const { promises } = vol; + vol.fromJSON({ + '/foo': '', + }); + await promises.writeFile('/foo', 'bar'); + expect(vol.readFileSync('/foo').toString()).toEqual('bar'); + }); + it('Write data to existing file using FileHandle', async () => { + const vol = new Volume; + const { promises } = vol; + vol.fromJSON({ + '/foo': '', + }); + const fileHandle = await promises.open('/foo', 'w'); + await promises.writeFile(fileHandle, 'bar'); + expect(vol.readFileSync('/foo').toString()).toEqual('bar'); + await fileHandle.close(); + }); + it('Reject when trying to write on a directory', () => { + const vol = new Volume; + const { promises } = vol; + vol.fromJSON({ + '/foo': null, + }); + return expect(promises.writeFile('/foo', 'bar')).rejects.toBeInstanceOf(Error); + }); + }); +}); diff --git a/src/__tests__/volume.test.ts b/src/__tests__/volume.test.ts index 9232b7fed..dcb828ed9 100644 --- a/src/__tests__/volume.test.ts +++ b/src/__tests__/volume.test.ts @@ -419,7 +419,7 @@ describe('volume', () => { }); }); describe('.read(fd, buffer, offset, length, position, callback)', () => { - xit('...'); + xit('...', () => {}); }); describe('.readFileSync(path[, options])', () => { const vol = new Volume; @@ -595,7 +595,7 @@ describe('volume', () => { }); }); describe('.symlink(target, path[, type], callback)', () => { - xit('...'); + xit('...', () => {}); }); describe('.realpathSync(path[, options])', () => { const vol = new Volume; @@ -662,7 +662,7 @@ describe('volume', () => { }); }); describe('.lstat(path, callback)', () => { - xit('...'); + xit('...', () => {}); }); describe('.statSync(path)', () => { const vol = new Volume; @@ -695,7 +695,7 @@ describe('volume', () => { }); }); describe('.stat(path, callback)', () => { - xit('...'); + xit('...', () => {}); }); describe('.fstatSync(fd)', () => { const vol = new Volume; @@ -713,7 +713,7 @@ describe('volume', () => { }); }); describe('.fstat(fd, callback)', () => { - xit('...'); + xit('...', () => {}); }); describe('.linkSync(existingPath, newPath)', () => { const vol = new Volume; @@ -733,7 +733,7 @@ describe('volume', () => { }); }); describe('.link(existingPath, newPath, callback)', () => { - xit('...'); + xit('...', () => {}); }); describe('.readdirSync(path)', () => { it('Returns simple list', () => { @@ -760,7 +760,7 @@ describe('volume', () => { }); }); describe('.readdir(path, callback)', () => { - xit('...'); + xit('...', () => {}); }); describe('.readlinkSync(path[, options])', () => { it('Simple symbolic link to one file', () => { @@ -808,7 +808,7 @@ describe('volume', () => { }); }); describe('.ftruncate(fd[, len], callback)', () => { - xit('...'); + xit('...', () => {}); }); describe('.truncateSync(path[, len])', () => { const vol = new Volume; @@ -828,7 +828,7 @@ describe('volume', () => { }); }); describe('.truncate(path[, len], callback)', () => { - xit('...'); + xit('...', () => {}); }); describe('.utimesSync(path, atime, mtime)', () => { const vol = new Volume; @@ -878,7 +878,7 @@ describe('volume', () => { }); }); describe('.mkdir(path[, mode], callback)', () => { - xit('...'); + xit('...', () => {}); xit('Create /dir1/dir2/dir3', () => { }); }); describe('.mkdtempSync(prefix[, options])', () => { @@ -936,6 +936,12 @@ describe('volume', () => { }, 1); }); }); + describe('.promises', () => { + it('Have a promises property', () => { + const vol = new Volume; + expect(typeof vol.promises).toBe('object'); + }); + }); }); describe('StatWatcher', () => { it('.vol points to current volume', () => { diff --git a/src/index.ts b/src/index.ts index c9585ae3b..7e8d5b51b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ import {Stats, Dirent} from './node'; import {Volume as _Volume, StatWatcher, FSWatcher, toUnixTimestamp, IReadStream, IWriteStream} from './volume'; import * as volume from './volume'; +import { IPromisesAPI } from './promises'; const {fsSyncMethods, fsAsyncMethods} = require('fs-monkey/lib/util/lists'); import {constants} from './constants'; const {F_OK, R_OK, W_OK, X_OK} = constants; @@ -21,6 +22,7 @@ export interface IFs extends _Volume { FSWatcher: new () => FSWatcher, ReadStream: new (...args) => IReadStream, WriteStream: new (...args) => IWriteStream, + promises: IPromisesAPI, _toUnixTimestamp, } @@ -39,6 +41,7 @@ export function createFsFromVolume(vol: _Volume): IFs { fs.FSWatcher = vol.FSWatcher; fs.WriteStream = vol.WriteStream; fs.ReadStream = vol.ReadStream; + fs.promises = vol.promises; fs._toUnixTimestamp = toUnixTimestamp; diff --git a/src/node.ts b/src/node.ts index 2865de246..77cf8a6ed 100644 --- a/src/node.ts +++ b/src/node.ts @@ -154,7 +154,7 @@ export class Node extends EventEmitter { chmod(perm: number) { this.perm = perm; - this.mode |= perm; + this.mode = (this.mode & ~0o777) | perm; this.touch(); } diff --git a/src/promises.ts b/src/promises.ts new file mode 100644 index 000000000..af5beaea7 --- /dev/null +++ b/src/promises.ts @@ -0,0 +1,297 @@ +import { + Volume, + TFilePath, + TData, + TMode, + TFlags, + TFlagsCopy, + TSymlinkType, + TTime, + IOptions, + IAppendFileOptions, + IMkdirOptions, + IReaddirOptions, + IReadFileOptions, + IRealpathOptions, + IWriteFileOptions, +} from './volume'; +import { Stats, Dirent } from './node'; +import { TDataOut } from './encoding'; + +function promisify( + vol: Volume, + fn: string, + getResult: (result: any) => any = input => input, +): (...args) => Promise { + return (...args) => new Promise((resolve, reject) => { + vol[fn].bind(vol)(...args, (error, result) => { + if (error) return reject(error); + return resolve(getResult(result)); + }); + }); +} + +export type TFileHandleReadResult = { + bytesRead: number, + buffer: Buffer | Uint8Array, +}; + +export type TFileHandleWriteResult = { + bytesWritten: number, + buffer: Buffer | Uint8Array, +}; + +export interface IFileHandle { + fd: number; + appendFile(data: TData, options?: IAppendFileOptions | string): Promise, + chmod(mode: TMode): Promise, + chown(uid: number, gid: number): Promise, + close(): Promise, + datasync(): Promise, + read( + buffer: Buffer | Uint8Array, + offset: number, + length: number, + position: number, + ): Promise, + readFile(options?: IReadFileOptions|string): Promise, + stat(): Promise, + truncate(len?: number): Promise, + utimes(atime: TTime, mtime: TTime): Promise, + write( + buffer: Buffer | Uint8Array, + offset?: number, + length?: number, + position?: number, + ): Promise, + writeFile(data: TData, options?: IWriteFileOptions): Promise, +} + +export type TFileHandle = TFilePath | IFileHandle; + +export interface IPromisesAPI { + FileHandle, + access(path: TFilePath, mode?: number): Promise, + appendFile(path: TFileHandle, data: TData, options?: IAppendFileOptions | string): Promise, + chmod(path: TFilePath, mode: TMode): Promise, + chown(path: TFilePath, uid: number, gid: number): Promise, + copyFile(src: TFilePath, dest: TFilePath, flags?: TFlagsCopy): Promise, + lchmod(path: TFilePath, mode: TMode): Promise, + lchown(path: TFilePath, uid: number, gid: number): Promise, + link(existingPath: TFilePath, newPath: TFilePath): Promise, + lstat(path: TFilePath): Promise, + mkdir(path: TFilePath, options?: TMode | IMkdirOptions): Promise, + mkdtemp(prefix: string, options?: IOptions): Promise, + open(path: TFilePath, flags: TFlags, mode?: TMode): Promise, + readdir(path: TFilePath, options?: IReaddirOptions | string): Promise, + readFile(id: TFileHandle, options?: IReadFileOptions|string): Promise, + readlink(path: TFilePath, options?: IOptions): Promise, + realpath(path: TFilePath, options?: IRealpathOptions | string): Promise, + rename(oldPath: TFilePath, newPath: TFilePath): Promise, + rmdir(path: TFilePath): Promise, + stat(path: TFilePath): Promise, + symlink(target: TFilePath, path: TFilePath, type?: TSymlinkType): Promise, + truncate(path: TFilePath, len?: number): Promise, + unlink(path: TFilePath): Promise, + utimes(path: TFilePath, atime: TTime, mtime: TTime): Promise, + writeFile(id: TFileHandle, data: TData, options?: IWriteFileOptions): Promise, +} + +export class FileHandle implements IFileHandle { + private vol: Volume; + + fd: number; + + constructor(vol: Volume, fd: number) { + this.vol = vol; + this.fd = fd; + } + + appendFile(data: TData, options?: IAppendFileOptions | string): Promise { + return promisify(this.vol, 'appendFile')(this.fd, data, options); + } + + chmod(mode: TMode): Promise { + return promisify(this.vol, 'fchmod')(this.fd, mode); + } + + chown(uid: number, gid: number): Promise { + return promisify(this.vol, 'fchown')(this.fd, uid, gid); + } + + close(): Promise { + return promisify(this.vol, 'close')(this.fd); + } + + datasync(): Promise { + return promisify(this.vol, 'fdatasync')(this.fd); + } + + read( + buffer: Buffer | Uint8Array, + offset: number, + length: number, + position: number, + ): Promise { + return promisify(this.vol, 'read', bytesRead => ({ bytesRead, buffer }))( + this.fd, + buffer, + offset, + length, + position + ); + } + + readFile(options?: IReadFileOptions|string): Promise { + return promisify(this.vol, 'readFile')(this.fd, options); + } + + stat(): Promise { + return promisify(this.vol, 'fstat')(this.fd); + } + + sync(): Promise { + return promisify(this.vol, 'fsync')(this.fd); + } + + truncate(len?: number): Promise { + return promisify(this.vol, 'ftruncate')(this.fd, len); + } + + utimes(atime: TTime, mtime: TTime): Promise { + return promisify(this.vol, 'futimes')(this.fd, atime, mtime); + } + + write( + buffer: Buffer | Uint8Array, + offset?: number, + length?: number, + position?: number, + ): Promise { + return promisify(this.vol, 'write', bytesWritten => ({ bytesWritten, buffer }))( + this.fd, + buffer, + offset, + length, + position, + ); + } + + writeFile(data: TData, options?: IWriteFileOptions): Promise { + return promisify(this.vol, 'writeFile')(this.fd, data, options); + } +} + +export default function createPromisesApi(vol: Volume): null | IPromisesAPI { + if (typeof Promise === 'undefined') return null; + return { + FileHandle, + + access(path: TFilePath, mode?: number): Promise { + return promisify(vol, 'access')(path, mode); + }, + + appendFile(path: TFileHandle, data: TData, options?: IAppendFileOptions | string): Promise { + return promisify(vol, 'appendFile')( + path instanceof FileHandle ? path.fd : path as TFilePath, + data, + options, + ); + }, + + chmod(path: TFilePath, mode: TMode): Promise { + return promisify(vol, 'chmod')(path, mode); + }, + + chown(path: TFilePath, uid: number, gid: number): Promise { + return promisify(vol, 'chown')(path, uid, gid); + }, + + copyFile(src: TFilePath, dest: TFilePath, flags?: TFlagsCopy): Promise { + return promisify(vol, 'copyFile')(src, dest, flags); + }, + + lchmod(path: TFilePath, mode: TMode): Promise { + return promisify(vol, 'lchmod')(path, mode); + }, + + lchown(path: TFilePath, uid: number, gid: number): Promise { + return promisify(vol, 'lchown')(path, uid, gid); + }, + + link(existingPath: TFilePath, newPath: TFilePath): Promise { + return promisify(vol, 'link')(existingPath, newPath); + }, + + lstat(path: TFilePath): Promise { + return promisify(vol, 'lstat')(path); + }, + + mkdir(path: TFilePath, options?: TMode | IMkdirOptions): Promise { + return promisify(vol, 'mkdir')(path, options); + }, + + mkdtemp(prefix: string, options?: IOptions): Promise { + return promisify(vol, 'mkdtemp')(prefix, options); + }, + + open(path: TFilePath, flags: TFlags, mode?: TMode): Promise { + return promisify(vol, 'open', fd => new FileHandle(vol, fd))(path, flags, mode); + }, + + readdir(path: TFilePath, options?: IReaddirOptions | string): Promise { + return promisify(vol, 'readdir')(path, options); + }, + + readFile(id: TFileHandle, options?: IReadFileOptions|string): Promise { + return promisify(vol, 'readFile')( + id instanceof FileHandle ? id.fd : id as TFilePath, + options, + ); + }, + + readlink(path: TFilePath, options?: IOptions): Promise { + return promisify(vol, 'readlink')(path, options); + }, + + realpath(path: TFilePath, options?: IRealpathOptions | string): Promise { + return promisify(vol, 'realpath')(path, options); + }, + + rename(oldPath: TFilePath, newPath: TFilePath): Promise { + return promisify(vol, 'rename')(oldPath, newPath); + }, + + rmdir(path: TFilePath): Promise { + return promisify(vol, 'rmdir')(path); + }, + + stat(path: TFilePath): Promise { + return promisify(vol, 'stat')(path); + }, + + symlink(target: TFilePath, path: TFilePath, type?: TSymlinkType): Promise { + return promisify(vol, 'symlink')(target, path, type); + }, + + truncate(path: TFilePath, len?: number): Promise { + return promisify(vol, 'truncate')(path, len); + }, + + unlink(path: TFilePath): Promise { + return promisify(vol, 'unlink')(path); + }, + + utimes(path: TFilePath, atime: TTime, mtime: TTime): Promise { + return promisify(vol, 'utimes')(path, atime, mtime); + }, + + writeFile(id: TFileHandle, data: TData, options?: IWriteFileOptions): Promise { + return promisify(vol, 'writeFile')( + id instanceof FileHandle ? id.fd : id as TFilePath, + data, + options, + ); + }, + }; +} diff --git a/src/volume.ts b/src/volume.ts index 3d4332f06..ec4ca5722 100644 --- a/src/volume.ts +++ b/src/volume.ts @@ -12,6 +12,7 @@ import {TEncoding, TEncodingExtended, TDataOut, assertEncoding, strToEncoding, E import errors = require('./internal/errors'); import extend = require('fast-extend'); import util = require('util'); +import createPromisesApi from './promises'; const {O_RDONLY, O_WRONLY, O_RDWR, O_CREAT, O_EXCL, O_NOCTTY, O_TRUNC, O_APPEND, O_DIRECTORY, O_NOATIME, O_NOFOLLOW, O_SYNC, O_DIRECT, O_NONBLOCK, @@ -49,7 +50,7 @@ export type TTime = number | string | Date; export type TCallback = (error?: IError, data?: TData) => void; // type TCallbackWrite = (err?: IError, bytesWritten?: number, source?: Buffer) => void; // type TCallbackWriteStr = (err?: IError, written?: number, str?: string) => void; - +export type TSymlinkType = 'file' | 'dir' | 'junction'; // ---------------------------------------- Constants @@ -465,15 +466,15 @@ export function toUnixTimestamp(time) { if(typeof time === 'string' && (+time == (time as any))) { return +time; } + if(time instanceof Date) { + return time.getTime() / 1000; + } if(isFinite(time)) { if (time < 0) { return Date.now() / 1000; } return time; } - if(time instanceof Date) { - return time.getTime() / 1000; - } throw new Error('Cannot parse time: ' + time); } @@ -485,7 +486,7 @@ export function toUnixTimestamp(time) { */ function getArgAndCb(arg: TArg | TCallback, callback?: TCallback, def?: TArg): [TArg, TCallback] { return typeof arg === 'function' - ? [def, arg] + ? [def, arg as TCallback] : [arg, callback]; } @@ -501,6 +502,8 @@ function validateGid(gid: number) { // ---------------------------------------- Volume +let promisesWarn = true; + /** * `Volume` represents a file system. */ @@ -562,6 +565,20 @@ export class Volume { File: new (...File) => File, }; + private promisesApi = createPromisesApi(this); + + get promises() { + if (promisesWarn) { + promisesWarn = false; + require('process').emitWarning( + 'The fs.promises API is experimental', + 'ExperimentalWarning', + ); + } + if (this.promisesApi === null) throw new Error('Promise is not supported in this environment.'); + return this.promisesApi; + } + constructor(props = {}) { this.props = extend({Node, Link, File}, props); @@ -904,10 +921,10 @@ export class Volume { private openFile(filename: string, flagsNum: number, modeNum: number, resolveSymlinks: boolean = true): File { const steps = filenameToSteps(filename); - let link: Link = this.getResolvedLink(steps); + let link: Link = resolveSymlinks ? this.getResolvedLink(steps) : this.getLink(steps); // Try creating a new file, if it does not exist. - if(!link) { + if(!link && (flagsNum & O_CREAT)) { // const dirLink: Link = this.getLinkParent(steps); const dirLink: Link = this.getResolvedLink(steps.slice(0, steps.length - 1)); // if(!dirLink) throwError(ENOENT, 'open', filename); @@ -919,6 +936,7 @@ export class Volume { } if(link) return this.openLink(link, flagsNum, resolveSymlinks); + throwError(ENOENT, 'open', filename); } private openBase(filename: string, flagsNum: number, modeNum: number, resolveSymlinks: boolean = true): number { @@ -1361,16 +1379,16 @@ export class Volume { } // `type` argument works only on Windows. - symlinkSync(target: TFilePath, path: TFilePath, type?: 'file' | 'dir' | 'junction') { + symlinkSync(target: TFilePath, path: TFilePath, type?: TSymlinkType) { const targetFilename = pathToFilename(target); const pathFilename = pathToFilename(path); this.symlinkBase(targetFilename, pathFilename); } symlink(target: TFilePath, path: TFilePath, callback: TCallback); - symlink(target: TFilePath, path: TFilePath, type: 'file' | 'dir' | 'junction', callback: TCallback); + symlink(target: TFilePath, path: TFilePath, type: TSymlinkType, callback: TCallback); symlink(target: TFilePath, path: TFilePath, a, b?) { - const [type, callback] = getArgAndCb<'file' | 'dir' | 'junction', TCallback>(a, b); + const [type, callback] = getArgAndCb>(a, b); const targetFilename = pathToFilename(target); const pathFilename = pathToFilename(path); this.wrapAsync(this.symlinkBase, [targetFilename, pathFilename], callback); @@ -1389,7 +1407,7 @@ export class Volume { return strToEncoding(realLink.getPath(), encoding); } - realpathSync(path: TFilePath, options?: IRealpathOptions): TDataOut { + realpathSync(path: TFilePath, options?: IRealpathOptions | string): TDataOut { return this.realpathBase(pathToFilename(path), getRealpathOptions(options).encoding); } diff --git a/tsconfig.json b/tsconfig.json index c7ae59b3e..bc1acc210 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { "target": "es5", + "lib": ["es5", "dom", "es2015.promise"], "module": "commonjs", "removeComments": false, "noImplicitAny": false,