diff --git a/README.md b/README.md index 2f6cf690..7341a4a1 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,7 @@ Methods - [copy](docs/copy.md) - [emptyDir](docs/emptyDir.md) +- [isEmptyDir](docs/isEmptyDir.md) - [ensureFile](docs/ensureFile.md) - [ensureDir](docs/ensureDir.md) - [ensureLink](docs/ensureLink.md) @@ -155,6 +156,7 @@ Methods - [copySync](docs/copy-sync.md) - [emptyDirSync](docs/emptyDir-sync.md) +- [isEmptyDirSync](docs/isEmptyDir-sync.md) - [ensureFileSync](docs/ensureFile-sync.md) - [ensureDirSync](docs/ensureDir-sync.md) - [ensureLinkSync](docs/ensureLink-sync.md) diff --git a/docs/isEmptyDir-sync.md b/docs/isEmptyDir-sync.md new file mode 100644 index 00000000..410db57c --- /dev/null +++ b/docs/isEmptyDir-sync.md @@ -0,0 +1,14 @@ +# isEmptyDirSync(dir) + +Returns `true` if the directory is empty, or `false` if it contains at least one entry. Only the first directory entry is read. + +- `dir` `` + +## Example: + +```js +const fs = require('fs-extra') + +const empty = fs.isEmptyDirSync('/tmp/some/dir') +console.log(empty ? 'empty' : 'not empty') +``` diff --git a/docs/isEmptyDir.md b/docs/isEmptyDir.md new file mode 100644 index 00000000..71885259 --- /dev/null +++ b/docs/isEmptyDir.md @@ -0,0 +1,29 @@ +# isEmptyDir(dir[, callback]) + +Returns a promise that resolves to `true` if the directory is empty, or `false` if it contains at least one entry. Only the first directory entry is read. + +- `dir` `` +- `callback` `` + - `err` `` + - `empty` `` + +## Example: + +```js +const fs = require('fs-extra') + +fs.isEmptyDir('/tmp/some/dir', (err, empty) => { + if (err) return console.error(err) + console.log(empty ? 'empty' : 'not empty') +}) +``` + +```js +const fs = require('fs-extra') + +fs.isEmptyDir('/tmp/some/dir') + .then(empty => { + console.log(empty ? 'empty' : 'not empty') + }) + .catch(err => console.error(err)) +``` diff --git a/lib/esm.mjs b/lib/esm.mjs index 27b7a1f7..4948f00b 100644 --- a/lib/esm.mjs +++ b/lib/esm.mjs @@ -1,6 +1,7 @@ import _copy from './copy/index.js' import _empty from './empty/index.js' import _ensure from './ensure/index.js' +import _isEmptyDir from './is-empty-dir/index.js' import _json from './json/index.js' import _mkdirs from './mkdirs/index.js' import _move from './move/index.js' @@ -28,6 +29,8 @@ export const createSymlink = _ensure.createSymlink export const createSymlinkSync = _ensure.createSymlinkSync export const ensureSymlink = _ensure.ensureSymlink export const ensureSymlinkSync = _ensure.ensureSymlinkSync +export const isEmptyDir = _isEmptyDir.isEmptyDir +export const isEmptyDirSync = _isEmptyDir.isEmptyDirSync export const readJson = _json.readJson export const readJSON = _json.readJSON export const readJsonSync = _json.readJsonSync @@ -59,6 +62,7 @@ export default { ..._copy, ..._empty, ..._ensure, + ..._isEmptyDir, ..._json, ..._mkdirs, ..._move, diff --git a/lib/index.js b/lib/index.js index da6711a4..f22a4d8f 100644 --- a/lib/index.js +++ b/lib/index.js @@ -7,6 +7,7 @@ module.exports = { ...require('./copy'), ...require('./empty'), ...require('./ensure'), + ...require('./is-empty-dir'), ...require('./json'), ...require('./mkdirs'), ...require('./move'), diff --git a/lib/is-empty-dir/__tests__/is-empty-dir.test.js b/lib/is-empty-dir/__tests__/is-empty-dir.test.js new file mode 100644 index 00000000..f440ec2b --- /dev/null +++ b/lib/is-empty-dir/__tests__/is-empty-dir.test.js @@ -0,0 +1,192 @@ +'use strict' + +const fs = require('fs') +const os = require('os') +const fse = require('../..') +const path = require('path') +const assert = require('assert') +const proxyquire = require('proxyquire') + +const { describe, it, beforeEach, afterEach } = global + +describe('+ isEmptyDir()', () => { + let TEST_DIR + + beforeEach(() => { + TEST_DIR = path.join(os.tmpdir(), 'test-fs-extra', 'is-empty-dir') + if (fs.existsSync(TEST_DIR)) { + fse.removeSync(TEST_DIR) + } + fse.ensureDirSync(TEST_DIR) + }) + + afterEach(done => fse.remove(TEST_DIR, done)) + + it('should return true for an empty directory', async () => { + assert.strictEqual(await fse.isEmptyDir(TEST_DIR), true) + }) + + it('should return false for a nonempty directory', async () => { + fs.writeFileSync(path.join(TEST_DIR, 'file'), '') + assert.strictEqual(await fse.isEmptyDir(TEST_DIR), false) + }) + + it('should support callbacks', done => { + fse.isEmptyDir(TEST_DIR, (err, result) => { + assert.ifError(err) + assert.strictEqual(result, true) + done() + }) + }) + + it('should reject missing paths', async () => { + await assert.rejects(fse.isEmptyDir(path.join(TEST_DIR, 'missing')), err => err.code === 'ENOENT') + }) + + it('should reject file paths', async () => { + const file = path.join(TEST_DIR, 'file') + fs.writeFileSync(file, '') + await assert.rejects(fse.isEmptyDir(file), err => err.code === 'ENOTDIR') + }) + + it('should support symlinks to directories', async () => { + const target = path.join(TEST_DIR, 'target') + const symlink = path.join(TEST_DIR, 'symlink') + fs.mkdirSync(target) + fs.symlinkSync(target, symlink) + assert.strictEqual(await fse.isEmptyDir(symlink), true) + }) + + it('should preserve read errors and close the directory', async () => { + const error = Object.assign(new Error('read failed'), { code: 'EIO' }) + let reads = 0 + let closes = 0 + const { isEmptyDir } = proxyquire('..', { + '../fs': { + opendir: async () => ({ + read: callback => { + reads++ + callback(error) + }, + close: callback => { + closes++ + callback() + } + }) + } + }) + + await assert.rejects(isEmptyDir(TEST_DIR), err => err === error) + assert.strictEqual(reads, 1) + assert.strictEqual(closes, 1) + }) + + it('should close the directory after reading the first entry', async () => { + let reads = 0 + let closes = 0 + const { isEmptyDir } = proxyquire('..', { + '../fs': { + opendir: async () => ({ + read: callback => { + reads++ + callback(null, { name: 'file' }) + }, + close: callback => { + closes++ + callback() + } + }) + } + }) + + assert.strictEqual(await isEmptyDir(TEST_DIR), false) + assert.strictEqual(reads, 1) + assert.strictEqual(closes, 1) + }) +}) + +describe('+ isEmptyDirSync()', () => { + let TEST_DIR + + beforeEach(() => { + TEST_DIR = path.join(os.tmpdir(), 'test-fs-extra', 'is-empty-dir-sync') + if (fs.existsSync(TEST_DIR)) { + fse.removeSync(TEST_DIR) + } + fse.ensureDirSync(TEST_DIR) + }) + + afterEach(done => fse.remove(TEST_DIR, done)) + + it('should return true for an empty directory', () => { + assert.strictEqual(fse.isEmptyDirSync(TEST_DIR), true) + }) + + it('should return false for a nonempty directory', () => { + fs.writeFileSync(path.join(TEST_DIR, 'file'), '') + assert.strictEqual(fse.isEmptyDirSync(TEST_DIR), false) + }) + + it('should throw for missing paths', () => { + assert.throws(() => fse.isEmptyDirSync(path.join(TEST_DIR, 'missing')), err => err.code === 'ENOENT') + }) + + it('should throw for file paths', () => { + const file = path.join(TEST_DIR, 'file') + fs.writeFileSync(file, '') + assert.throws(() => fse.isEmptyDirSync(file), err => err.code === 'ENOTDIR') + }) + + it('should support symlinks to directories', () => { + const target = path.join(TEST_DIR, 'target') + const symlink = path.join(TEST_DIR, 'symlink') + fs.mkdirSync(target) + fs.symlinkSync(target, symlink) + assert.strictEqual(fse.isEmptyDirSync(symlink), true) + }) + + it('should preserve read errors and close the directory', () => { + const error = Object.assign(new Error('read failed'), { code: 'EIO' }) + let reads = 0 + let closes = 0 + const { isEmptyDirSync } = proxyquire('..', { + '../fs': { + opendirSync: () => ({ + readSync: () => { + reads++ + throw error + }, + closeSync: () => { + closes++ + } + }) + } + }) + + assert.throws(() => isEmptyDirSync(TEST_DIR), err => err === error) + assert.strictEqual(reads, 1) + assert.strictEqual(closes, 1) + }) + + it('should close the directory after reading the first entry', () => { + let reads = 0 + let closes = 0 + const { isEmptyDirSync } = proxyquire('..', { + '../fs': { + opendirSync: () => ({ + readSync: () => { + reads++ + return { name: 'file' } + }, + closeSync: () => { + closes++ + } + }) + } + }) + + assert.strictEqual(isEmptyDirSync(TEST_DIR), false) + assert.strictEqual(reads, 1) + assert.strictEqual(closes, 1) + }) +}) diff --git a/lib/is-empty-dir/index.js b/lib/is-empty-dir/index.js new file mode 100644 index 00000000..1ffc6607 --- /dev/null +++ b/lib/is-empty-dir/index.js @@ -0,0 +1,69 @@ +'use strict' + +const u = require('universalify').fromPromise +const fs = require('../fs') + +function readFirst (dir) { + return new Promise((resolve, reject) => { + dir.read((err, dirent) => { + if (err) return reject(err) + resolve(dirent) + }) + }) +} + +function close (dir) { + return new Promise((resolve, reject) => { + dir.close(err => { + if (err) return reject(err) + resolve() + }) + }) +} + +async function isEmptyDir (dir) { + const handle = await fs.opendir(dir) + let result + let error + + try { + result = (await readFirst(handle)) === null + } catch (err) { + error = err + } + + try { + await close(handle) + } catch (err) { + if (!error) error = err + } + + if (error) throw error + return result +} + +function isEmptyDirSync (dir) { + const handle = fs.opendirSync(dir) + let result + let error + + try { + result = handle.readSync() === null + } catch (err) { + error = err + } + + try { + handle.closeSync() + } catch (err) { + if (!error) error = err + } + + if (error) throw error + return result +} + +module.exports = { + isEmptyDir: u(isEmptyDir), + isEmptyDirSync +} diff --git a/test.mjs b/test.mjs index 333f9fde..59c02583 100644 --- a/test.mjs +++ b/test.mjs @@ -22,6 +22,8 @@ import { createSymlinkSync, ensureSymlink, ensureSymlinkSync, + isEmptyDir, + isEmptyDirSync, readJson, readJsonSync, writeJson, @@ -68,6 +70,8 @@ const fsNamed = [ createSymlinkSync, ensureSymlink, ensureSymlinkSync, + isEmptyDir, + isEmptyDirSync, readJson, readJsonSync, writeJson,