From b1b0b5cf234e8fb87673b3c630d6d206026e0e00 Mon Sep 17 00:00:00 2001 From: nlf Date: Mon, 10 Oct 2022 13:43:18 -0700 Subject: [PATCH] feat: remove owner related code as well as stale polyfills BREAKING CHANGE: this removes the `owner` option from all methods that previously supported it, as well as the `withOwner` and `withOwnerSync` methods --- README.md | 22 +- lib/common/owner-sync.js | 96 ------- lib/common/owner.js | 96 ------- lib/copy-file.js | 16 -- lib/{ => cp}/errors.js | 0 lib/cp/polyfill.js | 2 +- lib/index.js | 7 - lib/mkdir.js | 19 -- lib/mkdtemp.js | 23 -- lib/rm/index.js | 22 -- lib/rm/polyfill.js | 239 ---------------- lib/with-owner-sync.js | 21 -- lib/with-owner.js | 21 -- lib/with-temp-dir.js | 10 +- lib/write-file.js | 14 - test/common/owner-sync.js | 232 --------------- test/common/owner.js | 228 --------------- test/copy-file.js | 53 ---- test/{ => cp}/errors.js | 2 +- test/mkdir.js | 12 - test/mkdtemp.js | 29 -- test/rm/index.js | 26 -- test/rm/polyfill.js | 588 -------------------------------------- test/with-owner.js | 117 -------- test/write-file.js | 39 --- 25 files changed, 7 insertions(+), 1927 deletions(-) delete mode 100644 lib/common/owner-sync.js delete mode 100644 lib/common/owner.js delete mode 100644 lib/copy-file.js rename lib/{ => cp}/errors.js (100%) delete mode 100644 lib/mkdir.js delete mode 100644 lib/mkdtemp.js delete mode 100644 lib/rm/index.js delete mode 100644 lib/rm/polyfill.js delete mode 100644 lib/with-owner-sync.js delete mode 100644 lib/with-owner.js delete mode 100644 lib/write-file.js delete mode 100644 test/common/owner-sync.js delete mode 100644 test/common/owner.js delete mode 100644 test/copy-file.js rename test/{ => cp}/errors.js (97%) delete mode 100644 test/mkdir.js delete mode 100644 test/mkdtemp.js delete mode 100644 test/rm/index.js delete mode 100644 test/rm/polyfill.js delete mode 100644 test/with-owner.js delete mode 100644 test/write-file.js diff --git a/README.md b/README.md index 0642968..c1ff757 100644 --- a/README.md +++ b/README.md @@ -5,28 +5,8 @@ polyfills, and extensions, of the core `fs` module. ## Features - all exposed functions return promises -- `fs.rm` polyfill for node versions < 14.14.0 -- `fs.mkdir` polyfill adding support for the `recursive` and `force` options in node versions < 10.12.0 -- `fs.copyFile` extended to accept an `owner` option -- `fs.mkdir` extended to accept an `owner` option -- `fs.mkdtemp` extended to accept an `owner` option -- `fs.writeFile` extended to accept an `owner` option -- `fs.withTempDir` added -- `fs.withOwner` added -- `fs.withOwnerSync` added - `fs.cp` polyfill for node < 16.7.0 - -## The `owner` option - -The `copyFile`, `mkdir`, `mkdtemp`, `writeFile`, and `withTempDir` functions -all accept a new `owner` property in their options. It can be used in two ways: - -- `{ owner: { uid: 100, gid: 100 } }` - set the `uid` and `gid` explicitly -- `{ owner: 100 }` - use one value, will set both `uid` and `gid` the same - -The special string `'inherit'` may be passed instead of a number, which will -cause this module to automatically determine the correct `uid` and/or `gid` -from the nearest existing parent directory of the target. +- `fs.withTempDir` added ## `fs.withTempDir(root, fn, options) -> Promise` diff --git a/lib/common/owner-sync.js b/lib/common/owner-sync.js deleted file mode 100644 index 3704aa6..0000000 --- a/lib/common/owner-sync.js +++ /dev/null @@ -1,96 +0,0 @@ -const { dirname, resolve } = require('path') -const url = require('url') - -const fs = require('../fs.js') - -// given a path, find the owner of the nearest parent -const find = (path) => { - // if we have no getuid, permissions are irrelevant on this platform - if (!process.getuid) { - return {} - } - - // fs methods accept URL objects with a scheme of file: so we need to unwrap - // those into an actual path string before we can resolve it - const resolved = path != null && path.href && path.origin - ? resolve(url.fileURLToPath(path)) - : resolve(path) - - let stat - - try { - stat = fs.lstatSync(resolved) - } finally { - // if we got a stat, return its contents - if (stat) { - return { uid: stat.uid, gid: stat.gid } - } - - // try the parent directory - if (resolved !== dirname(resolved)) { - return find(dirname(resolved)) - } - - // no more parents, never got a stat, just return an empty object - return {} - } -} - -// given a path, uid, and gid update the ownership of the path if necessary -const update = (path, uid, gid) => { - // nothing to update, just exit - if (uid === undefined && gid === undefined) { - return - } - - try { - // see if the permissions are already the same, if they are we don't - // need to do anything, so return early - const stat = fs.statSync(path) - if (uid === stat.uid && gid === stat.gid) { - return - } - } catch { - // ignore errors - } - - try { - fs.chownSync(path, uid, gid) - } catch { - // ignore errors - } -} - -// accepts a `path` and the `owner` property of an options object and normalizes -// it into an object with numerical `uid` and `gid` -const validate = (path, input) => { - let uid - let gid - - if (typeof input === 'string' || typeof input === 'number') { - uid = input - gid = input - } else if (input && typeof input === 'object') { - uid = input.uid - gid = input.gid - } - - if (uid === 'inherit' || gid === 'inherit') { - const owner = find(path) - if (uid === 'inherit') { - uid = owner.uid - } - - if (gid === 'inherit') { - gid = owner.gid - } - } - - return { uid, gid } -} - -module.exports = { - find, - update, - validate, -} diff --git a/lib/common/owner.js b/lib/common/owner.js deleted file mode 100644 index 9f02d41..0000000 --- a/lib/common/owner.js +++ /dev/null @@ -1,96 +0,0 @@ -const { dirname, resolve } = require('path') -const url = require('url') - -const fs = require('../fs.js') - -// given a path, find the owner of the nearest parent -const find = async (path) => { - // if we have no getuid, permissions are irrelevant on this platform - if (!process.getuid) { - return {} - } - - // fs methods accept URL objects with a scheme of file: so we need to unwrap - // those into an actual path string before we can resolve it - const resolved = path != null && path.href && path.origin - ? resolve(url.fileURLToPath(path)) - : resolve(path) - - let stat - - try { - stat = await fs.lstat(resolved) - } finally { - // if we got a stat, return its contents - if (stat) { - return { uid: stat.uid, gid: stat.gid } - } - - // try the parent directory - if (resolved !== dirname(resolved)) { - return find(dirname(resolved)) - } - - // no more parents, never got a stat, just return an empty object - return {} - } -} - -// given a path, uid, and gid update the ownership of the path if necessary -const update = async (path, uid, gid) => { - // nothing to update, just exit - if (uid === undefined && gid === undefined) { - return - } - - try { - // see if the permissions are already the same, if they are we don't - // need to do anything, so return early - const stat = await fs.stat(path) - if (uid === stat.uid && gid === stat.gid) { - return - } - } catch { - // ignore errors - } - - try { - await fs.chown(path, uid, gid) - } catch { - // ignore errors - } -} - -// accepts a `path` and the `owner` property of an options object and normalizes -// it into an object with numerical `uid` and `gid` -const validate = async (path, input) => { - let uid - let gid - - if (typeof input === 'string' || typeof input === 'number') { - uid = input - gid = input - } else if (input && typeof input === 'object') { - uid = input.uid - gid = input.gid - } - - if (uid === 'inherit' || gid === 'inherit') { - const owner = await find(path) - if (uid === 'inherit') { - uid = owner.uid - } - - if (gid === 'inherit') { - gid = owner.gid - } - } - - return { uid, gid } -} - -module.exports = { - find, - update, - validate, -} diff --git a/lib/copy-file.js b/lib/copy-file.js deleted file mode 100644 index 8888266..0000000 --- a/lib/copy-file.js +++ /dev/null @@ -1,16 +0,0 @@ -const fs = require('./fs.js') -const getOptions = require('./common/get-options.js') -const withOwner = require('./with-owner.js') - -const copyFile = async (src, dest, opts) => { - const options = getOptions(opts, { - copy: ['mode'], - wrap: 'mode', - }) - - // the node core method as of 16.5.0 does not support the mode being in an - // object, so we have to pass the mode value directly - return withOwner(dest, () => fs.copyFile(src, dest, options.mode), opts) -} - -module.exports = copyFile diff --git a/lib/errors.js b/lib/cp/errors.js similarity index 100% rename from lib/errors.js rename to lib/cp/errors.js diff --git a/lib/cp/polyfill.js b/lib/cp/polyfill.js index f83ccbf..1a3a1f2 100644 --- a/lib/cp/polyfill.js +++ b/lib/cp/polyfill.js @@ -23,7 +23,7 @@ const { ERR_FS_CP_UNKNOWN, ERR_FS_EISDIR, ERR_INVALID_ARG_TYPE, -} = require('../errors.js') +} = require('./errors.js') const { constants: { errno: { diff --git a/lib/index.js b/lib/index.js index 3a98648..b546fef 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,12 +1,5 @@ module.exports = { ...require('./fs.js'), - copyFile: require('./copy-file.js'), cp: require('./cp/index.js'), - mkdir: require('./mkdir.js'), - mkdtemp: require('./mkdtemp.js'), - rm: require('./rm/index.js'), withTempDir: require('./with-temp-dir.js'), - withOwner: require('./with-owner.js'), - withOwnerSync: require('./with-owner-sync.js'), - writeFile: require('./write-file.js'), } diff --git a/lib/mkdir.js b/lib/mkdir.js deleted file mode 100644 index 098d8d0..0000000 --- a/lib/mkdir.js +++ /dev/null @@ -1,19 +0,0 @@ -const fs = require('./fs.js') -const getOptions = require('./common/get-options.js') -const withOwner = require('./with-owner.js') - -// extends mkdir with the ability to specify an owner of the new dir -const mkdir = async (path, opts) => { - const options = getOptions(opts, { - copy: ['mode', 'recursive'], - wrap: 'mode', - }) - - return withOwner( - path, - () => fs.mkdir(path, options), - opts - ) -} - -module.exports = mkdir diff --git a/lib/mkdtemp.js b/lib/mkdtemp.js deleted file mode 100644 index 60b12a7..0000000 --- a/lib/mkdtemp.js +++ /dev/null @@ -1,23 +0,0 @@ -const { dirname, sep } = require('path') - -const fs = require('./fs.js') -const getOptions = require('./common/get-options.js') -const withOwner = require('./with-owner.js') - -const mkdtemp = async (prefix, opts) => { - const options = getOptions(opts, { - copy: ['encoding'], - wrap: 'encoding', - }) - - // mkdtemp relies on the trailing path separator to indicate if it should - // create a directory inside of the prefix. if that's the case then the root - // we infer ownership from is the prefix itself, otherwise it's the dirname - // /tmp -> /tmpABCDEF, infers from / - // /tmp/ -> /tmp/ABCDEF, infers from /tmp - const root = prefix.endsWith(sep) ? prefix : dirname(prefix) - - return withOwner(root, () => fs.mkdtemp(prefix, options), opts) -} - -module.exports = mkdtemp diff --git a/lib/rm/index.js b/lib/rm/index.js deleted file mode 100644 index cb81fbd..0000000 --- a/lib/rm/index.js +++ /dev/null @@ -1,22 +0,0 @@ -const fs = require('../fs.js') -const getOptions = require('../common/get-options.js') -const node = require('../common/node.js') -const polyfill = require('./polyfill.js') - -// node 14.14.0 added fs.rm, which allows both the force and recursive options -const useNative = node.satisfies('>=14.14.0') - -const rm = async (path, opts) => { - const options = getOptions(opts, { - copy: ['retryDelay', 'maxRetries', 'recursive', 'force'], - }) - - // the polyfill is tested separately from this module, no need to hack - // process.version to try to trigger it just for coverage - // istanbul ignore next - return useNative - ? fs.rm(path, options) - : polyfill(path, options) -} - -module.exports = rm diff --git a/lib/rm/polyfill.js b/lib/rm/polyfill.js deleted file mode 100644 index a25c174..0000000 --- a/lib/rm/polyfill.js +++ /dev/null @@ -1,239 +0,0 @@ -// this file is a modified version of the code in node core >=14.14.0 -// which is, in turn, a modified version of the rimraf module on npm -// node core changes: -// - Use of the assert module has been replaced with core's error system. -// - All code related to the glob dependency has been removed. -// - Bring your own custom fs module is not currently supported. -// - Some basic code cleanup. -// changes here: -// - remove all callback related code -// - drop sync support -// - change assertions back to non-internal methods (see options.js) -// - throws ENOTDIR when rmdir gets an ENOENT for a path that exists in Windows -const errnos = require('os').constants.errno -const { join } = require('path') -const fs = require('../fs.js') - -// error codes that mean we need to remove contents -const notEmptyCodes = new Set([ - 'ENOTEMPTY', - 'EEXIST', - 'EPERM', -]) - -// error codes we can retry later -const retryCodes = new Set([ - 'EBUSY', - 'EMFILE', - 'ENFILE', - 'ENOTEMPTY', - 'EPERM', -]) - -const isWindows = process.platform === 'win32' - -const defaultOptions = { - retryDelay: 100, - maxRetries: 0, - recursive: false, - force: false, -} - -// this is drastically simplified, but should be roughly equivalent to what -// node core throws -class ERR_FS_EISDIR extends Error { - constructor (path) { - super() - this.info = { - code: 'EISDIR', - message: 'is a directory', - path, - syscall: 'rm', - errno: errnos.EISDIR, - } - this.name = 'SystemError' - this.code = 'ERR_FS_EISDIR' - this.errno = errnos.EISDIR - this.syscall = 'rm' - this.path = path - this.message = `Path is a directory: ${this.syscall} returned ` + - `${this.info.code} (is a directory) ${path}` - } - - toString () { - return `${this.name} [${this.code}]: ${this.message}` - } -} - -class ENOTDIR extends Error { - constructor (path) { - super() - this.name = 'Error' - this.code = 'ENOTDIR' - this.errno = errnos.ENOTDIR - this.syscall = 'rmdir' - this.path = path - this.message = `not a directory, ${this.syscall} '${this.path}'` - } - - toString () { - return `${this.name}: ${this.code}: ${this.message}` - } -} - -// force is passed separately here because we respect it for the first entry -// into rimraf only, any further calls that are spawned as a result (i.e. to -// delete content within the target) will ignore ENOENT errors -const rimraf = async (path, options, isTop = false) => { - const force = isTop ? options.force : true - const stat = await fs.lstat(path) - .catch((err) => { - // we only ignore ENOENT if we're forcing this call - if (err.code === 'ENOENT' && force) { - return - } - - if (isWindows && err.code === 'EPERM') { - return fixEPERM(path, options, err, isTop) - } - - throw err - }) - - // no stat object here means either lstat threw an ENOENT, or lstat threw - // an EPERM and the fixPERM function took care of things. either way, we're - // already done, so return early - if (!stat) { - return - } - - if (stat.isDirectory()) { - return rmdir(path, options, null, isTop) - } - - return fs.unlink(path) - .catch((err) => { - if (err.code === 'ENOENT' && force) { - return - } - - if (err.code === 'EISDIR') { - return rmdir(path, options, err, isTop) - } - - if (err.code === 'EPERM') { - // in windows, we handle this through fixEPERM which will also try to - // delete things again. everywhere else since deleting the target as a - // file didn't work we go ahead and try to delete it as a directory - return isWindows - ? fixEPERM(path, options, err, isTop) - : rmdir(path, options, err, isTop) - } - - throw err - }) -} - -const fixEPERM = async (path, options, originalErr, isTop) => { - const force = isTop ? options.force : true - const targetMissing = await fs.chmod(path, 0o666) - .catch((err) => { - if (err.code === 'ENOENT' && force) { - return true - } - - throw originalErr - }) - - // got an ENOENT above, return now. no file = no problem - if (targetMissing) { - return - } - - // this function does its own lstat rather than calling rimraf again to avoid - // infinite recursion for a repeating EPERM - const stat = await fs.lstat(path) - .catch((err) => { - if (err.code === 'ENOENT' && force) { - return - } - - throw originalErr - }) - - if (!stat) { - return - } - - if (stat.isDirectory()) { - return rmdir(path, options, originalErr, isTop) - } - - return fs.unlink(path) -} - -const rmdir = async (path, options, originalErr, isTop) => { - if (!options.recursive && isTop) { - throw originalErr || new ERR_FS_EISDIR(path) - } - const force = isTop ? options.force : true - - return fs.rmdir(path) - .catch(async (err) => { - // in Windows, calling rmdir on a file path will fail with ENOENT rather - // than ENOTDIR. to determine if that's what happened, we have to do - // another lstat on the path. if the path isn't actually gone, we throw - // away the ENOENT and replace it with our own ENOTDIR - if (isWindows && err.code === 'ENOENT') { - const stillExists = await fs.lstat(path).then(() => true, () => false) - if (stillExists) { - err = new ENOTDIR(path) - } - } - - // not there, not a problem - if (err.code === 'ENOENT' && force) { - return - } - - // we may not have originalErr if lstat tells us our target is a - // directory but that changes before we actually remove it, so - // only throw it here if it's set - if (originalErr && err.code === 'ENOTDIR') { - throw originalErr - } - - // the directory isn't empty, remove the contents and try again - if (notEmptyCodes.has(err.code)) { - const files = await fs.readdir(path) - await Promise.all(files.map((file) => { - const target = join(path, file) - return rimraf(target, options) - })) - return fs.rmdir(path) - } - - throw err - }) -} - -const rm = async (path, opts) => { - const options = { ...defaultOptions, ...opts } - let retries = 0 - - const errHandler = async (err) => { - if (retryCodes.has(err.code) && ++retries < options.maxRetries) { - const delay = retries * options.retryDelay - await promiseTimeout(delay) - return rimraf(path, options, true).catch(errHandler) - } - - throw err - } - - return rimraf(path, options, true).catch(errHandler) -} - -const promiseTimeout = (ms) => new Promise((r) => setTimeout(r, ms)) - -module.exports = rm diff --git a/lib/with-owner-sync.js b/lib/with-owner-sync.js deleted file mode 100644 index 3597d1c..0000000 --- a/lib/with-owner-sync.js +++ /dev/null @@ -1,21 +0,0 @@ -const getOptions = require('./common/get-options.js') -const owner = require('./common/owner-sync.js') - -const withOwnerSync = (path, fn, opts) => { - const options = getOptions(opts, { - copy: ['owner'], - }) - - const { uid, gid } = owner.validate(path, options.owner) - - const result = fn({ uid, gid }) - - owner.update(path, uid, gid) - if (typeof result === 'string') { - owner.update(result, uid, gid) - } - - return result -} - -module.exports = withOwnerSync diff --git a/lib/with-owner.js b/lib/with-owner.js deleted file mode 100644 index a679102..0000000 --- a/lib/with-owner.js +++ /dev/null @@ -1,21 +0,0 @@ -const getOptions = require('./common/get-options.js') -const owner = require('./common/owner.js') - -const withOwner = async (path, fn, opts) => { - const options = getOptions(opts, { - copy: ['owner'], - }) - - const { uid, gid } = await owner.validate(path, options.owner) - - const result = await fn({ uid, gid }) - - await Promise.all([ - owner.update(path, uid, gid), - typeof result === 'string' ? owner.update(result, uid, gid) : null, - ]) - - return result -} - -module.exports = withOwner diff --git a/lib/with-temp-dir.js b/lib/with-temp-dir.js index 81db59d..0d7e826 100644 --- a/lib/with-temp-dir.js +++ b/lib/with-temp-dir.js @@ -1,9 +1,7 @@ const { join, sep } = require('path') const getOptions = require('./common/get-options.js') -const mkdir = require('./mkdir.js') -const mkdtemp = require('./mkdtemp.js') -const rm = require('./rm/index.js') +const { mkdir, mkdtemp, rm } = require('./fs.js') // create a temp directory, ensure its permissions match its parent, then call // the supplied function passing it the path to the directory. clean up after @@ -12,10 +10,10 @@ const withTempDir = async (root, fn, opts) => { const options = getOptions(opts, { copy: ['tmpPrefix'], }) - // create the directory, and fix its ownership - await mkdir(root, { recursive: true, owner: 'inherit' }) + // create the directory + await mkdir(root, { recursive: true }) - const target = await mkdtemp(join(`${root}${sep}`, options.tmpPrefix || ''), { owner: 'inherit' }) + const target = await mkdtemp(join(`${root}${sep}`, options.tmpPrefix || '')) let err let result diff --git a/lib/write-file.js b/lib/write-file.js deleted file mode 100644 index ff90057..0000000 --- a/lib/write-file.js +++ /dev/null @@ -1,14 +0,0 @@ -const fs = require('./fs.js') -const getOptions = require('./common/get-options.js') -const withOwner = require('./with-owner.js') - -const writeFile = async (file, data, opts) => { - const options = getOptions(opts, { - copy: ['encoding', 'mode', 'flag', 'signal'], - wrap: 'encoding', - }) - - return withOwner(file, () => fs.writeFile(file, data, options), opts) -} - -module.exports = writeFile diff --git a/test/common/owner-sync.js b/test/common/owner-sync.js deleted file mode 100644 index e326f5f..0000000 --- a/test/common/owner-sync.js +++ /dev/null @@ -1,232 +0,0 @@ -const { join } = require('path') -const { URL } = require('url') -const t = require('tap') - -const realFs = require('fs') -const fs = require('../../') -// use t.mock so fs sync methods can be overriden per test -const owner = () => t.mock('../../lib/common/owner-sync.js') - -t.test('find', async (t) => { - // if there's no process.getuid, none of the logic will ever run - // it never gets called, so it doesn't need to do anything, we just need it - // to exist so we don't exit early - if (!process.getuid) { - process.getuid = () => {} - } - - t.test('can find directory ownership', async (t) => { - const dir = t.testdir() - const stat = fs.lstatSync(dir) - - const result = owner().find(dir) - t.equal(result.uid, stat.uid, 'found the correct uid') - t.equal(result.gid, stat.gid, 'found the correct gid') - }) - - t.test('supports file: protocol URL objects as path', async (t) => { - const dir = t.testdir() - const stat = fs.lstatSync(dir) - - const result = owner().find(new URL(`file:${dir}`)) - t.equal(result.uid, stat.uid, 'found the correct uid') - t.equal(result.gid, stat.gid, 'found the correct gid') - }) - - t.test('checks parent directory if lstat rejects', async (t) => { - const dir = t.testdir() - const stat = fs.lstatSync(dir) - - const result = owner().find(join(dir, 'not-here')) - t.equal(result.uid, stat.uid, 'found the correct uid') - t.equal(result.gid, stat.gid, 'found the correct gid') - }) - - t.test('returns an empty object if lstat rejects for all paths', async (t) => { - const lstatSync = realFs.lstatSync - t.teardown(() => { - realFs.lstatSync = lstatSync - }) - - realFs.lstatSync = () => { - throw new Error('no') - } - - const result = owner().find(join('some', 'random', 'path')) - t.same(result, {}, 'returns an empty object') - }) - - t.test('returns an empty object if process.getuid is missing', async (t) => { - const getuid = process.getuid - process.getuid = undefined - t.teardown(() => process.getuid = getuid) - - const dir = t.testdir() - const result = owner().find(dir) - t.same(result, {}, 'returns an empty object') - }) -}) - -t.test('update', async (t) => { - t.test('updates ownership', async (t) => { - const dir = t.testdir({ - 'test.txt': 'some content', - }) - - // we hijack stat so we can be certain uid/gid are values other than - // what we're passing. we also hijack chown so we can be certain it gets - // called since we won't have a real chown on all platforms - const stat = realFs.statSync - const chown = realFs.chownSync - t.teardown(() => { - realFs.statSync = stat - realFs.chownSync = chown - }) - - realFs.statSync = () => ({ uid: 2, gid: 2 }) - realFs.chownSync = (path, uid, gid) => { - t.equal(path, join(dir, 'test.txt'), 'got the right path') - t.equal(uid, 1, 'chown() got right uid') - t.equal(gid, 1, 'chown() got right gid') - } - - t.doesNotThrow(() => owner().update(join(dir, 'test.txt'), 1, 1)) - }) - - t.test('does nothing if uid and gid are undefined', async (t) => { - const stat = realFs.statSync - const chown = realFs.chownSync - t.teardown(() => { - realFs.statSync = stat - realFs.chownSync = chown - }) - - realFs.statSync = () => t.fail('should not have called stat()') - realFs.chownSync = () => t.fail('should not have called chown()') - - t.doesNotThrow(() => owner().update(join('some', 'dir'), undefined, undefined)) - }) - - t.test('does not chown if uid and gid match current values', async (t) => { - const uid = 1 - const gid = 1 - - const stat = realFs.statSync - const chown = realFs.chownSync - t.teardown(() => { - realFs.statSync = stat - realFs.chownSync = chown - }) - - realFs.statSync = () => ({ uid, gid }) - realFs.chownSync = () => t.fail('should not have called chown()') - - t.doesNotThrow(() => owner().update(join('some', 'dir'), uid, gid)) - }) - - t.test('chowns if only uid differs from current values', async (t) => { - const dir = t.testdir({ - 'test.txt': 'some content', - }) - - const stat = realFs.statSync - const chown = realFs.chownSync - t.teardown(() => { - realFs.statSync = stat - realFs.chownSync = chown - }) - - realFs.statSync = () => ({ uid: 2, gid: 1 }) - realFs.chownSync = (path, uid, gid) => { - t.equal(path, join(dir, 'test.txt'), 'got the right path') - t.equal(uid, 1, 'chown() got right uid') - t.equal(gid, 1, 'chown() got right gid') - } - - t.doesNotThrow(() => owner().update(join(dir, 'test.txt'), 1, 1)) - }) - - t.test('chowns if only gid differs from current values', async (t) => { - const dir = t.testdir({ - 'test.txt': 'some content', - }) - - const stat = realFs.statSync - const chown = realFs.chownSync - t.teardown(() => { - realFs.statSync = stat - realFs.chownSync = chown - }) - - // stat returns uid 1, gid 2 - realFs.statSync = () => ({ uid: 1, gid: 2 }) - realFs.chownSync = (path, uid, gid) => { - t.equal(path, join(dir, 'test.txt'), 'got the right path') - t.equal(uid, 1, 'chown() got right uid') - t.equal(gid, 1, 'chown() got right gid') - } - - // owner().update tries to set uid 1, gid 1 - t.doesNotThrow(() => owner().update(join(dir, 'test.txt'), 1, 1)) - }) -}) - -t.test('validate', async (t) => { - t.test('keeps object values', async (t) => { - const opts = { - uid: 1, - gid: 1, - } - - const result = owner().validate(join('some', 'dir'), opts) - t.equal(result.uid, opts.uid, 'kept the uid') - t.equal(result.gid, opts.gid, 'kept the gid') - }) - - t.test('copies a number to both values', async (t) => { - const result = owner().validate(join('some', 'dir'), 1) - t.equal(result.uid, 1, 'set the uid') - t.equal(result.gid, 1, 'set the gid') - }) - - t.test('copies a string to both values', async (t) => { - const result = owner().validate(join('some', 'dir'), 'something') - t.equal(result.uid, 'something', 'set the uid') - t.equal(result.gid, 'something', 'set the gid') - }) - - t.test('can inherit values from the path', async (t) => { - const dir = t.testdir() - const stat = fs.lstatSync(dir) - - const result = owner().validate(dir, 'inherit') - t.equal(result.uid, stat.uid, 'found the right uid') - t.equal(result.gid, stat.gid, 'found the right gid') - }) - - t.test('can inherit just the uid', async (t) => { - const dir = t.testdir() - const stat = fs.lstatSync(dir) - - const opts = { - uid: 'inherit', - gid: 1, - } - const result = owner().validate(dir, opts) - t.equal(result.uid, stat.uid, 'found the right uid') - t.equal(result.gid, 1, 'kept the gid') - }) - - t.test('can inherit just the gid', async (t) => { - const dir = t.testdir() - const stat = fs.lstatSync(dir) - - const opts = { - uid: 1, - gid: 'inherit', - } - const result = owner().validate(dir, opts) - t.equal(result.uid, 1, 'kept the uid') - t.equal(result.gid, stat.gid, 'found the right gid') - }) -}) diff --git a/test/common/owner.js b/test/common/owner.js deleted file mode 100644 index 107ac64..0000000 --- a/test/common/owner.js +++ /dev/null @@ -1,228 +0,0 @@ -const { join } = require('path') -const { URL } = require('url') -const t = require('tap') - -const realFs = require('fs') -const fs = require('../../') -const owner = require('../../lib/common/owner.js') - -t.test('find', async (t) => { - // if there's no process.getuid, none of the logic will ever run - // it never gets called, so it doesn't need to do anything, we just need it - // to exist so we don't exit early - if (!process.getuid) { - process.getuid = () => {} - } - - t.test('can find directory ownership', async (t) => { - const dir = t.testdir() - const stat = await fs.lstat(dir) - - const result = await owner.find(dir) - t.equal(result.uid, stat.uid, 'found the correct uid') - t.equal(result.gid, stat.gid, 'found the correct gid') - }) - - t.test('supports file: protocol URL objects as path', async (t) => { - const dir = t.testdir() - const stat = await fs.lstat(dir) - - const result = await owner.find(new URL(`file:${dir}`)) - t.equal(result.uid, stat.uid, 'found the correct uid') - t.equal(result.gid, stat.gid, 'found the correct gid') - }) - - t.test('checks parent directory if lstat rejects', async (t) => { - const dir = t.testdir() - const stat = await fs.lstat(dir) - - const result = await owner.find(join(dir, 'not-here')) - t.equal(result.uid, stat.uid, 'found the correct uid') - t.equal(result.gid, stat.gid, 'found the correct gid') - }) - - t.test('returns an empty object if lstat rejects for all paths', async (t) => { - const lstat = realFs.lstat - realFs.lstat = (_, cb) => setImmediate(cb, new Error('no')) - t.teardown(() => { - realFs.lstat = lstat - }) - - const result = await owner.find(join('some', 'random', 'path')) - t.same(result, {}, 'returns an empty object') - }) - - t.test('returns an empty object if process.getuid is missing', async (t) => { - const getuid = process.getuid - process.getuid = undefined - t.teardown(() => { - process.getuid = getuid - }) - - const dir = t.testdir() - const result = await owner.find(dir) - t.same(result, {}, 'returns an empty object') - }) -}) - -t.test('update', async (t) => { - t.test('updates ownership', async (t) => { - // we hijack stat so we can be certain uid/gid are values other than - // what we're passing. we also hijack chown so we can be certain it gets - // called since we won't have a real chown on all platforms - const stat = realFs.stat - const chown = realFs.chown - realFs.stat = (_, cb) => setImmediate(cb, null, { uid: 2, gid: 2 }) - realFs.chown = (path, uid, gid, cb) => { - t.equal(path, join(dir, 'test.txt'), 'got the right path') - t.equal(uid, 1, 'chown() got right uid') - t.equal(gid, 1, 'chown() got right gid') - setImmediate(cb) - } - t.teardown(() => { - realFs.stat = stat - realFs.chown = chown - }) - - const dir = t.testdir({ - 'test.txt': 'some content', - }) - - await t.resolves(owner.update(join(dir, 'test.txt'), 1, 1)) - }) - - t.test('does nothing if uid and gid are undefined', async (t) => { - const stat = realFs.stat - const chown = realFs.chown - realFs.stat = () => t.fail('should not have called stat()') - realFs.chown = () => t.fail('should not have called chown()') - t.teardown(() => { - realFs.stat = stat - realFs.chown = chown - }) - - await t.resolves(owner.update(join('some', 'dir'), undefined, undefined)) - }) - - t.test('does not chown if uid and gid match current values', async (t) => { - const uid = 1 - const gid = 1 - - const stat = realFs.stat - const chown = realFs.chown - realFs.stat = (_, cb) => setImmediate(cb, null, { uid, gid }) - realFs.chown = () => t.fail('should not have called chown()') - t.teardown(() => { - realFs.stat = stat - realFs.chown = chown - }) - - await t.resolves(owner.update(join('some', 'dir'), uid, gid)) - }) - - t.test('chowns if only uid differs from current values', async (t) => { - const stat = realFs.stat - const chown = realFs.chown - realFs.stat = (_, cb) => setImmediate(cb, null, { uid: 2, gid: 1 }) - realFs.chown = (path, uid, gid, cb) => { - t.equal(path, join(dir, 'test.txt'), 'got the right path') - t.equal(uid, 1, 'chown() got right uid') - t.equal(gid, 1, 'chown() got right gid') - setImmediate(cb) - } - t.teardown(() => { - realFs.stat = stat - realFs.chown = chown - }) - - const dir = t.testdir({ - 'test.txt': 'some content', - }) - - await t.resolves(owner.update(join(dir, 'test.txt'), 1, 1)) - }) - - t.test('chowns if only gid differs from current values', async (t) => { - const stat = realFs.stat - const chown = realFs.chown - // stat returns uid 1, gid 2 - realFs.stat = (_, cb) => setImmediate(cb, null, { uid: 1, gid: 2 }) - realFs.chown = (path, uid, gid, cb) => { - t.equal(path, join(dir, 'test.txt'), 'got the right path') - t.equal(uid, 1, 'chown() got right uid') - t.equal(gid, 1, 'chown() got right gid') - setImmediate(cb) - } - t.teardown(() => { - realFs.stat = stat - realFs.chown = chown - }) - - const dir = t.testdir({ - 'test.txt': 'some content', - }) - - // owner.update tries to set uid 1, gid 1 - await t.resolves(owner.update(join(dir, 'test.txt'), 1, 1)) - }) -}) - -t.test('validate', async (t) => { - t.test('keeps object values', async (t) => { - const opts = { - uid: 1, - gid: 1, - } - - const result = await owner.validate(join('some', 'dir'), opts) - t.equal(result.uid, opts.uid, 'kept the uid') - t.equal(result.gid, opts.gid, 'kept the gid') - }) - - t.test('copies a number to both values', async (t) => { - const result = await owner.validate(join('some', 'dir'), 1) - t.equal(result.uid, 1, 'set the uid') - t.equal(result.gid, 1, 'set the gid') - }) - - t.test('copies a string to both values', async (t) => { - const result = await owner.validate(join('some', 'dir'), 'something') - t.equal(result.uid, 'something', 'set the uid') - t.equal(result.gid, 'something', 'set the gid') - }) - - t.test('can inherit values from the path', async (t) => { - const dir = t.testdir() - const stat = await fs.lstat(dir) - - const result = await owner.validate(dir, 'inherit') - t.equal(result.uid, stat.uid, 'found the right uid') - t.equal(result.gid, stat.gid, 'found the right gid') - }) - - t.test('can inherit just the uid', async (t) => { - const dir = t.testdir() - const stat = await fs.lstat(dir) - - const opts = { - uid: 'inherit', - gid: 1, - } - const result = await owner.validate(dir, opts) - t.equal(result.uid, stat.uid, 'found the right uid') - t.equal(result.gid, 1, 'kept the gid') - }) - - t.test('can inherit just the gid', async (t) => { - const dir = t.testdir() - const stat = await fs.lstat(dir) - - const opts = { - uid: 1, - gid: 'inherit', - } - const result = await owner.validate(dir, opts) - t.equal(result.uid, 1, 'kept the uid') - t.equal(result.gid, stat.gid, 'found the right gid') - }) -}) diff --git a/test/copy-file.js b/test/copy-file.js deleted file mode 100644 index f089491..0000000 --- a/test/copy-file.js +++ /dev/null @@ -1,53 +0,0 @@ -const { join } = require('path') -const t = require('tap') - -const fs = require('../') -const { COPYFILE_EXCL } = fs.constants - -t.test('can copy a file', async (t) => { - const dir = t.testdir({ - 'source.txt': 'the original content', - }) - const src = join(dir, 'source.txt') - const dest = join(dir, 'dest.txt') - - await fs.copyFile(src, dest) - - t.ok(await fs.exists(dest), 'dest.txt exists') - t.same(await fs.readFile(src), await fs.readFile(dest), 'file contents match') -}) - -t.test('mode', async (t) => { - t.test('can be passed in an object', async (t) => { - // start with a destination already in place - const dir = t.testdir({ - 'source.txt': 'the original content', - 'dest.txt': 'not the original content', - }) - const src = join(dir, 'source.txt') - const dest = join(dir, 'dest.txt') - - // do a plain copy first, which shows us that the default mode of always - // overwriting works - await fs.copyFile(src, dest) - - // now do it again with COPYFILE_EXCL as the mode, this one should fail - await t.rejects(fs.copyFile(src, dest, { mode: COPYFILE_EXCL }), { - code: 'EEXIST', - }) - }) - - t.test('can be passed as a number', async (t) => { - const dir = t.testdir({ - 'source.txt': 'the original content', - 'dest.txt': 'not the original content', - }) - const src = join(dir, 'source.txt') - const dest = join(dir, 'dest.txt') - - await fs.copyFile(src, dest) - await t.rejects(fs.copyFile(src, dest, COPYFILE_EXCL), { - code: 'EEXIST', - }) - }) -}) diff --git a/test/errors.js b/test/cp/errors.js similarity index 97% rename from test/errors.js rename to test/cp/errors.js index 12997ac..f5694a0 100644 --- a/test/errors.js +++ b/test/cp/errors.js @@ -1,5 +1,5 @@ const t = require('tap') -const { ERR_FS_EISDIR } = require('../lib/errors') +const { ERR_FS_EISDIR } = require('../../lib/cp/errors') const { constants: { errno: { EISDIR, EIO } } } = require('os') const { inspect } = require('util') diff --git a/test/mkdir.js b/test/mkdir.js deleted file mode 100644 index 9e78de5..0000000 --- a/test/mkdir.js +++ /dev/null @@ -1,12 +0,0 @@ -const { join } = require('path') -const t = require('tap') - -const fs = require('../lib') - -t.test('can create a directory', async (t) => { - const root = t.testdir() - const dir = join(root, 'test') - - await fs.mkdir(dir) - t.ok(await fs.exists(dir), 'directory was created') -}) diff --git a/test/mkdtemp.js b/test/mkdtemp.js deleted file mode 100644 index ceacec6..0000000 --- a/test/mkdtemp.js +++ /dev/null @@ -1,29 +0,0 @@ -const t = require('tap') - -const { basename, relative, sep } = require('path') -const fs = require('../') - -t.test('can make a temp directory', async (t) => { - const root = t.testdir() - - // appending the sep tells mkdtemp to create a directory within it - const temp = await fs.mkdtemp(`${root}${sep}`) - - t.type(temp, 'string', 'returned a string') - t.equal(relative(root, temp), basename(temp), 'temp dir is inside prefix') -}) - -t.test('can make a temp directory next to a prefix', async (t) => { - const root = t.testdir({ - temp: {}, - }) - const neighbor = `${root}${sep}temp` - - // not ending with sep tells mkdtemp to create a directory at the same level - // as the prefix - const temp = await fs.mkdtemp(neighbor) - - t.type(temp, 'string', 'returned a string') - t.equal(relative(root, temp), basename(temp), 'temp dir is inside root') - t.equal(relative(neighbor, temp), `..${sep}${basename(temp)}`, 'temp dir is a neighbor of prefix') -}) diff --git a/test/rm/index.js b/test/rm/index.js deleted file mode 100644 index 98d19ab..0000000 --- a/test/rm/index.js +++ /dev/null @@ -1,26 +0,0 @@ -const { join } = require('path') -const t = require('tap') - -const fs = require('../../') - -t.test('can remove a file', async (t) => { - const dir = t.testdir({ - file: 'some random file', - }) - const target = join(dir, 'file') - - await fs.rm(target) - - t.equal(await fs.exists(target), false, 'target no longer exists') -}) - -t.test('can remove a directory', async (t) => { - const dir = t.testdir({ - directory: {}, - }) - const target = join(dir, 'directory') - - await fs.rm(target, { recursive: true }) - - t.equal(await fs.exists(target), false, 'target no longer exists') -}) diff --git a/test/rm/polyfill.js b/test/rm/polyfill.js deleted file mode 100644 index 633bb76..0000000 --- a/test/rm/polyfill.js +++ /dev/null @@ -1,588 +0,0 @@ -const { join } = require('path') -const realFs = require('fs') -const t = require('tap') - -const fs = require('../../') -const rm = require('../../lib/rm/polyfill.js') - -// all error handling conditions use the code as the load bearing property, so -// for tests that's all we need to set -class ErrorCode extends Error { - constructor (code) { - super() - this.code = code - } -} - -// so we don't have to type the same codes multiple times -const EISDIR = new ErrorCode('EISDIR') -const EMFILE = new ErrorCode('EMFILE') -const ENOENT = new ErrorCode('ENOENT') -const ENOTDIR = new ErrorCode('ENOTDIR') -const EPERM = new ErrorCode('EPERM') -const EUNKNOWN = new ErrorCode('EUNKNOWN') // fake error code for else coverage - -t.test('can delete a file', async (t) => { - const dir = t.testdir({ - file: 'some random file', - }) - const target = join(dir, 'file') - - await rm(target) - - t.equal(await fs.exists(target), false, 'target no longer exists') -}) - -// real ENOENT, this is the initial lstat error handler being tested -t.test('rejects with ENOENT when target does not exist and force is unset', async (t) => { - const dir = t.testdir() - const target = join(dir, 'file') - - await t.rejects(rm(target), { code: 'ENOENT' }) -}) - -// race condition - lstat succeeds, but unlink rejects with ENOENT -t.test('resolves when unlink gets ENOENT with force', async (t) => { - const dir = t.testdir({ - file: 'some file content', - }) - const target = join(dir, 'file') - const unlink = realFs.unlink - let unlinked = false - realFs.unlink = (path, cb) => { - unlinked = true - setImmediate(cb, ENOENT) - } - t.teardown(() => { - realFs.unlink = unlink - }) - - await rm(target, { force: true }) - t.ok(unlinked, 'called unlink') -}) - -// race condition - lstat succeeds, but unlink rejects with ENOENT -t.test('rejects with ENOENT when unlink gets ENOENT without force', async (t) => { - const dir = t.testdir({ - file: 'some file content', - }) - const target = join(dir, 'file') - const unlink = realFs.unlink - realFs.unlink = (path, cb) => setImmediate(cb, ENOENT) - t.teardown(() => { - realFs.unlink = unlink - }) - - await t.rejects(rm(target), { code: 'ENOENT' }) -}) - -t.test('can delete a directory', async (t) => { - const dir = t.testdir({ - directory: {}, - }) - const target = join(dir, 'directory') - - await rm(target, { recursive: true }) - - t.equal(await fs.exists(target), false, 'target no longer exists') -}) - -t.test('resolves when rmdir gets ENOENT with force', async (t) => { - const dir = t.testdir() - // doesn't actually exist - const target = join(dir, 'directory') - - const lstat = realFs.lstat - realFs.lstat = (path, cb) => { - realFs.lstat = lstat - setImmediate(cb, null, { isDirectory: () => true }) - } - t.teardown(() => { - realFs.lstat = lstat - }) - - await t.resolves(rm(target, { recursive: true, force: true })) -}) - -t.test('rejects with EISDIR when deleting a directory without recursive', async (t) => { - const dir = t.testdir({ - directory: {}, - }) - const target = join(dir, 'directory') - - await t.rejects(rm(target), { code: 'EISDIR' }) -}) - -t.test('can delete a directory with children', async (t) => { - const dir = t.testdir({ - directory: { // a directory with files - inner: 'a file', - nested: {}, // an empty directory inside a directory - }, - empty: {}, // an empty directory - outer: 'another file', // a file by itself - }) - - await rm(dir, { recursive: true }) - t.equal(await fs.exists(dir), false, 'dir no longer exists') -}) - -// race condition - lstat sees a file, but unlink rejects with EISDIR -t.test('rejects with EISDIR when a file becomes a directory', async (t) => { - const dir = t.testdir({ - file: 'some file contents', - }) - const target = join(dir, 'file') - - const unlink = realFs.unlink - realFs.unlink = (path, cb) => setImmediate(cb, EISDIR) - t.teardown(() => { - realFs.unlink = unlink - }) - - await t.rejects(rm(target, { recursive: true }), { - code: 'EISDIR', - }) -}) - -// lstat sees a file, unlink rejects with EPERM, rimraf then calls rmdir -// this test ensures the original EPERM is what we reject with rather than the -// subsequent ENOTDIR -t.test('rejects with EPERM when unlink gets an EPERM and target is a file', async (t) => { - const dir = t.testdir({ - file: 'some file contents', - }) - const target = join(dir, 'file') - - const unlink = realFs.unlink - realFs.unlink = (path, cb) => setImmediate(cb, EPERM) - t.teardown(() => { - realFs.unlink = unlink - }) - - await t.rejects(rm(target), { - code: 'EPERM', - }) -}) - -t.test('retries EMFILE up to maxRetries', async (t) => { - const dir = t.testdir({ - file: 'some random file', - }) - const target = join(dir, 'file') - - let attempts = 0 - const unlink = realFs.unlink - realFs.unlink = (path, cb) => { - ++attempts - setImmediate(cb, EMFILE) - } - t.teardown(() => { - realFs.unlink = unlink - }) - - await t.rejects(rm(target, { maxRetries: 3 }), { - code: 'EMFILE', - }) - t.equal(attempts, 3, 'tried maxRetries times, then rejects') -}) - -t.test('ENOENT with force: true resolves', async (t) => { - const dir = t.testdir({ - file: 'some random file', - }) - const target = join(dir, 'file') - - const lstat = realFs.lstat - realFs.lstat = (path, cb) => setImmediate(cb, ENOENT) - t.teardown(() => { - realFs.lstat = lstat - }) - - const result = await rm(target, { force: true }) - t.equal(result, undefined, 'resolved with undefined') -}) - -t.test('rejects with unknown error removing top directory', async (t) => { - const dir = t.testdir() - const rmdir = realFs.rmdir - realFs.rmdir = (path, cb) => setImmediate(cb, EUNKNOWN) - t.teardown(() => { - realFs.rmdir = rmdir - }) - - await t.rejects(rm(dir, { recursive: true }), { - code: 'EUNKNOWN', - }) -}) - -t.test('posix', async (t) => { - let posixRm - t.before(() => { - t.context.platform = Object.getOwnPropertyDescriptor(process, 'platform') - Object.defineProperty(process, 'platform', { - ...t.context.platform, - value: 'linux', - }) - posixRm = t.mock('../../lib/rm/polyfill.js') - }) - - t.teardown(() => { - Object.defineProperty(process, 'platform', t.context.platform) - }) - - t.test('EPERM in unlink calls rmdir', async (t) => { - const dir = t.testdir({ - file: 'a file', - }) - const target = join(dir, 'file') - - const rmdir = realFs.rmdir - const unlink = realFs.unlink - // need to mock rmdir too so we can force an ENOTDIR - realFs.rmdir = (path, cb) => setImmediate(cb, ENOTDIR) - realFs.unlink = (path, cb) => setImmediate(cb, EPERM) - t.teardown(() => { - realFs.rmdir = rmdir - realFs.unlink = unlink - }) - - await t.rejects(posixRm(target, { recursive: true }), { - code: 'EPERM', - }, 'got the EPERM') - }) -}) - -t.test('windows', async (t) => { - // t.mock instead of require so we flush the cache first - let winRm - t.before(() => { - t.context.platform = Object.getOwnPropertyDescriptor(process, 'platform') - Object.defineProperty(process, 'platform', { - ...t.context.platform, - value: 'win32', - }) - winRm = t.mock('../../lib/rm/polyfill.js') - }) - - t.teardown(() => { - Object.defineProperty(process, 'platform', t.context.platform) - }) - - t.test('EPERM from lstat tries to chmod and remove a file', async (t) => { - const dir = t.testdir({ - file: 'some file content', - }) - const target = join(dir, 'file') - - let calledChmod = false - const chmod = realFs.chmod - const lstat = realFs.lstat - // hijack chmod so we can assert that it was called - realFs.chmod = (path, mode, cb) => { - t.equal(path, target, 'chmod() path is target') - t.equal(mode, 0o666, 'chmod() mode is 0o666') - calledChmod = true - chmod(path, mode, cb) - } - // remove mock after the first call, a second call will be made after the - // chmod to determine how to delete the target, we want that one to work - realFs.lstat = (path, cb) => { - realFs.lstat = lstat - setImmediate(cb, EPERM) - } - t.teardown(() => { - realFs.chmod = chmod - realFs.lstat = lstat - }) - - await winRm(target) - t.ok(calledChmod, 'chmod() was called') - t.not(await fs.exists(target), 'target no longer exists') - }) - - t.test('EPERM from lstat tries to chmod and remove a directory', async (t) => { - const dir = t.testdir({ - directory: {}, - }) - const target = join(dir, 'directory') - - let calledChmod = false - const chmod = realFs.chmod - const lstat = realFs.lstat - // hijack chmod so we can assert that it was called - realFs.chmod = (path, mode, cb) => { - t.equal(path, target, 'chmod() path is target') - t.equal(mode, 0o666, 'chmod() mode is 0o666') - calledChmod = true - chmod(path, mode, cb) - } - // remove mock after the first call, a second call will be made after the - // chmod to determine how to delete the target, we want that one to work - realFs.lstat = (path, cb) => { - realFs.lstat = lstat - setImmediate(cb, EPERM) - } - t.teardown(() => { - realFs.chmod = chmod - realFs.lstat = lstat - }) - - await winRm(target, { recursive: true }) - t.ok(calledChmod, 'chmod() was called') - t.not(await fs.exists(target), 'target no longer exists') - }) - - t.test('ENOENT in chmod after EPERM in lstat resolves when force is set', async (t) => { - const dir = t.testdir({ - directory: {}, - }) - const target = join(dir, 'directory') - - let chmodCalled = false - const chmod = realFs.chmod - const lstat = realFs.lstat - realFs.chmod = (path, mode, cb) => { - t.equal(path, target, 'chmod() path is target') - t.equal(mode, 0o666, 'chmod() mode is 0o666') - chmodCalled = true - setImmediate(cb, ENOENT) - } - realFs.lstat = (path, cb) => { - setImmediate(cb, EPERM) - } - t.teardown(() => { - realFs.chmod = chmod - realFs.lstat = lstat - }) - - await winRm(target, { force: true }) - t.ok(chmodCalled, 'chmod() was called') - }) - - t.test('chmod ENOENT after EPERM in lstat rejects with EPERM when force is unset', async (t) => { - const dir = t.testdir({ - directory: {}, - }) - const target = join(dir, 'directory') - - let chmodCalled = false - const chmod = realFs.chmod - const lstat = realFs.lstat - realFs.chmod = (path, mode, cb) => { - t.equal(path, target, 'chmod() path is target') - t.equal(mode, 0o666, 'chmod() mode is 0o666') - chmodCalled = true - setImmediate(cb, ENOENT) - } - realFs.lstat = (path, cb) => { - setImmediate(cb, EPERM) - } - t.teardown(() => { - realFs.chmod = chmod - realFs.lstat = lstat - }) - - await t.rejects(winRm(target), { - code: 'EPERM', - }) - t.ok(chmodCalled, 'chmod() was called') - }) - - t.test('ENOENT in lstat after EPERM and chmod resolves when force is set', async (t) => { - const dir = t.testdir({ - directory: {}, - }) - const target = join(dir, 'directory') - - let calledChmod = false - const chmod = realFs.chmod - const lstat = realFs.lstat - // hijack chmod so we can assert that it was called - realFs.chmod = (path, mode, cb) => { - t.equal(path, target, 'chmod() path is target') - t.equal(mode, 0o666, 'chmod() mode is 0o666') - calledChmod = true - chmod(path, mode, cb) - } - // after the first call, we swap in an ENOENT - realFs.lstat = (_, cb) => { - realFs.lstat = (__, cb2) => setImmediate(cb2, ENOENT) - setImmediate(cb, EPERM) - } - t.teardown(() => { - realFs.chmod = chmod - realFs.lstat = lstat - }) - - await winRm(target, { force: true }) - t.ok(calledChmod, 'chmod() was called') - }) - - t.test('lstat ENOENT after EPERM and chmod rejects with EPERM when force is unset', async (t) => { - const dir = t.testdir({ - directory: {}, - }) - const target = join(dir, 'directory') - - let calledChmod = false - const chmod = realFs.chmod - const lstat = realFs.lstat - // hijack chmod so we can assert that it was called - realFs.chmod = (path, mode, cb) => { - t.equal(path, target, 'chmod() path is target') - t.equal(mode, 0o666, 'chmod() mode is 0o666') - calledChmod = true - chmod(path, mode, cb) - } - // after the first call, we swap in an ENOENT - realFs.lstat = (_, cb) => { - realFs.lstat = (__, cb2) => setImmediate(cb2, ENOENT) - setImmediate(cb, EPERM) - } - t.teardown(() => { - realFs.chmod = chmod - realFs.lstat = lstat - }) - - await t.rejects(winRm(target), { - code: 'EPERM', - }) - t.ok(calledChmod, 'chmod() was called') - }) - - t.test('EPERM in unlink after lstat tries chmod & unlink again', async (t) => { - const dir = t.testdir({ - file: 'some file content', - }) - const target = join(dir, 'file') - - let chmodCalled = false - const chmod = realFs.chmod - realFs.chmod = (path, mode, cb) => { - chmodCalled = true - setImmediate(cb) - } - const unlink = realFs.unlink - // first call is an EPERM error, also restores now so second unlink works - realFs.unlink = (path, cb) => { - realFs.unlink = unlink - setImmediate(cb, EPERM) - } - t.teardown(() => { - realFs.chmod = chmod - realFs.unlink = unlink - }) - - await winRm(target) - t.ok(chmodCalled, 'chmod() was called') - t.not(await fs.exists(target), 'target no longer exists') - }) - - t.test('EPERM in lstat of a file within a directory calls chmod and unlink again', async (t) => { - const dir = t.testdir({ - directory: { - file: 'some file content', - }, - }) - const target = join(dir, 'directory') - - let targetStatted = false - const lstat = realFs.lstat - realFs.lstat = (path, cb) => { - // for the file, EPERM then restore the original function - if (path === join(target, 'file')) { - targetStatted = true - realFs.lstat = lstat - return setImmediate(cb, EPERM) - } - // for the directory, defer to the system - return lstat(path, cb) - } - - await winRm(target, { recursive: true }) - t.ok(targetStatted, 'lstat() called with child path') - t.not(await fs.exists(join(target, 'file')), 'file no longer exists') - t.not(await fs.exists(target), 'target no longer exists') - }) - - t.test('ENOENT in rmdir resolves when file is really gone with force', async (t) => { - const dir = t.testdir({ - directory: {}, - }) - const target = join(dir, 'directory') - - let rmdirCalled = false - const lstat = realFs.lstat - const rmdir = realFs.rmdir - realFs.rmdir = (_, cb) => { - rmdirCalled = true - realFs.lstat = (__, cb2) => setImmediate(cb2, ENOENT) - setImmediate(cb, ENOENT) - } - t.teardown(() => { - realFs.lstat = lstat - realFs.rmdir = rmdir - }) - - await winRm(target, { recursive: true, force: true }) - t.ok(rmdirCalled, 'rmdir() was called') - }) - - t.test('rmdir ENOENT rejects with ENOENT when file is really gone without force', async (t) => { - const dir = t.testdir({ - directory: {}, - }) - const target = join(dir, 'directory') - - const lstat = realFs.lstat - const rmdir = realFs.rmdir - realFs.rmdir = (_, cb) => { - // need to hijack this here so only the lstat after rmdir gets the ENOENT - realFs.lstat = (__, cb2) => setImmediate(cb2, ENOENT) - setImmediate(cb, ENOENT) - } - t.teardown(() => { - realFs.lstat = lstat - realFs.rmdir = rmdir - }) - - await t.rejects(winRm(target, { recursive: true }), { - code: 'ENOENT', - }, 'got the ENOENT') - }) - - t.test('ENOENT in rmdir rejects with ENOTDIR when target still exists', async (t) => { - const dir = t.testdir({ - directory: {}, - }) - const target = join(dir, 'directory') - - const rmdir = realFs.rmdir - realFs.rmdir = (_, cb) => setImmediate(cb, ENOENT) - t.teardown(() => { - realFs.rmdir = rmdir - }) - - await t.rejects(winRm(target, { recursive: true }), { - code: 'ENOTDIR', - }, 'got the ENOTDIR') - }) - - t.test('rmdir ENOENT rejects with ENOTDIR if target still exists and force is set', async (t) => { - const dir = t.testdir({ - directory: {}, - }) - const target = join(dir, 'directory') - - const rmdir = realFs.rmdir - realFs.rmdir = (_, cb) => setImmediate(cb, ENOENT) - t.teardown(() => { - realFs.rmdir = rmdir - }) - - await t.rejects(winRm(target, { recursive: true, force: true }), { - code: 'ENOTDIR', - }, 'got the ENOTDIR') - }) -}) diff --git a/test/with-owner.js b/test/with-owner.js deleted file mode 100644 index a319e1d..0000000 --- a/test/with-owner.js +++ /dev/null @@ -1,117 +0,0 @@ -const { join } = require('path') -const t = require('tap') - -const realFs = require('fs') -const fs = require('../') -const withOwner = require('../lib/with-owner.js') -// use t.mock so fs sync methods can be overriden per test -const withOwnerSync = () => t.mock('../lib/with-owner-sync.js') - -t.test('can be used with any method', async (t) => { - const root = t.testdir({ - 'test.txt': 'hello', - }) - - const f = join(root, 'test.txt') - - const stat = realFs.stat - const chown = realFs.chown - t.teardown(() => { - realFs.stat = stat - realFs.chown = chown - }) - - realFs.stat = (path, cb) => setImmediate(cb, null, { uid: 2, gid: 2 }) - realFs.chown = (path, uid, gid, cb) => { - t.equal(path, join(root, 'test.txt'), 'got the right path') - t.equal(uid, 1, 'chown() got right uid') - t.equal(gid, 1, 'chown() got right gid') - setImmediate(cb) - } - - await withOwner( - f, - () => fs.appendFile(f, ' world'), - { owner: 1 } - ) - - t.equal(await fs.readFile(f, 'utf-8'), 'hello world') -}) - -t.test('sync', async (t) => { - const root = t.testdir({ - 'test.txt': 'hello', - }) - - const f = join(root, 'test.txt') - - const stat = realFs.statSync - const chown = realFs.chownSync - t.teardown(() => { - realFs.statSync = stat - realFs.chownSync = chown - }) - - realFs.statSync = (path) => ({ uid: 2, gid: 2 }) - realFs.chownSync = (path, uid, gid) => { - t.equal(path, join(root, 'test.txt'), 'got the right path') - t.equal(uid, 1, 'chown() got right uid') - t.equal(gid, 1, 'chown() got right gid') - } - - withOwnerSync()( - f, - () => fs.appendFileSync(f, ' world'), - { owner: 1 } - ) - - t.equal(fs.readFileSync(f, 'utf-8'), 'hello world') -}) - -t.test('calls on result if it is a string', async (t) => { - const root = t.testdir({ - 'test.txt': 'hello', - }) - - const f = join(root, 'test.txt') - let count = 0 - - const stat = realFs.statSync - const chown = realFs.chownSync - t.teardown(() => { - realFs.statSync = stat - realFs.chownSync = chown - }) - - realFs.statSync = (path) => ({ uid: 2, gid: 2 }) - realFs.chownSync = (path, uid, gid) => { - count++ - t.equal(path, join(root, 'test.txt'), 'got the right path') - t.equal(uid, 1, 'chown() got right uid') - t.equal(gid, 1, 'chown() got right gid') - } - - withOwnerSync()( - f, - // just return string to make sure chown is called twice - () => f, - { owner: 1 } - ) - - t.equal(count, 2) -}) - -t.test('doesnt error without owner property', async (t) => { - const root = t.testdir({ - 'test.txt': 'hello', - }) - - const f = join(root, 'test.txt') - - withOwnerSync()( - f, - () => fs.writeFileSync(f, 'test') - ) - - t.equal(fs.readFileSync(f, 'utf-8'), 'test') -}) diff --git a/test/write-file.js b/test/write-file.js deleted file mode 100644 index 0247aaa..0000000 --- a/test/write-file.js +++ /dev/null @@ -1,39 +0,0 @@ -const { join } = require('path') -const t = require('tap') - -const fs = require('../') - -t.test('can write a file', async (t) => { - const root = t.testdir() - const data = 'hello, world' - const target = join(root, 'temp.txt') - - await fs.writeFile(target, data) - - t.ok(await fs.exists(target), 'target exists') - t.equal(await fs.readFile(target, { encoding: 'utf8' }), data, 'target has the right data') -}) - -t.test('encoding', async (t) => { - t.test('can be passed in an object', async (t) => { - const root = t.testdir() - const data = 'hello, world' - const target = join(root, 'temp.txt') - - await fs.writeFile(target, Buffer.from(data).toString('hex'), { encoding: 'hex' }) - - t.ok(await fs.exists(target), 'target exists') - t.equal(await fs.readFile(target, { encoding: 'utf8' }), data, 'target has the right data') - }) - - t.test('can be passed as a string', async (t) => { - const root = t.testdir() - const data = 'hello, world' - const target = join(root, 'temp.txt') - - await fs.writeFile(target, Buffer.from(data).toString('hex'), 'hex') - - t.ok(await fs.exists(target), 'target exists') - t.equal(await fs.readFile(target, { encoding: 'utf8' }), data, 'target has the right data') - }) -})