diff --git a/docs/copy-sync.md b/docs/copy-sync.md index 76bfd477..3fca1330 100644 --- a/docs/copy-sync.md +++ b/docs/copy-sync.md @@ -1,4 +1,4 @@ -# copySync(src, dest, [options]) +# copySync(src, dest[, options]) Copy a file or directory. The directory can have contents. Like `cp -r`. diff --git a/docs/copy.md b/docs/copy.md index e5f8582b..4add5684 100644 --- a/docs/copy.md +++ b/docs/copy.md @@ -1,4 +1,4 @@ -# copy(src, dest, [options, callback]) +# copy(src, dest[, options][, callback]) Copy a file or directory. The directory can have contents. Like `cp -r`. diff --git a/docs/emptyDir.md b/docs/emptyDir.md index 3d129d42..8f26dc10 100644 --- a/docs/emptyDir.md +++ b/docs/emptyDir.md @@ -1,4 +1,4 @@ -# emptyDir(dir, [callback]) +# emptyDir(dir[, callback]) Ensures that a directory is empty. Deletes directory contents if the directory is not empty. If the directory does not exist, it is created. The directory itself is not deleted. diff --git a/docs/ensureFile.md b/docs/ensureFile.md index aa8e82ac..a560e966 100644 --- a/docs/ensureFile.md +++ b/docs/ensureFile.md @@ -1,4 +1,4 @@ -# ensureFile(file, [callback]) +# ensureFile(file[, callback]) Ensures that the file exists. If the file that is requested to be created is in directories that do not exist, these directories are created. If the file already exists, it is **NOT MODIFIED**. diff --git a/docs/ensureLink.md b/docs/ensureLink.md index 3f39f07b..7bdddebf 100644 --- a/docs/ensureLink.md +++ b/docs/ensureLink.md @@ -1,4 +1,4 @@ -# ensureLink(srcpath, dstpath, [callback]) +# ensureLink(srcpath, dstpath[, callback]) Ensures that the link exists. If the directory structure does not exist, it is created. diff --git a/docs/ensureSymlink-sync.md b/docs/ensureSymlink-sync.md index 328d4c45..72b39c29 100644 --- a/docs/ensureSymlink-sync.md +++ b/docs/ensureSymlink-sync.md @@ -1,4 +1,4 @@ -# ensureSymlinkSync(srcpath, dstpath, [type]) +# ensureSymlinkSync(srcpath, dstpath[, type]) Ensures that the symlink exists. If the directory structure does not exist, it is created. diff --git a/docs/ensureSymlink.md b/docs/ensureSymlink.md index 39c09568..383c49f0 100644 --- a/docs/ensureSymlink.md +++ b/docs/ensureSymlink.md @@ -1,4 +1,4 @@ -# ensureSymlink(srcpath, dstpath, [type, callback]) +# ensureSymlink(srcpath, dstpath[, type][, callback]) Ensures that the symlink exists. If the directory structure does not exist, it is created. diff --git a/docs/move-sync.md b/docs/move-sync.md index cd701fef..d5153b58 100644 --- a/docs/move-sync.md +++ b/docs/move-sync.md @@ -1,4 +1,4 @@ -# moveSync(src, dest, [options]) +# moveSync(src, dest[, options]) Moves a file or directory, even across devices. diff --git a/docs/move.md b/docs/move.md index 343d10be..5f70853e 100644 --- a/docs/move.md +++ b/docs/move.md @@ -1,4 +1,4 @@ -# move(src, dest, [options, callback]) +# move(src, dest[, options][, callback]) Moves a file or directory, even across devices. diff --git a/docs/outputFile-sync.md b/docs/outputFile-sync.md index 38eee8b6..fc7abdf9 100644 --- a/docs/outputFile-sync.md +++ b/docs/outputFile-sync.md @@ -1,4 +1,4 @@ -# outputFileSync(file, data, [options]) +# outputFileSync(file, data[, options]) Almost the same as `writeFileSync` (i.e. it [overwrites](http://pages.citebite.com/v2o5n8l2f5reb)), except that if the parent directory does not exist, it's created. `file` must be a file path (a buffer or a file descriptor is not allowed). `options` are what you'd pass to [`fs.writeFileSync()`](https://nodejs.org/api/fs.html#fs_fs_writefilesync_file_data_options). diff --git a/docs/outputFile.md b/docs/outputFile.md index 34efdc94..ed9c19d6 100644 --- a/docs/outputFile.md +++ b/docs/outputFile.md @@ -1,4 +1,4 @@ -# outputFile(file, data, [options, callback]) +# outputFile(file, data[, options][, callback]) Almost the same as `writeFile` (i.e. it [overwrites](http://pages.citebite.com/v2o5n8l2f5reb)), except that if the parent directory does not exist, it's created. `file` must be a file path (a buffer or a file descriptor is not allowed). `options` are what you'd pass to [`fs.writeFile()`](https://nodejs.org/api/fs.html#fs_fs_writefile_file_data_options_callback). diff --git a/docs/outputJson-sync.md b/docs/outputJson-sync.md index ef78f802..c42497cf 100644 --- a/docs/outputJson-sync.md +++ b/docs/outputJson-sync.md @@ -1,4 +1,4 @@ -# outputJsonSync(file, object, [options]) +# outputJsonSync(file, object[, options]) Almost the same as [`writeJsonSync`](writeJson-sync.md), except that if the directory does not exist, it's created. diff --git a/docs/outputJson.md b/docs/outputJson.md index aa265aee..d5a6d7c7 100644 --- a/docs/outputJson.md +++ b/docs/outputJson.md @@ -1,4 +1,4 @@ -# outputJson(file, object, [options, callback]) +# outputJson(file, object[, options][, callback]) Almost the same as [`writeJson`](writeJson.md), except that if the directory does not exist, it's created. diff --git a/docs/readJson-sync.md b/docs/readJson-sync.md index a1356379..1fc6519d 100644 --- a/docs/readJson-sync.md +++ b/docs/readJson-sync.md @@ -1,4 +1,4 @@ -# readJsonSync(file, [options]) +# readJsonSync(file[, options]) Reads a JSON file and then parses it into an object. `options` are the same that you'd pass to [`jsonFile.readFileSync`](https://github.com/jprichardson/node-jsonfile#readfilesyncfilename-options). diff --git a/docs/readJson.md b/docs/readJson.md index baf80ac8..881e7972 100644 --- a/docs/readJson.md +++ b/docs/readJson.md @@ -1,4 +1,4 @@ -# readJson(file, [options, callback]) +# readJson(file[, options][, callback]) Reads a JSON file and then parses it into an object. `options` are the same that you'd pass to [`jsonFile.readFile`](https://github.com/jprichardson/node-jsonfile#readfilefilename-options-callback). diff --git a/docs/remove.md b/docs/remove.md index bcc49e39..0cdcf7ac 100644 --- a/docs/remove.md +++ b/docs/remove.md @@ -1,4 +1,4 @@ -# remove(path, [callback]) +# remove(path[, callback]) Removes a file or directory. The directory can have contents. Like `rm -rf`. diff --git a/docs/writeJson-sync.md b/docs/writeJson-sync.md index c22459db..b98e0c7d 100644 --- a/docs/writeJson-sync.md +++ b/docs/writeJson-sync.md @@ -1,4 +1,4 @@ -# writeJsonSync(file, object, [options]) +# writeJsonSync(file, object[, options]) Writes an object to a JSON file. diff --git a/docs/writeJson.md b/docs/writeJson.md index 56278029..216d1348 100644 --- a/docs/writeJson.md +++ b/docs/writeJson.md @@ -1,4 +1,4 @@ -# writeJson(file, object, [options, callback]) +# writeJson(file, object[, options][, callback]) Writes an object to a JSON file. diff --git a/lib/copy-sync/__tests__/broken-symlink.test.js b/lib/copy-sync/__tests__/broken-symlink.test.js index 2744c64e..a5e6d57f 100644 --- a/lib/copy-sync/__tests__/broken-symlink.test.js +++ b/lib/copy-sync/__tests__/broken-symlink.test.js @@ -2,7 +2,7 @@ const fs = require('fs') const os = require('os') -const fse = require(process.cwd()) +const fse = require('../../') const path = require('path') const assert = require('assert') const copySync = require('../copy-sync') diff --git a/lib/copy-sync/__tests__/copy-sync-case-insensitive-paths.test.js b/lib/copy-sync/__tests__/copy-sync-case-insensitive-paths.test.js index 216c58b2..fcffa53e 100644 --- a/lib/copy-sync/__tests__/copy-sync-case-insensitive-paths.test.js +++ b/lib/copy-sync/__tests__/copy-sync-case-insensitive-paths.test.js @@ -3,7 +3,8 @@ const assert = require('assert') const os = require('os') const path = require('path') -const fs = require(process.cwd()) +const fs = require('../../') +const platform = os.platform() /* global beforeEach, afterEach, describe, it */ @@ -17,7 +18,7 @@ describe('+ copySync() - case insensitive paths', () => { fs.emptyDir(TEST_DIR, done) }) - afterEach(done => fs.remove(TEST_DIR, done)) + afterEach(() => fs.removeSync(TEST_DIR)) describe('> when src is a directory', () => { it('should behave correctly based on the OS', () => { @@ -29,15 +30,13 @@ describe('+ copySync() - case insensitive paths', () => { try { fs.copySync(src, dest) } catch (err) { - if (os === 'darwin' || os === 'win32') { + if (platform === 'darwin' || platform === 'win32') { assert.strictEqual(err.message, 'Source and destination must not be the same.') - assert(fs.existsSync(src)) - assert(!fs.existsSync(dest)) errThrown = true } } - if (os === 'darwin' || os === 'win32') assert(errThrown) - if (os === 'linux') { + if (platform === 'darwin' || platform === 'win32') assert(errThrown) + if (platform === 'linux') { assert(fs.existsSync(dest)) assert.strictEqual(fs.readFileSync(path.join(dest, 'subdir', 'file.txt'), 'utf8'), 'some data') assert(!errThrown) @@ -55,15 +54,13 @@ describe('+ copySync() - case insensitive paths', () => { try { fs.copySync(src, dest) } catch (err) { - if (os === 'darwin' || os === 'win32') { + if (platform === 'darwin' || platform === 'win32') { assert.strictEqual(err.message, 'Source and destination must not be the same.') - assert(fs.existsSync(src)) - assert(!fs.existsSync(dest)) errThrown = true } } - if (os === 'darwin' || os === 'win32') assert(errThrown) - if (os === 'linux') { + if (platform === 'darwin' || platform === 'win32') assert(errThrown) + if (platform === 'linux') { assert(fs.existsSync(dest)) assert.strictEqual(fs.readFileSync(dest, 'utf8'), 'some data') assert(!errThrown) @@ -77,25 +74,23 @@ describe('+ copySync() - case insensitive paths', () => { fs.outputFileSync(path.join(src, 'subdir', 'file.txt'), 'some data') const srcLink = path.join(TEST_DIR, 'src-symlink') fs.symlinkSync(src, srcLink, 'dir') - dest = path.join(TEST_DIR, 'srcDir') + dest = path.join(TEST_DIR, 'src-Symlink') let errThrown = false try { - fs.copySync(src, dest) + fs.copySync(srcLink, dest) } catch (err) { - if (os === 'darwin' || os === 'win32') { + if (platform === 'darwin' || platform === 'win32') { assert.strictEqual(err.message, 'Source and destination must not be the same.') - assert(fs.existsSync(src)) - assert(!fs.existsSync(dest)) errThrown = true } } - if (os === 'darwin' || os === 'win32') assert(errThrown) - if (os === 'linux') { + if (platform === 'darwin' || platform === 'win32') assert(errThrown) + if (platform === 'linux') { assert(fs.existsSync(dest)) assert.strictEqual(fs.readFileSync(path.join(dest, 'subdir', 'file.txt'), 'utf8'), 'some data') - const link = fs.readlinkSync(srcLink) - assert.strictEqual(link, dest) + const destLink = fs.readlinkSync(dest) + assert.strictEqual(destLink, src) assert(!errThrown) } }) @@ -105,25 +100,23 @@ describe('+ copySync() - case insensitive paths', () => { fs.outputFileSync(src, 'some data') const srcLink = path.join(TEST_DIR, 'src-symlink') fs.symlinkSync(src, srcLink, 'file') - dest = path.join(TEST_DIR, 'srcFile') + dest = path.join(TEST_DIR, 'src-Symlink') let errThrown = false try { - fs.copySync(src, dest) + fs.copySync(srcLink, dest) } catch (err) { - if (os === 'darwin' || os === 'win32') { + if (platform === 'darwin' || platform === 'win32') { assert.strictEqual(err.message, 'Source and destination must not be the same.') - assert(fs.existsSync(src)) - assert(!fs.existsSync(dest)) errThrown = true } } - if (os === 'darwin' || os === 'win32') assert(errThrown) - if (os === 'linux') { + if (platform === 'darwin' || platform === 'win32') assert(errThrown) + if (platform === 'linux') { assert(fs.existsSync(dest)) assert.strictEqual(fs.readFileSync(dest, 'utf8'), 'some data') - const link = fs.readlinkSync(srcLink) - assert.strictEqual(link, dest) + const destLink = fs.readlinkSync(dest) + assert.strictEqual(destLink, src) assert(!errThrown) } }) diff --git a/lib/copy-sync/__tests__/copy-sync-dir.test.js b/lib/copy-sync/__tests__/copy-sync-dir.test.js index 7ce6e981..7a5ecf51 100644 --- a/lib/copy-sync/__tests__/copy-sync-dir.test.js +++ b/lib/copy-sync/__tests__/copy-sync-dir.test.js @@ -1,6 +1,6 @@ 'use strict' -const fs = require(process.cwd()) +const fs = require('../../') const os = require('os') const path = require('path') const assert = require('assert') @@ -75,7 +75,7 @@ describe('+ copySync() / dir', () => { const srcTarget = path.join(TEST_DIR, 'destination') fs.mkdirSync(src) fs.mkdirSync(srcTarget) - fs.symlinkSync(srcTarget, path.join(src, 'symlink')) + fs.symlinkSync(srcTarget, path.join(src, 'symlink'), 'dir') fs.copySync(src, dest) diff --git a/lib/copy-sync/__tests__/copy-sync-file.test.js b/lib/copy-sync/__tests__/copy-sync-file.test.js index 45ef334d..b1122a23 100644 --- a/lib/copy-sync/__tests__/copy-sync-file.test.js +++ b/lib/copy-sync/__tests__/copy-sync-file.test.js @@ -1,6 +1,6 @@ 'use strict' -const fs = require(process.cwd()) +const fs = require('../../') const os = require('os') const path = require('path') const assert = require('assert') diff --git a/lib/copy-sync/__tests__/copy-sync-preserve-time.test.js b/lib/copy-sync/__tests__/copy-sync-preserve-timestamp.test.js similarity index 93% rename from lib/copy-sync/__tests__/copy-sync-preserve-time.test.js rename to lib/copy-sync/__tests__/copy-sync-preserve-timestamp.test.js index c190da70..aa8318d0 100644 --- a/lib/copy-sync/__tests__/copy-sync-preserve-time.test.js +++ b/lib/copy-sync/__tests__/copy-sync-preserve-timestamp.test.js @@ -1,12 +1,13 @@ 'use strict' -const fs = require(process.cwd()) +const fs = require('../../') const os = require('os') const path = require('path') const utimes = require('../../util/utimes') const assert = require('assert') -const nodeVersion = process.versions.node -const nodeVersionMajor = parseInt(nodeVersion.split('.')[0], 10) +const semver = require('semver') +const nodeVersion = process.version +const nodeVersionMajor = semver.major(nodeVersion) /* global beforeEach, afterEach, describe, it */ diff --git a/lib/copy-sync/__tests__/copy-sync-prevent-copying-identical.test.js b/lib/copy-sync/__tests__/copy-sync-prevent-copying-identical.test.js index d9dca5e0..c1f3091d 100644 --- a/lib/copy-sync/__tests__/copy-sync-prevent-copying-identical.test.js +++ b/lib/copy-sync/__tests__/copy-sync-prevent-copying-identical.test.js @@ -3,7 +3,7 @@ const assert = require('assert') const os = require('os') const path = require('path') -const fs = require(process.cwd()) +const fs = require('../../') const klawSync = require('klaw-sync') /* global beforeEach, afterEach, describe, it */ diff --git a/lib/copy-sync/__tests__/copy-sync-prevent-copying-into-itself.test.js b/lib/copy-sync/__tests__/copy-sync-prevent-copying-into-itself.test.js index 487c997a..51f864ac 100644 --- a/lib/copy-sync/__tests__/copy-sync-prevent-copying-into-itself.test.js +++ b/lib/copy-sync/__tests__/copy-sync-prevent-copying-into-itself.test.js @@ -3,7 +3,7 @@ const assert = require('assert') const os = require('os') const path = require('path') -const fs = require(process.cwd()) +const fs = require('../../') const klawSync = require('klaw-sync') /* global beforeEach, afterEach, describe, it */ @@ -166,6 +166,56 @@ describe('+ copySync() - prevent copying into itself', () => { assert.strictEqual(link, src) }) + it('should error when dest is a subdirectory of src (bind-mounted directory with subdirectory)', () => { + const destLink = path.join(TEST_DIR, 'dest-symlink') + fs.symlinkSync(src, destLink, 'dir') + + const srclenBefore = klawSync(src).length + assert(srclenBefore > 2) + + const dest = path.join(destLink, 'dir1') + assert(fs.existsSync(dest)) + let errThrown = false + try { + fs.copySync(src, dest) + } catch (err) { + assert.strictEqual(err.message, `Cannot copy '${src}' to a subdirectory of itself, '${dest}'.`) + errThrown = true + } finally { + assert(errThrown) + const srclenAfter = klawSync(src).length + assert.strictEqual(srclenAfter, srclenBefore, 'src length should not change') + + const link = fs.readlinkSync(destLink) + assert.strictEqual(link, src) + } + }) + + it('should error when dest is a subdirectory of src (more than one level depth)', () => { + const destLink = path.join(TEST_DIR, 'dest-symlink') + fs.symlinkSync(src, destLink, 'dir') + + const srclenBefore = klawSync(src).length + assert(srclenBefore > 2) + + const dest = path.join(destLink, 'dir1', 'dir2') + assert(fs.existsSync(dest)) + let errThrown = false + try { + fs.copySync(src, dest) + } catch (err) { + assert.strictEqual(err.message, `Cannot copy '${src}' to a subdirectory of itself, '${path.join(destLink, 'dir1')}'.`) + errThrown = true + } finally { + assert(errThrown) + const srclenAfter = klawSync(src).length + assert.strictEqual(srclenAfter, srclenBefore, 'src length should not change') + + const link = fs.readlinkSync(destLink) + assert.strictEqual(link, src) + } + }) + it('should copy the directory successfully when src is a subdir of resolved dest path', () => { const srcInDest = path.join(TEST_DIR, 'dest', 'some', 'nested', 'src') const destLink = path.join(TEST_DIR, 'dest-symlink') @@ -351,10 +401,6 @@ function testSuccess (src, dest) { fs.copySync(src, dest) - const destlen = klawSync(dest).length - - assert.strictEqual(destlen, srclen) - FILES.forEach(f => assert(fs.existsSync(path.join(dest, f)), 'file copied')) const o0 = fs.readFileSync(path.join(dest, FILES[0]), 'utf8') diff --git a/lib/copy-sync/__tests__/copy-sync-readonly-dir.test.js b/lib/copy-sync/__tests__/copy-sync-readonly-dir.test.js index b5648043..30ace539 100644 --- a/lib/copy-sync/__tests__/copy-sync-readonly-dir.test.js +++ b/lib/copy-sync/__tests__/copy-sync-readonly-dir.test.js @@ -2,9 +2,8 @@ // relevant: https://github.com/jprichardson/node-fs-extra/issues/599 -const fs = require(process.cwd()) const os = require('os') -const fse = require('../../') +const fs = require('../../') const path = require('path') const assert = require('assert') const klawSync = require('klaw-sync') @@ -22,12 +21,12 @@ const FILES = [ describe('+ copySync() - copy a readonly directory with content', () => { beforeEach(done => { TEST_DIR = path.join(os.tmpdir(), 'test', 'fs-extra', 'copy-readonly-dir') - fse.emptyDir(TEST_DIR, done) + fs.emptyDir(TEST_DIR, done) }) afterEach(done => { klawSync(TEST_DIR).forEach(data => fs.chmodSync(data.path, 0o777)) - fse.remove(TEST_DIR, done) + fs.remove(TEST_DIR, done) }) describe('> when src is readonly directory with content', () => { @@ -40,7 +39,7 @@ describe('+ copySync() - copy a readonly directory with content', () => { sourceHierarchy.forEach(source => fs.chmodSync(source.path, source.stats.isDirectory() ? 0o555 : 0o444)) const targetDir = path.join(TEST_DIR, 'target') - fse.copySync(sourceDir, targetDir) + fs.copySync(sourceDir, targetDir) // Make sure copy was made and mode was preserved assert(fs.existsSync(targetDir)) diff --git a/lib/copy-sync/__tests__/symlink.test.js b/lib/copy-sync/__tests__/symlink.test.js index 062ea0ef..1825eece 100644 --- a/lib/copy-sync/__tests__/symlink.test.js +++ b/lib/copy-sync/__tests__/symlink.test.js @@ -1,8 +1,7 @@ 'use strict' -const fs = require('fs') const os = require('os') -const fse = require(process.cwd()) +const fs = require('../../') const path = require('path') const assert = require('assert') const copySync = require('../copy-sync') @@ -15,14 +14,14 @@ describe('copy-sync / symlink', () => { const out = path.join(TEST_DIR, 'out') beforeEach(done => { - fse.emptyDir(TEST_DIR, err => { + fs.emptyDir(TEST_DIR, err => { assert.ifError(err) createFixtures(src, done) }) }) afterEach(done => { - fse.remove(TEST_DIR, done) + fs.remove(TEST_DIR, done) }) it('copies symlinks by default', () => { diff --git a/lib/copy-sync/copy-sync.js b/lib/copy-sync/copy-sync.js index 14ad9939..0ca35c85 100644 --- a/lib/copy-sync/copy-sync.js +++ b/lib/copy-sync/copy-sync.js @@ -1,11 +1,12 @@ 'use strict' -const fs = require('graceful-fs') +// TODO: enable this once graceful-fs supports bigint option. +// const fs = require('graceful-fs') +const fs = require('fs') const path = require('path') const mkdirpSync = require('../mkdirs').mkdirsSync const utimesSync = require('../util/utimes.js').utimesMillisSync - -const notExist = Symbol('notExist') +const stat = require('../util/stat') function copySync (src, dest, opts) { if (typeof opts === 'function') { @@ -22,10 +23,13 @@ function copySync (src, dest, opts) { see https://github.com/jprichardson/node-fs-extra/issues/269`) } - const destStat = checkPaths(src, dest) + const { srcStat, destStat } = stat.checkPathsSync(src, dest, 'copy') + stat.checkParentPathsSync(src, srcStat, dest, 'copy') + return handleFilterAndCopy(destStat, src, dest, opts) +} +function handleFilterAndCopy (destStat, src, dest, opts) { if (opts.filter && !opts.filter(src, dest)) return - const destParent = path.dirname(dest) if (!fs.existsSync(destParent)) mkdirpSync(destParent) return startCopy(destStat, src, dest, opts) @@ -48,7 +52,7 @@ function getStats (destStat, src, dest, opts) { } function onFile (srcStat, destStat, src, dest, opts) { - if (destStat === notExist) return copyFile(srcStat, src, dest, opts) + if (!destStat) return copyFile(srcStat, src, dest, opts) return mayCopyFile(srcStat, src, dest, opts) } @@ -94,7 +98,7 @@ function copyFileFallback (srcStat, src, dest, opts) { } function onDir (srcStat, destStat, src, dest, opts) { - if (destStat === notExist) return mkDirAndCopy(srcStat, src, dest, opts) + if (!destStat) return mkDirAndCopy(srcStat, src, dest, opts) if (destStat && !destStat.isDirectory()) { throw new Error(`Cannot overwrite non-directory '${dest}' with directory '${src}'.`) } @@ -114,18 +118,17 @@ function copyDir (src, dest, opts) { function copyDirItem (item, src, dest, opts) { const srcItem = path.join(src, item) const destItem = path.join(dest, item) - const destStat = checkPaths(srcItem, destItem) + const { destStat } = stat.checkPathsSync(srcItem, destItem, 'copy') return startCopy(destStat, srcItem, destItem, opts) } function onLink (destStat, src, dest, opts) { let resolvedSrc = fs.readlinkSync(src) - if (opts.dereference) { resolvedSrc = path.resolve(process.cwd(), resolvedSrc) } - if (destStat === notExist) { + if (!destStat) { return fs.symlinkSync(resolvedSrc, dest) } else { let resolvedDest @@ -141,14 +144,14 @@ function onLink (destStat, src, dest, opts) { if (opts.dereference) { resolvedDest = path.resolve(process.cwd(), resolvedDest) } - if (isSrcSubdir(resolvedSrc, resolvedDest)) { + if (stat.isSrcSubdir(resolvedSrc, resolvedDest)) { throw new Error(`Cannot copy '${resolvedSrc}' to a subdirectory of itself, '${resolvedDest}'.`) } // prevent copy if src is a subdir of dest since unlinking // dest in this case would result in removing src contents // and therefore a broken symlink would be created. - if (fs.statSync(dest).isDirectory() && isSrcSubdir(resolvedDest, resolvedSrc)) { + if (fs.statSync(dest).isDirectory() && stat.isSrcSubdir(resolvedDest, resolvedSrc)) { throw new Error(`Cannot overwrite '${resolvedDest}' with '${resolvedSrc}'.`) } return copyLink(resolvedSrc, dest) @@ -160,34 +163,4 @@ function copyLink (resolvedSrc, dest) { return fs.symlinkSync(resolvedSrc, dest) } -// return true if dest is a subdir of src, otherwise false. -function isSrcSubdir (src, dest) { - const srcArray = path.resolve(src).split(path.sep) - const destArray = path.resolve(dest).split(path.sep) - return srcArray.reduce((acc, current, i) => acc && destArray[i] === current, true) -} - -function checkStats (src, dest) { - const srcStat = fs.statSync(src) - let destStat - try { - destStat = fs.statSync(dest) - } catch (err) { - if (err.code === 'ENOENT') return {srcStat, destStat: notExist} - throw err - } - return {srcStat, destStat} -} - -function checkPaths (src, dest) { - const {srcStat, destStat} = checkStats(src, dest) - if (destStat.ino && destStat.ino === srcStat.ino) { - throw new Error('Source and destination must not be the same.') - } - if (srcStat.isDirectory() && isSrcSubdir(src, dest)) { - throw new Error(`Cannot copy '${src}' to a subdirectory of itself, '${dest}'.`) - } - return destStat -} - module.exports = copySync diff --git a/lib/copy/__tests__/copy-case-insensitive-paths.test.js b/lib/copy/__tests__/copy-case-insensitive-paths.test.js index 9c581324..7618510e 100644 --- a/lib/copy/__tests__/copy-case-insensitive-paths.test.js +++ b/lib/copy/__tests__/copy-case-insensitive-paths.test.js @@ -3,7 +3,8 @@ const assert = require('assert') const os = require('os') const path = require('path') -const fs = require(process.cwd()) +const fs = require('../../') +const platform = os.platform() /* global beforeEach, afterEach, describe, it */ @@ -26,15 +27,13 @@ describe('+ copy() - case insensitive paths', () => { dest = path.join(TEST_DIR, 'srcDir') fs.copy(src, dest, err => { - if (os === 'linux') { + if (platform === 'linux') { assert.ifError(err) assert(fs.existsSync(dest)) assert.strictEqual(fs.readFileSync(path.join(dest, 'subdir', 'file.txt'), 'utf8'), 'some data') } - if (os === 'darwin' || os === 'win32') { + if (platform === 'darwin' || platform === 'win32') { assert.strictEqual(err.message, 'Source and destination must not be the same.') - assert(fs.existsSync(src)) - assert(!fs.existsSync(dest)) } done() }) @@ -48,15 +47,13 @@ describe('+ copy() - case insensitive paths', () => { dest = path.join(TEST_DIR, 'srcFile') fs.copy(src, dest, err => { - if (os === 'linux') { + if (platform === 'linux') { assert.ifError(err) assert(fs.existsSync(dest)) assert.strictEqual(fs.readFileSync(dest, 'utf8'), 'some data') } - if (os === 'darwin' || os === 'win32') { + if (platform === 'darwin' || platform === 'win32') { assert.strictEqual(err.message, 'Source and destination must not be the same.') - assert(fs.existsSync(src)) - assert(!fs.existsSync(dest)) } done() }) @@ -69,20 +66,18 @@ describe('+ copy() - case insensitive paths', () => { fs.outputFileSync(path.join(src, 'subdir', 'file.txt'), 'some data') const srcLink = path.join(TEST_DIR, 'src-symlink') fs.symlinkSync(src, srcLink, 'dir') - dest = path.join(TEST_DIR, 'srcDir') + dest = path.join(TEST_DIR, 'src-Symlink') - fs.copy(src, dest, err => { - if (os === 'linux') { + fs.copy(srcLink, dest, err => { + if (platform === 'linux') { assert.ifError(err) assert(fs.existsSync(dest)) assert.strictEqual(fs.readFileSync(path.join(dest, 'subdir', 'file.txt'), 'utf8'), 'some data') - const link = fs.readlinkSync(srcLink) - assert.strictEqual(link, dest) + const destLink = fs.readlinkSync(dest) + assert.strictEqual(destLink, src) } - if (os === 'darwin' || os === 'win32') { + if (platform === 'darwin' || platform === 'win32') { assert.strictEqual(err.message, 'Source and destination must not be the same.') - assert(fs.existsSync(src)) - assert(!fs.existsSync(dest)) } done() }) @@ -93,20 +88,18 @@ describe('+ copy() - case insensitive paths', () => { fs.outputFileSync(src, 'some data') const srcLink = path.join(TEST_DIR, 'src-symlink') fs.symlinkSync(src, srcLink, 'file') - dest = path.join(TEST_DIR, 'srcFile') + dest = path.join(TEST_DIR, 'src-Symlink') - fs.copy(src, dest, err => { - if (os === 'linux') { + fs.copy(srcLink, dest, err => { + if (platform === 'linux') { assert.ifError(err) assert(fs.existsSync(dest)) assert.strictEqual(fs.readFileSync(dest, 'utf8'), 'some data') - const link = fs.readlinkSync(srcLink) - assert.strictEqual(link, dest) + const destLink = fs.readlinkSync(dest) + assert.strictEqual(destLink, src) } - if (os === 'darwin' || os === 'win32') { + if (platform === 'darwin' || platform === 'win32') { assert.strictEqual(err.message, 'Source and destination must not be the same.') - assert(fs.existsSync(src)) - assert(!fs.existsSync(dest)) } done() }) diff --git a/lib/copy/__tests__/copy-gh-89.test.js b/lib/copy/__tests__/copy-gh-89.test.js index 339fc567..537dae8f 100644 --- a/lib/copy/__tests__/copy-gh-89.test.js +++ b/lib/copy/__tests__/copy-gh-89.test.js @@ -5,7 +5,7 @@ const fs = require('fs') const os = require('os') -const fse = require(process.cwd()) +const fse = require('../../') const path = require('path') const assert = require('assert') diff --git a/lib/copy/__tests__/copy-preserve-time.test.js b/lib/copy/__tests__/copy-preserve-timestamp.test.js similarity index 93% rename from lib/copy/__tests__/copy-preserve-time.test.js rename to lib/copy/__tests__/copy-preserve-timestamp.test.js index ae83ab4c..3e1e5584 100644 --- a/lib/copy/__tests__/copy-preserve-time.test.js +++ b/lib/copy/__tests__/copy-preserve-timestamp.test.js @@ -1,13 +1,14 @@ 'use strict' -const fs = require(process.cwd()) +const fs = require('../../') const os = require('os') const path = require('path') const copy = require('../copy') const utimes = require('../../util/utimes') const assert = require('assert') -const nodeVersion = process.versions.node -const nodeVersionMajor = parseInt(nodeVersion.split('.')[0], 10) +const semver = require('semver') +const nodeVersion = process.version +const nodeVersionMajor = semver.major(nodeVersion) /* global beforeEach, afterEach, describe, it */ diff --git a/lib/copy/__tests__/copy-prevent-copying-identical.test.js b/lib/copy/__tests__/copy-prevent-copying-identical.test.js index b2aa211b..d30ec91b 100644 --- a/lib/copy/__tests__/copy-prevent-copying-identical.test.js +++ b/lib/copy/__tests__/copy-prevent-copying-identical.test.js @@ -3,7 +3,7 @@ const assert = require('assert') const os = require('os') const path = require('path') -const fs = require(process.cwd()) +const fs = require('../../') const klawSync = require('klaw-sync') /* global beforeEach, afterEach, describe, it */ diff --git a/lib/copy/__tests__/copy-prevent-copying-into-itself.test.js b/lib/copy/__tests__/copy-prevent-copying-into-itself.test.js index a3b944f7..552cbded 100644 --- a/lib/copy/__tests__/copy-prevent-copying-into-itself.test.js +++ b/lib/copy/__tests__/copy-prevent-copying-into-itself.test.js @@ -3,7 +3,7 @@ const assert = require('assert') const os = require('os') const path = require('path') -const fs = require(process.cwd()) +const fs = require('../../') const klawSync = require('klaw-sync') /* global beforeEach, afterEach, describe, it */ @@ -168,6 +168,48 @@ describe('+ copy() - prevent copying into itself', () => { }) }) + it('should error when dest is a subdirectory of src (bind-mounted directory with subdirectory)', done => { + const destLink = path.join(TEST_DIR, 'dest-symlink') + fs.symlinkSync(src, destLink, 'dir') + + const srclenBefore = klawSync(src).length + assert(srclenBefore > 2) + + const dest = path.join(destLink, 'dir1') + assert(fs.existsSync(dest)) + fs.copy(src, dest, err => { + assert.strictEqual(err.message, `Cannot copy '${src}' to a subdirectory of itself, '${dest}'.`) + + const srclenAfter = klawSync(src).length + assert.strictEqual(srclenAfter, srclenBefore, 'src length should not change') + + const link = fs.readlinkSync(destLink) + assert.strictEqual(link, src) + done() + }) + }) + + it('should error when dest is a subdirectory of src (more than one level depth)', done => { + const destLink = path.join(TEST_DIR, 'dest-symlink') + fs.symlinkSync(src, destLink, 'dir') + + const srclenBefore = klawSync(src).length + assert(srclenBefore > 2) + + const dest = path.join(destLink, 'dir1', 'dir2') + assert(fs.existsSync(dest)) + fs.copy(src, dest, err => { + assert.strictEqual(err.message, `Cannot copy '${src}' to a subdirectory of itself, '${path.join(destLink, 'dir1')}'.`) + + const srclenAfter = klawSync(src).length + assert.strictEqual(srclenAfter, srclenBefore, 'src length should not change') + + const link = fs.readlinkSync(destLink) + assert.strictEqual(link, src) + done() + }) + }) + it('should copy the directory successfully when src is a subdir of resolved dest path', done => { const srcInDest = path.join(TEST_DIR, 'dest', 'some', 'nested', 'src') const destLink = path.join(TEST_DIR, 'dest-symlink') @@ -350,10 +392,6 @@ function testSuccess (src, dest, done) { fs.copy(src, dest, err => { assert.ifError(err) - const destlen = klawSync(dest).length - - assert.strictEqual(destlen, srclen) - FILES.forEach(f => assert(fs.existsSync(path.join(dest, f)), 'file copied')) const o0 = fs.readFileSync(path.join(dest, FILES[0]), 'utf8') diff --git a/lib/copy/__tests__/copy-readonly-dir.test.js b/lib/copy/__tests__/copy-readonly-dir.test.js index 99a3d9af..7a21f4aa 100644 --- a/lib/copy/__tests__/copy-readonly-dir.test.js +++ b/lib/copy/__tests__/copy-readonly-dir.test.js @@ -2,7 +2,7 @@ // relevant: https://github.com/jprichardson/node-fs-extra/issues/599 -const fs = require(process.cwd()) +const fs = require('../../') const os = require('os') const fse = require('../../') const path = require('path') diff --git a/lib/copy/__tests__/copy.test.js b/lib/copy/__tests__/copy.test.js index 23d80863..9c1ce7f8 100644 --- a/lib/copy/__tests__/copy.test.js +++ b/lib/copy/__tests__/copy.test.js @@ -144,7 +144,9 @@ describe('fs-extra', () => { const srcTarget = path.join(TEST_DIR, 'destination') fse.mkdirSync(src) fse.mkdirSync(srcTarget) - fse.symlinkSync(srcTarget, path.join(src, 'symlink')) + // symlink type is only used for Windows and the default is 'file'. + // https://nodejs.org/api/fs.html#fs_fs_symlink_target_path_type_callback + fse.symlinkSync(srcTarget, path.join(src, 'symlink'), 'dir') fse.copy(src, dest, err => { assert.ifError(err) @@ -340,11 +342,12 @@ describe('fs-extra', () => { const dest = path.join(TEST_DIR, 'dest') - fse.copySync(src, dest, filter) - - assert(!fs.existsSync(path.join(dest, IGNORE)), 'directory was not ignored') - assert(!fs.existsSync(path.join(dest, IGNORE, 'file')), 'file was not ignored') - done() + fse.copy(src, dest, filter, err => { + assert.ifError(err) + assert(!fs.existsSync(path.join(dest, IGNORE)), 'directory was not ignored') + assert(!fs.existsSync(path.join(dest, IGNORE, 'file')), 'file was not ignored') + done() + }) }) it('should apply filter when it is applied only to dest', done => { diff --git a/lib/copy/copy.js b/lib/copy/copy.js index 3dfbc540..b5738b72 100644 --- a/lib/copy/copy.js +++ b/lib/copy/copy.js @@ -1,12 +1,13 @@ 'use strict' -const fs = require('graceful-fs') +// TODO: enable this once graceful-fs supports bigint option. +// const fs = require('graceful-fs') +const fs = require('fs') const path = require('path') const mkdirp = require('../mkdirs').mkdirs const pathExists = require('../path-exists').pathExists const utimes = require('../util/utimes').utimesMillis - -const notExist = Symbol('notExist') +const stat = require('../util/stat') function copy (src, dest, opts, cb) { if (typeof opts === 'function' && !cb) { @@ -28,10 +29,14 @@ function copy (src, dest, opts, cb) { see https://github.com/jprichardson/node-fs-extra/issues/269`) } - checkPaths(src, dest, (err, destStat) => { + stat.checkPaths(src, dest, 'copy', (err, stats) => { if (err) return cb(err) - if (opts.filter) return handleFilter(checkParentDir, destStat, src, dest, opts, cb) - return checkParentDir(destStat, src, dest, opts, cb) + const { srcStat, destStat } = stats + stat.checkParentPaths(src, srcStat, dest, 'copy', err => { + if (err) return cb(err) + if (opts.filter) return handleFilter(checkParentDir, destStat, src, dest, opts, cb) + return checkParentDir(destStat, src, dest, opts, cb) + }) }) } @@ -49,10 +54,7 @@ function checkParentDir (destStat, src, dest, opts, cb) { function handleFilter (onInclude, destStat, src, dest, opts, cb) { Promise.resolve(opts.filter(src, dest)).then(include => { - if (include) { - if (destStat) return onInclude(destStat, src, dest, opts, cb) - return onInclude(src, dest, opts, cb) - } + if (include) return onInclude(destStat, src, dest, opts, cb) return cb() }, error => cb(error)) } @@ -76,7 +78,7 @@ function getStats (destStat, src, dest, opts, cb) { } function onFile (srcStat, destStat, src, dest, opts, cb) { - if (destStat === notExist) return copyFile(srcStat, src, dest, opts, cb) + if (!destStat) return copyFile(srcStat, src, dest, opts, cb) return mayCopyFile(srcStat, src, dest, opts, cb) } @@ -122,7 +124,7 @@ function setDestModeAndTimestamps (srcStat, dest, opts, cb) { } function onDir (srcStat, destStat, src, dest, opts, cb) { - if (destStat === notExist) return mkDirAndCopy(srcStat, src, dest, opts, cb) + if (!destStat) return mkDirAndCopy(srcStat, src, dest, opts, cb) if (destStat && !destStat.isDirectory()) { return cb(new Error(`Cannot overwrite non-directory '${dest}' with directory '${src}'.`)) } @@ -155,8 +157,9 @@ function copyDirItems (items, src, dest, opts, cb) { function copyDirItem (items, item, src, dest, opts, cb) { const srcItem = path.join(src, item) const destItem = path.join(dest, item) - checkPaths(srcItem, destItem, (err, destStat) => { + stat.checkPaths(srcItem, destItem, 'copy', (err, stats) => { if (err) return cb(err) + const { destStat } = stats startCopy(destStat, srcItem, destItem, opts, err => { if (err) return cb(err) return copyDirItems(items, src, dest, opts, cb) @@ -167,12 +170,11 @@ function copyDirItem (items, item, src, dest, opts, cb) { function onLink (destStat, src, dest, opts, cb) { fs.readlink(src, (err, resolvedSrc) => { if (err) return cb(err) - if (opts.dereference) { resolvedSrc = path.resolve(process.cwd(), resolvedSrc) } - if (destStat === notExist) { + if (!destStat) { return fs.symlink(resolvedSrc, dest, cb) } else { fs.readlink(dest, (err, resolvedDest) => { @@ -186,14 +188,14 @@ function onLink (destStat, src, dest, opts, cb) { if (opts.dereference) { resolvedDest = path.resolve(process.cwd(), resolvedDest) } - if (isSrcSubdir(resolvedSrc, resolvedDest)) { + if (stat.isSrcSubdir(resolvedSrc, resolvedDest)) { return cb(new Error(`Cannot copy '${resolvedSrc}' to a subdirectory of itself, '${resolvedDest}'.`)) } // do not copy if src is a subdir of dest since unlinking // dest in this case would result in removing src contents // and therefore a broken symlink would be created. - if (destStat.isDirectory() && isSrcSubdir(resolvedDest, resolvedSrc)) { + if (destStat.isDirectory() && stat.isSrcSubdir(resolvedDest, resolvedSrc)) { return cb(new Error(`Cannot overwrite '${resolvedDest}' with '${resolvedSrc}'.`)) } return copyLink(resolvedSrc, dest, cb) @@ -209,38 +211,4 @@ function copyLink (resolvedSrc, dest, cb) { }) } -// return true if dest is a subdir of src, otherwise false. -function isSrcSubdir (src, dest) { - const srcArray = path.resolve(src).split(path.sep) - const destArray = path.resolve(dest).split(path.sep) - return srcArray.reduce((acc, current, i) => acc && destArray[i] === current, true) -} - -function checkStats (src, dest, cb) { - fs.stat(src, (err, srcStat) => { - if (err) return cb(err) - fs.stat(dest, (err, destStat) => { - if (err) { - if (err.code === 'ENOENT') return cb(null, {srcStat, destStat: notExist}) - return cb(err) - } - return cb(null, {srcStat, destStat}) - }) - }) -} - -function checkPaths (src, dest, cb) { - checkStats(src, dest, (err, stats) => { - if (err) return cb(err) - const {srcStat, destStat} = stats - if (destStat.ino && destStat.ino === srcStat.ino) { - return cb(new Error('Source and destination must not be the same.')) - } - if (srcStat.isDirectory() && isSrcSubdir(src, dest)) { - return cb(new Error(`Cannot copy '${src}' to a subdirectory of itself, '${dest}'.`)) - } - return cb(null, destStat) - }) -} - module.exports = copy diff --git a/lib/move-sync/__tests__/move-sync-case-insensitive-paths.test.js b/lib/move-sync/__tests__/move-sync-case-insensitive-paths.test.js new file mode 100644 index 00000000..32600b88 --- /dev/null +++ b/lib/move-sync/__tests__/move-sync-case-insensitive-paths.test.js @@ -0,0 +1,124 @@ +'use strict' + +const assert = require('assert') +const os = require('os') +const path = require('path') +const fs = require('../../') +const platform = os.platform() + +/* global beforeEach, afterEach, describe, it */ + +describe('+ moveSync() - case insensitive paths', () => { + let TEST_DIR = '' + let src = '' + let dest = '' + + beforeEach(done => { + TEST_DIR = path.join(os.tmpdir(), 'fs-extra', 'move-sync-case-insensitive-paths') + fs.emptyDir(TEST_DIR, done) + }) + + afterEach(() => fs.removeSync(TEST_DIR)) + + describe('> when src is a directory', () => { + it('should behave correctly based on the OS', () => { + src = path.join(TEST_DIR, 'srcdir') + fs.outputFileSync(path.join(src, 'subdir', 'file.txt'), 'some data') + dest = path.join(TEST_DIR, 'srcDir') + let errThrown = false + + try { + fs.moveSync(src, dest) + } catch (err) { + if (platform === 'darwin' || platform === 'win32') { + assert.strictEqual(err.message, 'Source and destination must not be the same.') + errThrown = true + } + } + if (platform === 'darwin' || platform === 'win32') assert(errThrown) + if (platform === 'linux') { + assert(fs.existsSync(dest)) + assert.strictEqual(fs.readFileSync(path.join(dest, 'subdir', 'file.txt'), 'utf8'), 'some data') + assert(!errThrown) + } + }) + }) + + describe('> when src is a file', () => { + it('should behave correctly based on the OS', () => { + src = path.join(TEST_DIR, 'srcfile') + fs.outputFileSync(src, 'some data') + dest = path.join(TEST_DIR, 'srcFile') + let errThrown = false + + try { + fs.moveSync(src, dest) + } catch (err) { + if (platform === 'darwin' || platform === 'win32') { + assert.strictEqual(err.message, 'Source and destination must not be the same.') + errThrown = true + } + } + if (platform === 'darwin' || platform === 'win32') assert(errThrown) + if (platform === 'linux') { + assert(fs.existsSync(dest)) + assert.strictEqual(fs.readFileSync(dest, 'utf8'), 'some data') + assert(!errThrown) + } + }) + }) + + describe('> when src is a symlink', () => { + it('should behave correctly based on the OS, symlink dir', () => { + src = path.join(TEST_DIR, 'srcdir') + fs.outputFileSync(path.join(src, 'subdir', 'file.txt'), 'some data') + const srcLink = path.join(TEST_DIR, 'src-symlink') + fs.symlinkSync(src, srcLink, 'dir') + dest = path.join(TEST_DIR, 'src-Symlink') + let errThrown = false + + try { + fs.moveSync(srcLink, dest) + } catch (err) { + if (platform === 'darwin' || platform === 'win32') { + assert.strictEqual(err.message, 'Source and destination must not be the same.') + errThrown = true + } + } + if (platform === 'darwin' || platform === 'win32') assert(errThrown) + if (platform === 'linux') { + assert(fs.existsSync(dest)) + assert.strictEqual(fs.readFileSync(path.join(dest, 'subdir', 'file.txt'), 'utf8'), 'some data') + const destLink = fs.readlinkSync(dest) + assert.strictEqual(destLink, src) + assert(!errThrown) + } + }) + + it('should behave correctly based on the OS, symlink file', () => { + src = path.join(TEST_DIR, 'srcfile') + fs.outputFileSync(src, 'some data') + const srcLink = path.join(TEST_DIR, 'src-symlink') + fs.symlinkSync(src, srcLink, 'file') + dest = path.join(TEST_DIR, 'src-Symlink') + let errThrown = false + + try { + fs.moveSync(srcLink, dest) + } catch (err) { + if (platform === 'darwin' || platform === 'win32') { + assert.strictEqual(err.message, 'Source and destination must not be the same.') + errThrown = true + } + } + if (platform === 'darwin' || platform === 'win32') assert(errThrown) + if (platform === 'linux') { + assert(fs.existsSync(dest)) + assert.strictEqual(fs.readFileSync(dest, 'utf8'), 'some data') + const destLink = fs.readlinkSync(dest) + assert.strictEqual(destLink, src) + assert(!errThrown) + } + }) + }) +}) diff --git a/lib/move-sync/__tests__/move-sync-prevent-moving-identical.test.js b/lib/move-sync/__tests__/move-sync-prevent-moving-identical.test.js new file mode 100644 index 00000000..238cb2a3 --- /dev/null +++ b/lib/move-sync/__tests__/move-sync-prevent-moving-identical.test.js @@ -0,0 +1,271 @@ +'use strict' + +const assert = require('assert') +const os = require('os') +const path = require('path') +const fs = require('../../') +const klawSync = require('klaw-sync') + +/* global beforeEach, afterEach, describe, it */ + +describe('+ moveSync() - prevent moving identical files and dirs', () => { + let TEST_DIR = '' + let src = '' + let dest = '' + + beforeEach(done => { + TEST_DIR = path.join(os.tmpdir(), 'fs-extra', 'move-sync-prevent-moving-identical') + fs.emptyDir(TEST_DIR, done) + }) + + afterEach(done => fs.remove(TEST_DIR, done)) + + it('should return an error if src and dest are the same', () => { + const fileSrc = path.join(TEST_DIR, 'TEST_fs-extra_move_sync') + const fileDest = path.join(TEST_DIR, 'TEST_fs-extra_move_sync') + fs.ensureFileSync(fileSrc) + + try { + fs.moveSync(fileSrc, fileDest) + } catch (err) { + assert.strictEqual(err.message, 'Source and destination must not be the same.') + } + }) + + describe('dest with parent symlink', () => { + describe('first parent is symlink', () => { + it('should error when src is file', () => { + const src = path.join(TEST_DIR, 'a', 'file.txt') + const dest = path.join(TEST_DIR, 'b', 'file.txt') + const srcParent = path.join(TEST_DIR, 'a') + const destParent = path.join(TEST_DIR, 'b') + fs.ensureFileSync(src) + fs.ensureSymlinkSync(srcParent, destParent, 'dir') + + try { + fs.moveSync(src, dest) + } catch (err) { + assert.strictEqual(err.message, 'Source and destination must not be the same.') + } finally { + assert(fs.existsSync(src)) + } + }) + + it('should error when src is directory', () => { + const src = path.join(TEST_DIR, 'a', 'foo') + const dest = path.join(TEST_DIR, 'b', 'foo') + const srcParent = path.join(TEST_DIR, 'a') + const destParent = path.join(TEST_DIR, 'b') + fs.ensureDirSync(src) + fs.ensureSymlinkSync(srcParent, destParent, 'dir') + + try { + fs.moveSync(src, dest) + } catch (err) { + assert.strictEqual(err.message, 'Source and destination must not be the same.') + } finally { + assert(fs.existsSync(src)) + } + }) + }) + + describe('nested dest', () => { + it('should error when src is file', () => { + const src = path.join(TEST_DIR, 'a', 'dir', 'file.txt') + const dest = path.join(TEST_DIR, 'b', 'dir', 'file.txt') + const srcParent = path.join(TEST_DIR, 'a') + const destParent = path.join(TEST_DIR, 'b') + fs.ensureFileSync(src) + fs.ensureSymlinkSync(srcParent, destParent, 'dir') + + try { + fs.moveSync(src, dest) + } catch (err) { + assert.strictEqual(err.message, 'Source and destination must not be the same.') + } finally { + assert(fs.existsSync(src)) + } + }) + + it('should error when src is directory', () => { + const src = path.join(TEST_DIR, 'a', 'dir', 'foo') + const dest = path.join(TEST_DIR, 'b', 'dir', 'foo') + const srcParent = path.join(TEST_DIR, 'a') + const destParent = path.join(TEST_DIR, 'b') + fs.ensureDirSync(src) + fs.ensureSymlinkSync(srcParent, destParent, 'dir') + + try { + fs.moveSync(src, dest) + } catch (err) { + assert.strictEqual(err.message, 'Source and destination must not be the same.') + } finally { + assert(fs.existsSync(src)) + } + }) + }) + }) + + // src is directory: + // src is regular, dest is symlink + // src is symlink, dest is regular + // src is symlink, dest is symlink + + describe('> when src is a directory', () => { + describe(`>> when src is regular and dest is a symlink that points to src`, () => { + it('should error', () => { + src = path.join(TEST_DIR, 'src') + fs.mkdirsSync(src) + const subdir = path.join(TEST_DIR, 'src', 'subdir') + fs.mkdirsSync(subdir) + fs.writeFileSync(path.join(subdir, 'file.txt'), 'some data') + + const destLink = path.join(TEST_DIR, 'dest-symlink') + fs.symlinkSync(src, destLink, 'dir') + + const oldlen = klawSync(src).length + + try { + fs.moveSync(src, destLink) + } catch (err) { + assert.strictEqual(err.message, 'Source and destination must not be the same.') + } + + const newlen = klawSync(src).length + assert.strictEqual(newlen, oldlen) + const link = fs.readlinkSync(destLink) + assert.strictEqual(link, src) + }) + }) + + describe(`>> when src is a symlink that points to a regular dest`, () => { + it('should throw error', () => { + dest = path.join(TEST_DIR, 'dest') + fs.mkdirsSync(dest) + const subdir = path.join(TEST_DIR, 'dest', 'subdir') + fs.mkdirsSync(subdir) + fs.writeFileSync(path.join(subdir, 'file.txt'), 'some data') + + const srcLink = path.join(TEST_DIR, 'src-symlink') + fs.symlinkSync(dest, srcLink, 'dir') + + const oldlen = klawSync(dest).length + + try { + fs.moveSync(srcLink, dest) + } catch (err) { + assert(err) + } + + // assert nothing copied + const newlen = klawSync(dest).length + assert.strictEqual(newlen, oldlen) + const link = fs.readlinkSync(srcLink) + assert.strictEqual(link, dest) + }) + }) + + describe('>> when src and dest are symlinks that point to the exact same path', () => { + it('should error src and dest are the same', () => { + src = path.join(TEST_DIR, 'src') + fs.mkdirsSync(src) + const srcLink = path.join(TEST_DIR, 'src_symlink') + fs.symlinkSync(src, srcLink, 'dir') + const destLink = path.join(TEST_DIR, 'dest_symlink') + fs.symlinkSync(src, destLink, 'dir') + + const srclenBefore = klawSync(srcLink).length + const destlenBefore = klawSync(destLink).length + + try { + fs.moveSync(srcLink, destLink) + } catch (err) { + assert.strictEqual(err.message, 'Source and destination must not be the same.') + } + + const srclenAfter = klawSync(srcLink).length + assert.strictEqual(srclenAfter, srclenBefore, 'src length should not change') + const destlenAfter = klawSync(destLink).length + assert.strictEqual(destlenAfter, destlenBefore, 'dest length should not change') + + const srcln = fs.readlinkSync(srcLink) + assert.strictEqual(srcln, src) + const destln = fs.readlinkSync(destLink) + assert.strictEqual(destln, src) + }) + }) + }) + + // src is file: + // src is regular, dest is symlink + // src is symlink, dest is regular + // src is symlink, dest is symlink + + describe('> when src is a file', () => { + describe(`>> when src is regular and dest is a symlink that points to src`, () => { + it('should error', () => { + src = path.join(TEST_DIR, 'src', 'somefile.txt') + fs.ensureFileSync(src) + fs.writeFileSync(src, 'some data') + + const destLink = path.join(TEST_DIR, 'dest-symlink') + fs.symlinkSync(src, destLink, 'file') + + try { + fs.moveSync(src, destLink) + } catch (err) { + assert.strictEqual(err.message, 'Source and destination must not be the same.') + } + + const link = fs.readlinkSync(destLink) + assert.strictEqual(link, src) + assert(fs.readFileSync(link, 'utf8'), 'some data') + }) + }) + + describe(`>> when src is a symlink that points to a regular dest`, () => { + it('should throw error', () => { + dest = path.join(TEST_DIR, 'dest', 'somefile.txt') + fs.outputFileSync(dest, 'some data') + + const srcLink = path.join(TEST_DIR, 'src-symlink') + fs.symlinkSync(dest, srcLink, 'file') + + try { + fs.moveSync(srcLink, dest) + } catch (err) { + assert.ok(err) + } + const link = fs.readlinkSync(srcLink) + assert.strictEqual(link, dest) + assert(fs.readFileSync(link, 'utf8'), 'some data') + }) + }) + + describe('>> when src and dest are symlinks that point to the exact same path', () => { + it('should error src and dest are the same', () => { + src = path.join(TEST_DIR, 'src', 'srcfile.txt') + fs.outputFileSync(src, 'src data') + + const srcLink = path.join(TEST_DIR, 'src_symlink') + fs.symlinkSync(src, srcLink, 'file') + + const destLink = path.join(TEST_DIR, 'dest_symlink') + fs.symlinkSync(src, destLink, 'file') + + try { + fs.moveSync(srcLink, destLink) + } catch (err) { + assert.strictEqual(err.message, 'Source and destination must not be the same.') + } + + const srcln = fs.readlinkSync(srcLink) + assert.strictEqual(srcln, src) + const destln = fs.readlinkSync(destLink) + assert.strictEqual(destln, src) + assert(fs.readFileSync(srcln, 'utf8'), 'src data') + assert(fs.readFileSync(destln, 'utf8'), 'src data') + }) + }) + }) +}) diff --git a/lib/move-sync/__tests__/move-sync-prevent-moving-into-itself.test.js b/lib/move-sync/__tests__/move-sync-prevent-moving-into-itself.test.js index 3685faaf..2cdfadfd 100644 --- a/lib/move-sync/__tests__/move-sync-prevent-moving-into-itself.test.js +++ b/lib/move-sync/__tests__/move-sync-prevent-moving-into-itself.test.js @@ -3,11 +3,12 @@ const assert = require('assert') const os = require('os') const path = require('path') -const fs = require(process.cwd()) +const fs = require('../../') const klawSync = require('klaw-sync') /* global beforeEach, afterEach, describe, it */ +// these files are used for all tests const FILES = [ 'file0.txt', path.join('dir1', 'file1.txt'), @@ -21,20 +22,35 @@ const dat2 = 'file2' const dat3 = 'file3' describe('+ moveSync() - prevent moving into itself', () => { - let TEST_DIR, src, dest + let TEST_DIR, src - beforeEach(() => { + beforeEach(done => { TEST_DIR = path.join(os.tmpdir(), 'fs-extra', 'move-sync-prevent-moving-into-itself') src = path.join(TEST_DIR, 'src') - fs.mkdirsSync(src) + fs.mkdirpSync(src) fs.outputFileSync(path.join(src, FILES[0]), dat0) fs.outputFileSync(path.join(src, FILES[1]), dat1) fs.outputFileSync(path.join(src, FILES[2]), dat2) fs.outputFileSync(path.join(src, FILES[3]), dat3) + done() }) - afterEach(() => fs.removeSync(TEST_DIR)) + afterEach(done => fs.remove(TEST_DIR, done)) + + describe('> when source is a file', () => { + it('should move the file successfully even if dest parent is a subdir of src', () => { + const srcFile = path.join(TEST_DIR, 'src', 'srcfile.txt') + const destFile = path.join(TEST_DIR, 'src', 'dest', 'destfile.txt') + fs.writeFileSync(srcFile, dat0) + + fs.moveSync(srcFile, destFile) + + assert(fs.existsSync(destFile)) + const out = fs.readFileSync(destFile, 'utf8') + assert.strictEqual(out, dat0, 'file contents matched') + }) + }) describe('> when source is a file', () => { it(`should move the file successfully even when dest parent is 'src/dest'`, () => { @@ -64,82 +80,276 @@ describe('+ moveSync() - prevent moving into itself', () => { }) describe('> when source is a directory', () => { - it(`should move the directory successfully when dest is 'src_dest'`, () => { - dest = path.join(TEST_DIR, 'src_dest') - return testSuccessDir(src, dest) - }) - - it(`should move the directory successfully when dest is 'src-dest'`, () => { - dest = path.join(TEST_DIR, 'src-dest') - return testSuccessDir(src, dest) - }) - - it(`should move the directory successfully when dest is 'dest_src'`, () => { - dest = path.join(TEST_DIR, 'dest_src') - return testSuccessDir(src, dest) - }) - - it(`should move the directory successfully when dest is 'src_dest/src'`, () => { - dest = path.join(TEST_DIR, 'src_dest', 'src') - return testSuccessDir(src, dest) - }) - - it(`should move the directory successfully when dest is 'src-dest/src'`, () => { - dest = path.join(TEST_DIR, 'src-dest', 'src') - return testSuccessDir(src, dest) - }) - - it(`should move the directory successfully when dest is 'dest_src/src'`, () => { - dest = path.join(TEST_DIR, 'dest_src', 'src') - return testSuccessDir(src, dest) + describe('>> when dest is a directory', () => { + it(`of not itself`, () => { + const dest = path.join(TEST_DIR, src.replace(/^\w:/, '')) + return testSuccessDir(src, dest) + }) + it(`of itself`, () => { + const dest = path.join(src, 'dest') + return testError(src, dest) + }) + it(`should move the directory successfully when dest is 'src_dest'`, () => { + const dest = path.join(TEST_DIR, 'src_dest') + return testSuccessDir(src, dest) + }) + it(`should move the directory successfully when dest is 'src-dest'`, () => { + const dest = path.join(TEST_DIR, 'src-dest') + return testSuccessDir(src, dest) + }) + + it(`should move the directory successfully when dest is 'dest_src'`, () => { + const dest = path.join(TEST_DIR, 'dest_src') + return testSuccessDir(src, dest) + }) + + it(`should move the directory successfully when dest is 'src_dest/src'`, () => { + const dest = path.join(TEST_DIR, 'src_dest', 'src') + return testSuccessDir(src, dest) + }) + + it(`should move the directory successfully when dest is 'src-dest/src'`, () => { + const dest = path.join(TEST_DIR, 'src-dest', 'src') + return testSuccessDir(src, dest) + }) + + it(`should move the directory successfully when dest is 'dest_src/src'`, () => { + const dest = path.join(TEST_DIR, 'dest_src', 'src') + return testSuccessDir(src, dest) + }) + + it(`should move the directory successfully when dest is 'src_src/dest'`, () => { + const dest = path.join(TEST_DIR, 'src_src', 'dest') + return testSuccessDir(src, dest) + }) + + it(`should move the directory successfully when dest is 'src-src/dest'`, () => { + const dest = path.join(TEST_DIR, 'src-src', 'dest') + return testSuccessDir(src, dest) + }) + + it(`should move the directory successfully when dest is 'srcsrc/dest'`, () => { + const dest = path.join(TEST_DIR, 'srcsrc', 'dest') + return testSuccessDir(src, dest) + }) + + it(`should move the directory successfully when dest is 'dest/src'`, () => { + const dest = path.join(TEST_DIR, 'dest', 'src') + return testSuccessDir(src, dest) + }) + + it('should move the directory successfully when dest is very nested that all its parents need to be created', () => { + const dest = path.join(TEST_DIR, 'dest', 'src', 'foo', 'bar', 'baz', 'qux', 'quux', 'waldo', + 'grault', 'garply', 'fred', 'plugh', 'thud', 'some', 'highly', 'deeply', + 'badly', 'nasty', 'crazy', 'mad', 'nested', 'dest') + return testSuccessDir(src, dest) + }) + + it(`should error when dest is 'src/dest'`, () => { + const dest = path.join(TEST_DIR, 'src', 'dest') + return testError(src, dest) + }) + + it(`should error when dest is 'src/src_dest'`, () => { + const dest = path.join(TEST_DIR, 'src', 'src_dest') + return testError(src, dest) + }) + + it(`should error when dest is 'src/dest_src'`, () => { + const dest = path.join(TEST_DIR, 'src', 'dest_src') + return testError(src, dest) + }) + + it(`should error when dest is 'src/dest/src'`, () => { + const dest = path.join(TEST_DIR, 'src', 'dest', 'src') + return testError(src, dest) + }) }) - it(`should move the directory successfully when dest is 'src_src/dest'`, () => { - dest = path.join(TEST_DIR, 'src_src', 'dest') - return testSuccessDir(src, dest) - }) - - it(`should move the directory successfully when dest is 'src-src/dest'`, () => { - dest = path.join(TEST_DIR, 'src-src', 'dest') - return testSuccessDir(src, dest) - }) - - it(`should move the directory successfully when dest is 'srcsrc/dest'`, () => { - dest = path.join(TEST_DIR, 'srcsrc', 'dest') - return testSuccessDir(src, dest) - }) - - it(`should move the directory successfully when dest is 'dest/src'`, () => { - dest = path.join(TEST_DIR, 'dest', 'src') - return testSuccessDir(src, dest) - }) - - it('should move the directory successfully when dest is very nested that all its parents need to be created', () => { - dest = path.join(TEST_DIR, 'dest', 'src', 'foo', 'bar', 'baz', 'qux', 'quux', 'waldo', - 'grault', 'garply', 'fred', 'plugh', 'thud', 'some', 'highly', 'deeply', - 'badly', 'nasty', 'crazy', 'mad', 'nested', 'dest') - assert(!fs.existsSync(dest)) - return testSuccessDir(src, dest) - }) - - it(`should throw error when dest is 'src/dest'`, () => { - dest = path.join(TEST_DIR, 'src', 'dest') - return testError(src, dest) - }) - - it(`should throw error when dest is 'src/src_dest'`, () => { - dest = path.join(TEST_DIR, 'src', 'src_dest') - return testError(src, dest) + describe('>> when dest is a symlink', () => { + it('should error when dest points exactly to src', () => { + const destLink = path.join(TEST_DIR, 'dest-symlink') + fs.symlinkSync(src, destLink, 'dir') + + const srclenBefore = klawSync(src).length + assert(srclenBefore > 2) + let errThrown = false + try { + fs.moveSync(src, destLink) + } catch (err) { + assert.strictEqual(err.message, 'Source and destination must not be the same.') + errThrown = true + } finally { + assert(errThrown) + const srclenAfter = klawSync(src).length + assert.strictEqual(srclenAfter, srclenBefore, 'src length should not change') + const link = fs.readlinkSync(destLink) + assert.strictEqual(link, src) + } + }) + + it('should error when dest is a subdirectory of src (bind-mounted directory with subdirectory)', () => { + const destLink = path.join(TEST_DIR, 'dest-symlink') + fs.symlinkSync(src, destLink, 'dir') + + const srclenBefore = klawSync(src).length + assert(srclenBefore > 2) + + const dest = path.join(destLink, 'dir1') + assert(fs.existsSync(dest)) + let errThrown = false + try { + fs.moveSync(src, dest) + } catch (err) { + assert.strictEqual(err.message, `Cannot move '${src}' to a subdirectory of itself, '${dest}'.`) + errThrown = true + } finally { + assert(errThrown) + const srclenAfter = klawSync(src).length + assert.strictEqual(srclenAfter, srclenBefore, 'src length should not change') + const link = fs.readlinkSync(destLink) + assert.strictEqual(link, src) + } + }) + + it('should error when dest is a subdirectory of src (more than one level depth)', () => { + const destLink = path.join(TEST_DIR, 'dest-symlink') + fs.symlinkSync(src, destLink, 'dir') + + const srclenBefore = klawSync(src).length + assert(srclenBefore > 2) + + const dest = path.join(destLink, 'dir1', 'dir2') + assert(fs.existsSync(dest)) + let errThrown = false + try { + fs.moveSync(src, dest) + } catch (err) { + assert.strictEqual(err.message, `Cannot move '${src}' to a subdirectory of itself, '${path.join(destLink, 'dir1')}'.`) + errThrown = true + } finally { + assert(errThrown) + const srclenAfter = klawSync(src).length + assert.strictEqual(srclenAfter, srclenBefore, 'src length should not change') + const link = fs.readlinkSync(destLink) + assert.strictEqual(link, src) + } + }) }) + }) - it(`should throw error when dest is 'src/dest_src'`, () => { - dest = path.join(TEST_DIR, 'src', 'dest_src') - return testError(src, dest) + describe('> when source is a symlink', () => { + describe('>> when dest is a directory', () => { + it('should error when resolved src path points to dest', () => { + const srcLink = path.join(TEST_DIR, 'src-symlink') + fs.symlinkSync(src, srcLink, 'dir') + + const dest = path.join(TEST_DIR, 'src') + let errThrown = false + try { + fs.moveSync(srcLink, dest) + } catch (err) { + assert(err) + errThrown = true + } finally { + assert(errThrown) + // assert source not affected + const link = fs.readlinkSync(srcLink) + assert.strictEqual(link, src) + } + }) + + it('should error when dest is a subdir of resolved src path', () => { + const srcLink = path.join(TEST_DIR, 'src-symlink') + fs.symlinkSync(src, srcLink, 'dir') + + const dest = path.join(TEST_DIR, 'src', 'some', 'nested', 'dest') + fs.mkdirsSync(dest) + let errThrown = false + try { + fs.moveSync(srcLink, dest) + } catch (err) { + assert(err) + errThrown = true + } finally { + assert(errThrown) + const link = fs.readlinkSync(srcLink) + assert.strictEqual(link, src) + } + }) + + it('should error when resolved src path is a subdir of dest', () => { + const dest = path.join(TEST_DIR, 'dest') + + const resolvedSrcPath = path.join(dest, 'contains', 'src') + const srcLink = path.join(TEST_DIR, 'src-symlink') + fs.copySync(src, resolvedSrcPath) + + // make symlink that points to a subdir in dest + fs.symlinkSync(resolvedSrcPath, srcLink, 'dir') + + let errThrown = false + try { + fs.moveSync(srcLink, dest) + } catch (err) { + assert(err) + errThrown = true + } finally { + assert(errThrown) + } + }) + + it(`should move the directory successfully when dest is 'src_src/dest'`, () => { + const srcLink = path.join(TEST_DIR, 'src-symlink') + fs.symlinkSync(src, srcLink, 'dir') + + const dest = path.join(TEST_DIR, 'src_src', 'dest') + testSuccessDir(srcLink, dest) + const link = fs.readlinkSync(dest) + assert.strictEqual(link, src) + }) + + it(`should move the directory successfully when dest is 'srcsrc/dest'`, () => { + const srcLink = path.join(TEST_DIR, 'src-symlink') + fs.symlinkSync(src, srcLink, 'dir') + + const dest = path.join(TEST_DIR, 'srcsrc', 'dest') + testSuccessDir(srcLink, dest) + const link = fs.readlinkSync(dest) + assert.strictEqual(link, src) + }) }) - it(`should throw error when dest is 'src/dest/src'`, () => { - dest = path.join(TEST_DIR, 'src', 'dest', 'src') - return testError(src, dest) + describe('>> when dest is a symlink', () => { + it('should error when resolved dest path is exactly the same as resolved src path', () => { + const srcLink = path.join(TEST_DIR, 'src-symlink') + fs.symlinkSync(src, srcLink, 'dir') + const destLink = path.join(TEST_DIR, 'dest-symlink') + fs.symlinkSync(src, destLink, 'dir') + + const srclenBefore = klawSync(srcLink).length + const destlenBefore = klawSync(destLink).length + assert(srclenBefore > 2) + assert(destlenBefore > 2) + let errThrown = false + try { + fs.moveSync(srcLink, destLink) + } catch (err) { + assert.strictEqual(err.message, 'Source and destination must not be the same.') + errThrown = true + } finally { + assert(errThrown) + const srclenAfter = klawSync(srcLink).length + assert.strictEqual(srclenAfter, srclenBefore, 'src length should not change') + const destlenAfter = klawSync(destLink).length + assert.strictEqual(destlenAfter, destlenBefore, 'dest length should not change') + + const srcln = fs.readlinkSync(srcLink) + assert.strictEqual(srcln, src) + const destln = fs.readlinkSync(destLink) + assert.strictEqual(destln, src) + } + }) }) }) }) @@ -148,42 +358,43 @@ function testSuccessFile (src, destFile) { const srcFile = path.join(src, FILES[0]) fs.moveSync(srcFile, destFile) - - const o0 = fs.readFileSync(destFile, 'utf8') - assert.strictEqual(o0, dat0, 'file contents matched') + const f0 = fs.readFileSync(destFile, 'utf8') + assert.strictEqual(f0, dat0, 'file contents matched') assert(!fs.existsSync(srcFile)) } function testSuccessDir (src, dest) { const srclen = klawSync(src).length - // assert src has contents - assert(srclen > 2) - fs.moveSync(src, dest) + assert(srclen > 2) // assert src has contents + fs.moveSync(src, dest) const destlen = klawSync(dest).length - // assert src and dest length are the same assert.strictEqual(destlen, srclen, 'src and dest length should be equal') - const o0 = fs.readFileSync(path.join(dest, FILES[0]), 'utf8') - const o1 = fs.readFileSync(path.join(dest, FILES[1]), 'utf8') - const o2 = fs.readFileSync(path.join(dest, FILES[2]), 'utf8') - const o3 = fs.readFileSync(path.join(dest, FILES[3]), 'utf8') + const f0 = fs.readFileSync(path.join(dest, FILES[0]), 'utf8') + const f1 = fs.readFileSync(path.join(dest, FILES[1]), 'utf8') + const f2 = fs.readFileSync(path.join(dest, FILES[2]), 'utf8') + const f3 = fs.readFileSync(path.join(dest, FILES[3]), 'utf8') - assert.strictEqual(o0, dat0, 'file contents matched') - assert.strictEqual(o1, dat1, 'file contents matched') - assert.strictEqual(o2, dat2, 'file contents matched') - assert.strictEqual(o3, dat3, 'file contents matched') + assert.strictEqual(f0, dat0, 'file contents matched') + assert.strictEqual(f1, dat1, 'file contents matched') + assert.strictEqual(f2, dat2, 'file contents matched') + assert.strictEqual(f3, dat3, 'file contents matched') assert(!fs.existsSync(src)) } function testError (src, dest) { + let errThrown = false try { fs.moveSync(src, dest) } catch (err) { - assert.strictEqual(err.message, `Cannot move '${src}' into itself '${dest}'.`) + assert.strictEqual(err.message, `Cannot move '${src}' to a subdirectory of itself, '${dest}'.`) assert(fs.existsSync(src)) assert(!fs.existsSync(dest)) + errThrown = true + } finally { + assert(errThrown) } } diff --git a/lib/move-sync/__tests__/move-sync.test.js b/lib/move-sync/__tests__/move-sync.test.js index d2c226b3..e4b97ddb 100644 --- a/lib/move-sync/__tests__/move-sync.test.js +++ b/lib/move-sync/__tests__/move-sync.test.js @@ -1,11 +1,12 @@ 'use strict' -const fs = require('graceful-fs') +// TODO: enable this once graceful-fs supports bigint option. +// const fs = require('graceful-fs') +const fs = require('fs') const os = require('os') const fse = require(process.cwd()) const path = require('path') const assert = require('assert') -const rimraf = require('rimraf') /* global afterEach, beforeEach, describe, it */ @@ -19,16 +20,13 @@ function createSyncErrFn (errCode) { } const originalRenameSync = fs.renameSync -const originalLinkSync = fs.linkSync function setUpMockFs (errCode) { fs.renameSync = createSyncErrFn(errCode) - fs.linkSync = createSyncErrFn(errCode) } function tearDownMockFs () { fs.renameSync = originalRenameSync - fs.linkSync = originalLinkSync } describe('moveSync()', () => { @@ -36,35 +34,39 @@ describe('moveSync()', () => { beforeEach(() => { TEST_DIR = path.join(os.tmpdir(), 'fs-extra', 'move-sync') - fse.emptyDirSync(TEST_DIR) - // Create fixtures: - fs.writeFileSync(path.join(TEST_DIR, 'a-file'), 'sonic the hedgehog\n') - fs.mkdirSync(path.join(TEST_DIR, 'a-folder')) - fs.writeFileSync(path.join(TEST_DIR, 'a-folder/another-file'), 'tails\n') - fs.mkdirSync(path.join(TEST_DIR, 'a-folder/another-folder')) - fs.writeFileSync(path.join(TEST_DIR, 'a-folder/another-folder/file3'), 'knuckles\n') + // Create fixtures + fse.outputFileSync(path.join(TEST_DIR, 'a-file'), 'sonic the hedgehog\n') + fse.outputFileSync(path.join(TEST_DIR, 'a-folder/another-file'), 'tails\n') + fse.outputFileSync(path.join(TEST_DIR, 'a-folder/another-folder/file3'), 'knuckles\n') }) - afterEach(done => rimraf(TEST_DIR, done)) + afterEach(() => fse.removeSync(TEST_DIR)) it('should not move if src and dest are the same', () => { const src = `${TEST_DIR}/a-file` const dest = `${TEST_DIR}/a-file` - fse.moveSync(src, dest) + let errThrown = false + try { + fse.moveSync(src, dest) + } catch (err) { + assert.strictEqual(err.message, 'Source and destination must not be the same.') + errThrown = true + } finally { + assert(errThrown) + } // assert src not affected const contents = fs.readFileSync(src, 'utf8') const expected = /^sonic the hedgehog\r?\n$/ - assert.ok(contents.match(expected), `${contents} match ${expected}`) + assert(contents.match(expected)) }) it('should error if src and dest are the same and src does not exist', () => { const src = `${TEST_DIR}/non-existent` const dest = src - assert.throws(() => fse.moveSync(src, dest)) }) @@ -76,7 +78,7 @@ describe('moveSync()', () => { const contents = fs.readFileSync(dest, 'utf8') const expected = /^sonic the hedgehog\r?\n$/ - assert.ok(contents.match(expected), `${contents} match ${expected}`) + assert(contents.match(expected)) }) it('should not overwrite the destination by default', () => { @@ -89,7 +91,7 @@ describe('moveSync()', () => { try { fse.moveSync(src, dest) } catch (err) { - assert.ok(err && err.code === 'EEXIST', 'throw EEXIST') + assert.strictEqual(err.message, 'dest already exists.') } }) @@ -103,7 +105,7 @@ describe('moveSync()', () => { try { fse.moveSync(src, dest, {overwrite: false}) } catch (err) { - assert.ok(err && err.code === 'EEXIST', 'throw EEXIST') + assert.strictEqual(err.message, 'dest already exists.') } }) @@ -121,7 +123,7 @@ describe('moveSync()', () => { assert.ok(contents.match(expected), `${contents} match ${expected}`) }) - it('should overwrite the destination directory if overwrite = true', done => { + it('should overwrite the destination directory if overwrite = true', () => { // Create src const src = path.join(TEST_DIR, 'src') fse.ensureDirSync(src) @@ -145,7 +147,6 @@ describe('moveSync()', () => { // verify dest has new stuff assert(pathsAfter.indexOf('some-file') >= 0) assert(pathsAfter.indexOf('some-folder') >= 0) - done() }) it('should create directory structure by default', () => { @@ -159,7 +160,7 @@ describe('moveSync()', () => { const contents = fs.readFileSync(dest, 'utf8') const expected = /^sonic the hedgehog\r?\n$/ - assert.ok(contents.match(expected), `${contents} match ${expected}`) + assert(contents.match(expected)) }) it('should work across devices', () => { @@ -172,7 +173,7 @@ describe('moveSync()', () => { const contents = fs.readFileSync(dest, 'utf8') const expected = /^sonic the hedgehog\r?\n$/ - assert.ok(contents.match(expected), `${contents} match ${expected}`) + assert(contents.match(expected)) tearDownMockFs() }) @@ -187,27 +188,12 @@ describe('moveSync()', () => { const contents = fs.readFileSync(dest + '/another-file', 'utf8') const expected = /^tails\r?\n$/ - assert.ok(contents.match(expected), `${contents} match ${expected}`) - }) - - it('should move folders across devices with EISDIR error', () => { - const src = `${TEST_DIR}/a-folder` - const dest = `${TEST_DIR}/a-folder-dest` - - setUpMockFs('EISDIR') - - fse.moveSync(src, dest) - - const contents = fs.readFileSync(dest + '/another-folder/file3', 'utf8') - const expected = /^knuckles\r?\n$/ - assert.ok(contents.match(expected), `${contents} match ${expected}`) - tearDownMockFs() + assert(contents.match(expected)) }) it('should overwrite folders across devices', () => { const src = `${TEST_DIR}/a-folder` const dest = `${TEST_DIR}/a-folder-dest` - fs.mkdirSync(dest) setUpMockFs('EXDEV') @@ -216,7 +202,7 @@ describe('moveSync()', () => { const contents = fs.readFileSync(dest + '/another-folder/file3', 'utf8') const expected = /^knuckles\r?\n$/ - assert.ok(contents.match(expected), `${contents} match ${expected}`) + assert(contents.match(expected)) tearDownMockFs() }) @@ -230,35 +216,7 @@ describe('moveSync()', () => { const contents = fs.readFileSync(dest + '/another-folder/file3', 'utf8') const expected = /^knuckles\r?\n$/ - assert.ok(contents.match(expected), `${contents} match ${expected}`) - tearDownMockFs() - }) - - it('should move folders across devices with EPERM error', () => { - const src = `${TEST_DIR}/a-folder` - const dest = `${TEST_DIR}/a-folder-dest` - - setUpMockFs('EPERM') - - fse.moveSync(src, dest) - - const contents = fs.readFileSync(dest + '/another-folder/file3', 'utf8') - const expected = /^knuckles\r?\n$/ - assert.ok(contents.match(expected), `${contents} match ${expected}`) - tearDownMockFs() - }) - - it('should move folders across devices with ENOTSUP error', () => { - const src = `${TEST_DIR}/a-folder` - const dest = `${TEST_DIR}/a-folder-dest` - - setUpMockFs('ENOTSUP') - - fse.moveSync(src, dest) - - const contents = fs.readFileSync(dest + '/another-folder/file3', 'utf8') - const expected = /^knuckles\r?\n$/ - assert.ok(contents.match(expected), `${contents} match ${expected}`) + assert(contents.match(expected)) tearDownMockFs() }) @@ -274,7 +232,7 @@ describe('moveSync()', () => { const contents = fs.readFileSync(dest, 'utf8') const expected = /^sonic the hedgehog\r?\n$/ - assert.ok(contents.match(expected), `${contents} match ${expected}`) + assert(contents.match(expected)) }) }) @@ -306,7 +264,7 @@ describe('moveSync()', () => { const contents = fs.readFileSync(dest, 'utf8') const expected = /^sonic the hedgehog\r?\n$/ - assert.ok(contents.match(expected), `${contents} match ${expected}`) + assert(contents.match(expected)) }) }) @@ -324,7 +282,7 @@ describe('moveSync()', () => { try { fs.writeFileSync(path.join(differentDevice, 'file'), 'hi') } catch (err) { - console.log("Can't write to device. Skipping moveSync test.") + console.log(`Can't write to device. Skipping moveSync test.`) __skipTests = true } @@ -335,12 +293,8 @@ describe('moveSync()', () => { const src = '/mnt/some/weird/dir-really-weird' const dest = path.join(TEST_DIR, 'device-weird') - if (!fs.existsSync(src)) { - fse.mkdirpSync(src) - } - + if (!fs.existsSync(src)) fse.mkdirpSync(src) assert(!fs.existsSync(dest)) - assert(fs.lstatSync(src).isDirectory()) fse.moveSync(src, dest) diff --git a/lib/move-sync/index.js b/lib/move-sync/index.js index 6d4f56fa..af90b06b 100644 --- a/lib/move-sync/index.js +++ b/lib/move-sync/index.js @@ -1,117 +1,5 @@ 'use strict' -const fs = require('graceful-fs') -const path = require('path') -const copySync = require('../copy-sync').copySync -const removeSync = require('../remove').removeSync -const mkdirpSync = require('../mkdirs').mkdirsSync -const buffer = require('../util/buffer') - -function moveSync (src, dest, options) { - options = options || {} - const overwrite = options.overwrite || options.clobber || false - - src = path.resolve(src) - dest = path.resolve(dest) - - if (src === dest) return fs.accessSync(src) - - if (isSrcSubdir(src, dest)) throw new Error(`Cannot move '${src}' into itself '${dest}'.`) - - mkdirpSync(path.dirname(dest)) - tryRenameSync() - - function tryRenameSync () { - if (overwrite) { - try { - return fs.renameSync(src, dest) - } catch (err) { - if (err.code === 'ENOTEMPTY' || err.code === 'EEXIST' || err.code === 'EPERM') { - removeSync(dest) - options.overwrite = false // just overwriteed it, no need to do it again - return moveSync(src, dest, options) - } - - if (err.code !== 'EXDEV') throw err - return moveSyncAcrossDevice(src, dest, overwrite) - } - } else { - try { - fs.linkSync(src, dest) - return fs.unlinkSync(src) - } catch (err) { - if (err.code === 'EXDEV' || err.code === 'EISDIR' || err.code === 'EPERM' || err.code === 'ENOTSUP') { - return moveSyncAcrossDevice(src, dest, overwrite) - } - throw err - } - } - } -} - -function moveSyncAcrossDevice (src, dest, overwrite) { - const stat = fs.statSync(src) - - if (stat.isDirectory()) { - return moveDirSyncAcrossDevice(src, dest, overwrite) - } else { - return moveFileSyncAcrossDevice(src, dest, overwrite) - } -} - -function moveFileSyncAcrossDevice (src, dest, overwrite) { - const BUF_LENGTH = 64 * 1024 - const _buff = buffer(BUF_LENGTH) - - const flags = overwrite ? 'w' : 'wx' - - const fdr = fs.openSync(src, 'r') - const stat = fs.fstatSync(fdr) - const fdw = fs.openSync(dest, flags, stat.mode) - let pos = 0 - - while (pos < stat.size) { - const bytesRead = fs.readSync(fdr, _buff, 0, BUF_LENGTH, pos) - fs.writeSync(fdw, _buff, 0, bytesRead) - pos += bytesRead - } - - fs.closeSync(fdr) - fs.closeSync(fdw) - return fs.unlinkSync(src) -} - -function moveDirSyncAcrossDevice (src, dest, overwrite) { - const options = { - overwrite: false - } - - if (overwrite) { - removeSync(dest) - tryCopySync() - } else { - tryCopySync() - } - - function tryCopySync () { - copySync(src, dest, options) - return removeSync(src) - } -} - -// return true if dest is a subdir of src, otherwise false. -// extract dest base dir and check if that is the same as src basename -function isSrcSubdir (src, dest) { - try { - return fs.statSync(src).isDirectory() && - src !== dest && - dest.indexOf(src) > -1 && - dest.split(path.dirname(src) + path.sep)[1].split(path.sep)[0] === path.basename(src) - } catch (e) { - return false - } -} - module.exports = { - moveSync + moveSync: require('./move-sync') } diff --git a/lib/move-sync/move-sync.js b/lib/move-sync/move-sync.js new file mode 100644 index 00000000..62f8e1a6 --- /dev/null +++ b/lib/move-sync/move-sync.js @@ -0,0 +1,49 @@ +'use strict' + +// TODO: enable this once graceful-fs supports bigint option. +// const fs = require('graceful-fs') +const fs = require('fs') +const path = require('path') +const copySync = require('../copy-sync').copySync +const removeSync = require('../remove').removeSync +const mkdirpSync = require('../mkdirs').mkdirpSync +const stat = require('../util/stat') + +function moveSync (src, dest, opts) { + opts = opts || {} + const overwrite = opts.overwrite || opts.clobber || false + + const { srcStat } = stat.checkPathsSync(src, dest, 'move') + stat.checkParentPathsSync(src, srcStat, dest, 'move') + mkdirpSync(path.dirname(dest)) + return doRename(src, dest, overwrite) +} + +function doRename (src, dest, overwrite) { + if (overwrite) { + removeSync(dest) + return rename(src, dest, overwrite) + } + if (fs.existsSync(dest)) throw new Error('dest already exists.') + return rename(src, dest, overwrite) +} + +function rename (src, dest, overwrite) { + try { + fs.renameSync(src, dest) + } catch (err) { + if (err.code !== 'EXDEV') throw err + return moveAcrossDevice(src, dest, overwrite) + } +} + +function moveAcrossDevice (src, dest, overwrite) { + const opts = { + overwrite, + errorOnExist: true + } + copySync(src, dest, opts) + return removeSync(src) +} + +module.exports = moveSync diff --git a/lib/move/__tests__/move-case-insensitive-paths.test.js b/lib/move/__tests__/move-case-insensitive-paths.test.js new file mode 100644 index 00000000..30763249 --- /dev/null +++ b/lib/move/__tests__/move-case-insensitive-paths.test.js @@ -0,0 +1,108 @@ +'use strict' + +const assert = require('assert') +const os = require('os') +const path = require('path') +const fs = require('../../') +const platform = os.platform() + +/* global beforeEach, afterEach, describe, it */ + +describe('+ move() - case insensitive paths', () => { + let TEST_DIR = '' + let src = '' + let dest = '' + + beforeEach(done => { + TEST_DIR = path.join(os.tmpdir(), 'fs-extra', 'move-case-insensitive-paths') + fs.emptyDir(TEST_DIR, done) + }) + + afterEach(done => fs.remove(TEST_DIR, done)) + + describe('> when src is a directory', () => { + it('should behave correctly based on the OS', done => { + src = path.join(TEST_DIR, 'srcdir') + fs.outputFileSync(path.join(src, 'subdir', 'file.txt'), 'some data') + dest = path.join(TEST_DIR, 'srcDir') + + fs.move(src, dest, err => { + if (platform === 'linux') { + assert.ifError(err) + assert(fs.existsSync(dest)) + assert.strictEqual(fs.readFileSync(path.join(dest, 'subdir', 'file.txt'), 'utf8'), 'some data') + } + if (platform === 'darwin' || platform === 'win32') { + assert.strictEqual(err.message, 'Source and destination must not be the same.') + } + done() + }) + }) + }) + + describe('> when src is a file', () => { + it('should behave correctly based on the OS', done => { + src = path.join(TEST_DIR, 'srcfile') + fs.outputFileSync(src, 'some data') + dest = path.join(TEST_DIR, 'srcFile') + + fs.move(src, dest, err => { + if (platform === 'linux') { + assert.ifError(err) + assert(fs.existsSync(dest)) + assert.strictEqual(fs.readFileSync(dest, 'utf8'), 'some data') + } + if (platform === 'darwin' || platform === 'win32') { + assert.strictEqual(err.message, 'Source and destination must not be the same.') + } + done() + }) + }) + }) + + describe('> when src is a symlink', () => { + it('should behave correctly based on the OS, symlink dir', done => { + src = path.join(TEST_DIR, 'srcdir') + fs.outputFileSync(path.join(src, 'subdir', 'file.txt'), 'some data') + const srcLink = path.join(TEST_DIR, 'src-symlink') + fs.symlinkSync(src, srcLink, 'dir') + dest = path.join(TEST_DIR, 'src-Symlink') + + fs.move(srcLink, dest, err => { + if (platform === 'linux') { + assert.ifError(err) + assert(fs.existsSync(dest)) + assert.strictEqual(fs.readFileSync(path.join(dest, 'subdir', 'file.txt'), 'utf8'), 'some data') + const destLink = fs.readlinkSync(dest) + assert.strictEqual(destLink, src) + } + if (platform === 'darwin' || platform === 'win32') { + assert.strictEqual(err.message, 'Source and destination must not be the same.') + } + done() + }) + }) + + it('should behave correctly based on the OS, symlink file', done => { + src = path.join(TEST_DIR, 'srcfile') + fs.outputFileSync(src, 'some data') + const srcLink = path.join(TEST_DIR, 'src-symlink') + fs.symlinkSync(src, srcLink, 'file') + dest = path.join(TEST_DIR, 'src-Symlink') + + fs.move(srcLink, dest, err => { + if (platform === 'linux') { + assert.ifError(err) + assert(fs.existsSync(dest)) + assert.strictEqual(fs.readFileSync(dest, 'utf8'), 'some data') + const destLink = fs.readlinkSync(dest) + assert.strictEqual(destLink, src) + } + if (platform === 'darwin' || platform === 'win32') { + assert.strictEqual(err.message, 'Source and destination must not be the same.') + } + done() + }) + }) + }) +}) diff --git a/lib/move/__tests__/move-prevent-moving-identical.test.js b/lib/move/__tests__/move-prevent-moving-identical.test.js new file mode 100644 index 00000000..096a9d45 --- /dev/null +++ b/lib/move/__tests__/move-prevent-moving-identical.test.js @@ -0,0 +1,252 @@ +'use strict' + +const assert = require('assert') +const os = require('os') +const path = require('path') +const fs = require('../../') +const klawSync = require('klaw-sync') + +/* global beforeEach, afterEach, describe, it */ + +describe('+ move() - prevent moving identical files and dirs', () => { + let TEST_DIR = '' + let src = '' + let dest = '' + + beforeEach(done => { + TEST_DIR = path.join(os.tmpdir(), 'fs-extra', 'move-prevent-moving-identical') + fs.emptyDir(TEST_DIR, done) + }) + + afterEach(done => fs.remove(TEST_DIR, done)) + + it('should return an error if src and dest are the same', done => { + const fileSrc = path.join(TEST_DIR, 'TEST_fs-extra_move') + const fileDest = path.join(TEST_DIR, 'TEST_fs-extra_move') + fs.ensureFileSync(fileSrc) + + fs.move(fileSrc, fileDest, err => { + assert.strictEqual(err.message, 'Source and destination must not be the same.') + done() + }) + }) + + describe('dest with parent symlink', () => { + describe('first parent is symlink', () => { + it('should error when src is file', done => { + const src = path.join(TEST_DIR, 'a', 'file.txt') + const dest = path.join(TEST_DIR, 'b', 'file.txt') + const srcParent = path.join(TEST_DIR, 'a') + const destParent = path.join(TEST_DIR, 'b') + fs.ensureFileSync(src) + fs.ensureSymlinkSync(srcParent, destParent, 'dir') + + fs.move(src, dest, err => { + assert.strictEqual(err.message, 'Source and destination must not be the same.') + assert(fs.existsSync(src)) + done() + }) + }) + + it('should error when src is directory', done => { + const src = path.join(TEST_DIR, 'a', 'foo') + const dest = path.join(TEST_DIR, 'b', 'foo') + const srcParent = path.join(TEST_DIR, 'a') + const destParent = path.join(TEST_DIR, 'b') + fs.ensureDirSync(src) + fs.ensureSymlinkSync(srcParent, destParent, 'dir') + + fs.move(src, dest, err => { + assert.strictEqual(err.message, 'Source and destination must not be the same.') + assert(fs.existsSync(src)) + done() + }) + }) + }) + + describe('nested dest', () => { + it('should error when src is file', done => { + const src = path.join(TEST_DIR, 'a', 'dir', 'file.txt') + const dest = path.join(TEST_DIR, 'b', 'dir', 'file.txt') + const srcParent = path.join(TEST_DIR, 'a') + const destParent = path.join(TEST_DIR, 'b') + fs.ensureFileSync(src) + fs.ensureSymlinkSync(srcParent, destParent, 'dir') + + fs.move(src, dest, err => { + assert.strictEqual(err.message, 'Source and destination must not be the same.') + assert(fs.existsSync(src)) + done() + }) + }) + + it('should error when src is directory', done => { + const src = path.join(TEST_DIR, 'a', 'dir', 'foo') + const dest = path.join(TEST_DIR, 'b', 'dir', 'foo') + const srcParent = path.join(TEST_DIR, 'a') + const destParent = path.join(TEST_DIR, 'b') + fs.ensureDirSync(src) + fs.ensureSymlinkSync(srcParent, destParent, 'dir') + + fs.move(src, dest, err => { + assert.strictEqual(err.message, 'Source and destination must not be the same.') + assert(fs.existsSync(src)) + done() + }) + }) + }) + }) + + // src is directory: + // src is regular, dest is symlink + // src is symlink, dest is regular + // src is symlink, dest is symlink + + describe('> when src is a directory', () => { + describe(`>> when src is regular and dest is a symlink that points to src`, () => { + it('should error', done => { + src = path.join(TEST_DIR, 'src') + fs.mkdirsSync(src) + const subdir = path.join(TEST_DIR, 'src', 'subdir') + fs.mkdirsSync(subdir) + fs.writeFileSync(path.join(subdir, 'file.txt'), 'some data') + + const destLink = path.join(TEST_DIR, 'dest-symlink') + fs.symlinkSync(src, destLink, 'dir') + + const oldlen = klawSync(src).length + + fs.move(src, destLink, err => { + assert.strictEqual(err.message, 'Source and destination must not be the same.') + + const newlen = klawSync(src).length + assert.strictEqual(newlen, oldlen) + const link = fs.readlinkSync(destLink) + assert.strictEqual(link, src) + done() + }) + }) + }) + + describe(`>> when src is a symlink that points to a regular dest`, () => { + it('should throw error', done => { + dest = path.join(TEST_DIR, 'dest') + fs.mkdirsSync(dest) + const subdir = path.join(TEST_DIR, 'dest', 'subdir') + fs.mkdirsSync(subdir) + fs.writeFileSync(path.join(subdir, 'file.txt'), 'some data') + + const srcLink = path.join(TEST_DIR, 'src-symlink') + fs.symlinkSync(dest, srcLink, 'dir') + + const oldlen = klawSync(dest).length + + fs.move(srcLink, dest, err => { + assert.ok(err) + + // assert nothing copied + const newlen = klawSync(dest).length + assert.strictEqual(newlen, oldlen) + const link = fs.readlinkSync(srcLink) + assert.strictEqual(link, dest) + done() + }) + }) + }) + + describe('>> when src and dest are symlinks that point to the exact same path', () => { + it('should error src and dest are the same', done => { + src = path.join(TEST_DIR, 'src') + fs.mkdirsSync(src) + const srcLink = path.join(TEST_DIR, 'src_symlink') + fs.symlinkSync(src, srcLink, 'dir') + const destLink = path.join(TEST_DIR, 'dest_symlink') + fs.symlinkSync(src, destLink, 'dir') + + const srclenBefore = klawSync(srcLink).length + const destlenBefore = klawSync(destLink).length + + fs.move(srcLink, destLink, err => { + assert.strictEqual(err.message, 'Source and destination must not be the same.') + + const srclenAfter = klawSync(srcLink).length + assert.strictEqual(srclenAfter, srclenBefore, 'src length should not change') + const destlenAfter = klawSync(destLink).length + assert.strictEqual(destlenAfter, destlenBefore, 'dest length should not change') + + const srcln = fs.readlinkSync(srcLink) + assert.strictEqual(srcln, src) + const destln = fs.readlinkSync(destLink) + assert.strictEqual(destln, src) + done() + }) + }) + }) + }) + + // src is file: + // src is regular, dest is symlink + // src is symlink, dest is regular + // src is symlink, dest is symlink + + describe('> when src is a file', () => { + describe(`>> when src is regular and dest is a symlink that points to src`, () => { + it('should error', done => { + src = path.join(TEST_DIR, 'src.txt') + fs.outputFileSync(src, 'some data') + + const destLink = path.join(TEST_DIR, 'dest-symlink') + fs.symlinkSync(src, destLink, 'file') + + fs.move(src, destLink, err => { + assert.strictEqual(err.message, 'Source and destination must not be the same.') + done() + }) + }) + }) + + describe(`>> when src is a symlink that points to a regular dest`, () => { + it('should throw error', done => { + dest = path.join(TEST_DIR, 'dest', 'somefile.txt') + fs.outputFileSync(dest, 'some data') + + const srcLink = path.join(TEST_DIR, 'src-symlink') + fs.symlinkSync(dest, srcLink, 'file') + + fs.move(srcLink, dest, err => { + assert.strictEqual(err.message, 'Source and destination must not be the same.') + + const link = fs.readlinkSync(srcLink) + assert.strictEqual(link, dest) + assert(fs.readFileSync(link, 'utf8'), 'some data') + done() + }) + }) + }) + + describe('>> when src and dest are symlinks that point to the exact same path', () => { + it('should error src and dest are the same', done => { + src = path.join(TEST_DIR, 'src', 'srcfile.txt') + fs.outputFileSync(src, 'src data') + + const srcLink = path.join(TEST_DIR, 'src_symlink') + fs.symlinkSync(src, srcLink, 'file') + + const destLink = path.join(TEST_DIR, 'dest_symlink') + fs.symlinkSync(src, destLink, 'file') + + fs.move(srcLink, destLink, err => { + assert.strictEqual(err.message, 'Source and destination must not be the same.') + + const srcln = fs.readlinkSync(srcLink) + assert.strictEqual(srcln, src) + const destln = fs.readlinkSync(destLink) + assert.strictEqual(destln, src) + assert(fs.readFileSync(srcln, 'utf8'), 'src data') + assert(fs.readFileSync(destln, 'utf8'), 'src data') + done() + }) + }) + }) + }) +}) diff --git a/lib/move/__tests__/move-prevent-moving-into-itself.test.js b/lib/move/__tests__/move-prevent-moving-into-itself.test.js index 4b98fc6f..fe13d095 100644 --- a/lib/move/__tests__/move-prevent-moving-into-itself.test.js +++ b/lib/move/__tests__/move-prevent-moving-into-itself.test.js @@ -3,11 +3,12 @@ const assert = require('assert') const os = require('os') const path = require('path') -const fs = require(process.cwd()) +const fs = require('../../') const klawSync = require('klaw-sync') /* global beforeEach, afterEach, describe, it */ +// these files are used for all tests const FILES = [ 'file0.txt', path.join('dir1', 'file1.txt'), @@ -21,7 +22,7 @@ const dat2 = 'file2' const dat3 = 'file3' describe('+ move() - prevent moving into itself', () => { - let TEST_DIR, src, dest + let TEST_DIR, src beforeEach(() => { TEST_DIR = path.join(os.tmpdir(), 'fs-extra', 'move-prevent-moving-into-itself') @@ -36,6 +37,23 @@ describe('+ move() - prevent moving into itself', () => { afterEach(() => fs.removeSync(TEST_DIR)) + describe('> when source is a file', () => { + it('should move the file successfully even if dest parent is a subdir of src', done => { + const srcFile = path.join(TEST_DIR, 'src', 'srcfile.txt') + const destFile = path.join(TEST_DIR, 'src', 'dest', 'destfile.txt') + fs.writeFileSync(srcFile, dat0) + + fs.move(srcFile, destFile, err => { + assert.ifError(err) + + assert(fs.existsSync(destFile)) + const out = fs.readFileSync(destFile, 'utf8') + assert.strictEqual(out, dat0, 'file contents matched') + done() + }) + }) + }) + describe('> when source is a file', () => { it(`should move the file successfully even when dest parent is 'src/dest'`, done => { const destFile = path.join(TEST_DIR, 'src', 'dest', 'destfile.txt') @@ -64,82 +82,256 @@ describe('+ move() - prevent moving into itself', () => { }) describe('> when source is a directory', () => { - it(`should move the directory successfully when dest is 'src_dest'`, done => { - dest = path.join(TEST_DIR, 'src_dest') - return testSuccessDir(src, dest, done) + describe('>> when dest is a directory', () => { + it(`of not itself`, done => { + const dest = path.join(TEST_DIR, src.replace(/^\w:/, '')) + return testSuccessDir(src, dest, done) + }) + it(`of itself`, done => { + const dest = path.join(src, 'dest') + return testError(src, dest, done) + }) + it(`should move the directory successfully when dest is 'src_dest'`, done => { + const dest = path.join(TEST_DIR, 'src_dest') + return testSuccessDir(src, dest, done) + }) + it(`should move the directory successfully when dest is 'src-dest'`, done => { + const dest = path.join(TEST_DIR, 'src-dest') + return testSuccessDir(src, dest, done) + }) + + it(`should move the directory successfully when dest is 'dest_src'`, done => { + const dest = path.join(TEST_DIR, 'dest_src') + return testSuccessDir(src, dest, done) + }) + + it(`should move the directory successfully when dest is 'src_dest/src'`, done => { + const dest = path.join(TEST_DIR, 'src_dest', 'src') + return testSuccessDir(src, dest, done) + }) + + it(`should move the directory successfully when dest is 'src-dest/src'`, done => { + const dest = path.join(TEST_DIR, 'src-dest', 'src') + return testSuccessDir(src, dest, done) + }) + + it(`should move the directory successfully when dest is 'dest_src/src'`, done => { + const dest = path.join(TEST_DIR, 'dest_src', 'src') + return testSuccessDir(src, dest, done) + }) + + it(`should move the directory successfully when dest is 'src_src/dest'`, done => { + const dest = path.join(TEST_DIR, 'src_src', 'dest') + return testSuccessDir(src, dest, done) + }) + + it(`should move the directory successfully when dest is 'src-src/dest'`, done => { + const dest = path.join(TEST_DIR, 'src-src', 'dest') + return testSuccessDir(src, dest, done) + }) + + it(`should move the directory successfully when dest is 'srcsrc/dest'`, done => { + const dest = path.join(TEST_DIR, 'srcsrc', 'dest') + return testSuccessDir(src, dest, done) + }) + + it(`should move the directory successfully when dest is 'dest/src'`, done => { + const dest = path.join(TEST_DIR, 'dest', 'src') + return testSuccessDir(src, dest, done) + }) + + it('should move the directory successfully when dest is very nested that all its parents need to be created', done => { + const dest = path.join(TEST_DIR, 'dest', 'src', 'foo', 'bar', 'baz', 'qux', 'quux', 'waldo', + 'grault', 'garply', 'fred', 'plugh', 'thud', 'some', 'highly', 'deeply', + 'badly', 'nasty', 'crazy', 'mad', 'nested', 'dest') + return testSuccessDir(src, dest, done) + }) + + it(`should error when dest is 'src/dest'`, done => { + const dest = path.join(TEST_DIR, 'src', 'dest') + return testError(src, dest, done) + }) + + it(`should error when dest is 'src/src_dest'`, done => { + const dest = path.join(TEST_DIR, 'src', 'src_dest') + return testError(src, dest, done) + }) + + it(`should error when dest is 'src/dest_src'`, done => { + const dest = path.join(TEST_DIR, 'src', 'dest_src') + return testError(src, dest, done) + }) + + it(`should error when dest is 'src/dest/src'`, done => { + const dest = path.join(TEST_DIR, 'src', 'dest', 'src') + return testError(src, dest, done) + }) }) - it(`should move the directory successfully when dest is 'src-dest'`, done => { - dest = path.join(TEST_DIR, 'src-dest') - return testSuccessDir(src, dest, done) - }) + describe('>> when dest is a symlink', () => { + it('should error when dest points exactly to src', done => { + const destLink = path.join(TEST_DIR, 'dest-symlink') + fs.symlinkSync(src, destLink, 'dir') - it(`should move the directory successfully when dest is 'dest_src'`, done => { - dest = path.join(TEST_DIR, 'dest_src') - return testSuccessDir(src, dest, done) - }) + const srclenBefore = klawSync(src).length + assert(srclenBefore > 2) - it(`should move the directory successfully when dest is 'src_dest/src'`, done => { - dest = path.join(TEST_DIR, 'src_dest', 'src') - return testSuccessDir(src, dest, done) - }) + fs.move(src, destLink, err => { + assert.strictEqual(err.message, 'Source and destination must not be the same.') - it(`should move the directory successfully when dest is 'src-dest/src'`, done => { - dest = path.join(TEST_DIR, 'src-dest', 'src') - return testSuccessDir(src, dest, done) - }) + const srclenAfter = klawSync(src).length + assert.strictEqual(srclenAfter, srclenBefore, 'src length should not change') - it(`should move the directory successfully when dest is 'dest_src/src'`, done => { - dest = path.join(TEST_DIR, 'dest_src', 'src') - return testSuccessDir(src, dest, done) - }) + const link = fs.readlinkSync(destLink) + assert.strictEqual(link, src) + done() + }) + }) - it(`should move the directory successfully when dest is 'src_src/dest'`, done => { - dest = path.join(TEST_DIR, 'src_src', 'dest') - return testSuccessDir(src, dest, done) - }) + it('should error when dest is a subdirectory of src (bind-mounted directory with subdirectory)', done => { + const destLink = path.join(TEST_DIR, 'dest-symlink') + fs.symlinkSync(src, destLink, 'dir') - it(`should move the directory successfully when dest is 'src-src/dest'`, done => { - dest = path.join(TEST_DIR, 'src-src', 'dest') - return testSuccessDir(src, dest, done) - }) + const srclenBefore = klawSync(src).length + assert(srclenBefore > 2) - it(`should move the directory successfully when dest is 'srcsrc/dest'`, done => { - dest = path.join(TEST_DIR, 'srcsrc', 'dest') - return testSuccessDir(src, dest, done) - }) + const dest = path.join(destLink, 'dir1') + assert(fs.existsSync(dest)) + fs.move(src, dest, err => { + assert.strictEqual(err.message, `Cannot move '${src}' to a subdirectory of itself, '${dest}'.`) - it(`should move the directory successfully when dest is 'dest/src'`, done => { - dest = path.join(TEST_DIR, 'dest', 'src') - return testSuccessDir(src, dest, done) - }) + const srclenAfter = klawSync(src).length + assert.strictEqual(srclenAfter, srclenBefore, 'src length should not change') - it('should move the directory successfully when dest is very nested that all its parents need to be created', done => { - dest = path.join(TEST_DIR, 'dest', 'src', 'foo', 'bar', 'baz', 'qux', 'quux', 'waldo', - 'grault', 'garply', 'fred', 'plugh', 'thud', 'some', 'highly', 'deeply', - 'badly', 'nasty', 'crazy', 'mad', 'nested', 'dest') - assert(!fs.existsSync(dest)) - return testSuccessDir(src, dest, done) - }) + const link = fs.readlinkSync(destLink) + assert.strictEqual(link, src) + done() + }) + }) - it(`should return error when dest is 'src/dest'`, done => { - dest = path.join(TEST_DIR, 'src', 'dest') - return testError(src, dest, done) - }) + it('should error when dest is a subdirectory of src (more than one level depth)', done => { + const destLink = path.join(TEST_DIR, 'dest-symlink') + fs.symlinkSync(src, destLink, 'dir') + + const srclenBefore = klawSync(src).length + assert(srclenBefore > 2) + + const dest = path.join(destLink, 'dir1', 'dir2') + assert(fs.existsSync(dest)) + fs.move(src, dest, err => { + assert.strictEqual(err.message, `Cannot move '${src}' to a subdirectory of itself, '${path.join(destLink, 'dir1')}'.`) - it(`should return error when dest is 'src/src_dest'`, done => { - dest = path.join(TEST_DIR, 'src', 'src_dest') - return testError(src, dest, done) + const srclenAfter = klawSync(src).length + assert.strictEqual(srclenAfter, srclenBefore, 'src length should not change') + + const link = fs.readlinkSync(destLink) + assert.strictEqual(link, src) + done() + }) + }) }) + }) - it(`should return error when dest is 'src/dest_src'`, done => { - dest = path.join(TEST_DIR, 'src', 'dest_src') - return testError(src, dest, done) + describe('> when source is a symlink', () => { + describe('>> when dest is a directory', () => { + it('should error when resolved src path points to dest', done => { + const srcLink = path.join(TEST_DIR, 'src-symlink') + fs.symlinkSync(src, srcLink, 'dir') + + const dest = path.join(TEST_DIR, 'src') + + fs.move(srcLink, dest, err => { + assert(err) + // assert source not affected + const link = fs.readlinkSync(srcLink) + assert.strictEqual(link, src) + done() + }) + }) + + it('should error when dest is a subdir of resolved src path', done => { + const srcLink = path.join(TEST_DIR, 'src-symlink') + fs.symlinkSync(src, srcLink, 'dir') + + const dest = path.join(TEST_DIR, 'src', 'some', 'nested', 'dest') + fs.mkdirsSync(dest) + + fs.move(srcLink, dest, err => { + assert(err) + const link = fs.readlinkSync(srcLink) + assert.strictEqual(link, src) + done() + }) + }) + + it('should error when resolved src path is a subdir of dest', done => { + const dest = path.join(TEST_DIR, 'dest') + + const resolvedSrcPath = path.join(dest, 'contains', 'src') + const srcLink = path.join(TEST_DIR, 'src-symlink') + fs.copySync(src, resolvedSrcPath) + + // make symlink that points to a subdir in dest + fs.symlinkSync(resolvedSrcPath, srcLink, 'dir') + + fs.move(srcLink, dest, err => { + assert(err) + done() + }) + }) + + it(`should move the directory successfully when dest is 'src_src/dest'`, done => { + const srcLink = path.join(TEST_DIR, 'src-symlink') + fs.symlinkSync(src, srcLink, 'dir') + + const dest = path.join(TEST_DIR, 'src_src', 'dest') + testSuccessDir(srcLink, dest, () => { + const link = fs.readlinkSync(dest) + assert.strictEqual(link, src) + done() + }) + }) + + it(`should move the directory successfully when dest is 'srcsrc/dest'`, done => { + const srcLink = path.join(TEST_DIR, 'src-symlink') + fs.symlinkSync(src, srcLink, 'dir') + + const dest = path.join(TEST_DIR, 'srcsrc', 'dest') + testSuccessDir(srcLink, dest, () => { + const link = fs.readlinkSync(dest) + assert.strictEqual(link, src) + done() + }) + }) }) - it(`should return error when dest is 'src/dest/src'`, done => { - dest = path.join(TEST_DIR, 'src', 'dest', 'src') - return testError(src, dest, done) + describe('>> when dest is a symlink', () => { + it('should error when resolved dest path is exactly the same as resolved src path', done => { + const srcLink = path.join(TEST_DIR, 'src-symlink') + fs.symlinkSync(src, srcLink, 'dir') + const destLink = path.join(TEST_DIR, 'dest-symlink') + fs.symlinkSync(src, destLink, 'dir') + + const srclenBefore = klawSync(srcLink).length + const destlenBefore = klawSync(destLink).length + assert(srclenBefore > 2) + assert(destlenBefore > 2) + + fs.move(srcLink, destLink, err => { + assert.strictEqual(err.message, 'Source and destination must not be the same.') + + const srclenAfter = klawSync(srcLink).length + assert.strictEqual(srclenAfter, srclenBefore, 'src length should not change') + const destlenAfter = klawSync(destLink).length + assert.strictEqual(destlenAfter, destlenBefore, 'dest length should not change') + + const srcln = fs.readlinkSync(srcLink) + assert.strictEqual(srcln, src) + const destln = fs.readlinkSync(destLink) + assert.strictEqual(destln, src) + done() + }) + }) }) }) }) diff --git a/lib/move/__tests__/move.test.js b/lib/move/__tests__/move.test.js index ef37efc3..7ee2ac02 100644 --- a/lib/move/__tests__/move.test.js +++ b/lib/move/__tests__/move.test.js @@ -1,8 +1,10 @@ 'use strict' -const fs = require('graceful-fs') +// TODO: enable this once graceful-fs supports bigint option. +// const fs = require('graceful-fs') +const fs = require('fs') const os = require('os') -const fse = require(process.cwd()) +const fse = require('../../') const path = require('path') const assert = require('assert') @@ -143,7 +145,7 @@ describe('+ move()', () => { const dest = src fse.move(src, dest, err => { - assert.ifError(err) + assert.strictEqual(err.message, 'Source and destination must not be the same.') done() }) }) @@ -163,7 +165,7 @@ describe('+ move()', () => { const dest = src fse.move(src, dest, err => { - assert.ifError(err) + assert.strictEqual(err.message, 'Source and destination must not be the same.') done() }) }) diff --git a/lib/move/index.js b/lib/move/index.js index 68947f05..3785345b 100644 --- a/lib/move/index.js +++ b/lib/move/index.js @@ -1,82 +1,6 @@ 'use strict' const u = require('universalify').fromCallback -const fs = require('graceful-fs') -const path = require('path') -const copy = require('../copy').copy -const remove = require('../remove').remove -const mkdirp = require('../mkdirs').mkdirp -const pathExists = require('../path-exists').pathExists - -function move (src, dest, opts, cb) { - if (typeof opts === 'function') { - cb = opts - opts = {} - } - - const overwrite = opts.overwrite || opts.clobber || false - - src = path.resolve(src) - dest = path.resolve(dest) - - if (src === dest) return fs.access(src, cb) - - fs.stat(src, (err, st) => { - if (err) return cb(err) - - if (st.isDirectory() && isSrcSubdir(src, dest)) { - return cb(new Error(`Cannot move '${src}' to a subdirectory of itself, '${dest}'.`)) - } - mkdirp(path.dirname(dest), err => { - if (err) return cb(err) - return doRename(src, dest, overwrite, cb) - }) - }) -} - -function doRename (src, dest, overwrite, cb) { - if (overwrite) { - return remove(dest, err => { - if (err) return cb(err) - return rename(src, dest, overwrite, cb) - }) - } - pathExists(dest, (err, destExists) => { - if (err) return cb(err) - if (destExists) return cb(new Error('dest already exists.')) - return rename(src, dest, overwrite, cb) - }) -} - -function rename (src, dest, overwrite, cb) { - fs.rename(src, dest, err => { - if (!err) return cb() - if (err.code !== 'EXDEV') return cb(err) - return moveAcrossDevice(src, dest, overwrite, cb) - }) -} - -function moveAcrossDevice (src, dest, overwrite, cb) { - const opts = { - overwrite, - errorOnExist: true - } - - copy(src, dest, opts, err => { - if (err) return cb(err) - return remove(src, cb) - }) -} - -function isSrcSubdir (src, dest) { - const srcArray = src.split(path.sep) - const destArray = dest.split(path.sep) - - return srcArray.reduce((acc, current, i) => { - return acc && destArray[i] === current - }, true) -} - module.exports = { - move: u(move) + move: u(require('./move')) } diff --git a/lib/move/move.js b/lib/move/move.js new file mode 100644 index 00000000..94dc7949 --- /dev/null +++ b/lib/move/move.js @@ -0,0 +1,67 @@ +'use strict' + +// TODO: enable this once graceful-fs supports bigint option. +// const fs = require('graceful-fs') +const fs = require('fs') +const path = require('path') +const copy = require('../copy').copy +const remove = require('../remove').remove +const mkdirp = require('../mkdirs').mkdirp +const pathExists = require('../path-exists').pathExists +const stat = require('../util/stat') + +function move (src, dest, opts, cb) { + if (typeof opts === 'function') { + cb = opts + opts = {} + } + + const overwrite = opts.overwrite || opts.clobber || false + + stat.checkPaths(src, dest, 'move', (err, stats) => { + if (err) return cb(err) + const { srcStat } = stats + stat.checkParentPaths(src, srcStat, dest, 'move', err => { + if (err) return cb(err) + mkdirp(path.dirname(dest), err => { + if (err) return cb(err) + return doRename(src, dest, overwrite, cb) + }) + }) + }) +} + +function doRename (src, dest, overwrite, cb) { + if (overwrite) { + return remove(dest, err => { + if (err) return cb(err) + return rename(src, dest, overwrite, cb) + }) + } + pathExists(dest, (err, destExists) => { + if (err) return cb(err) + if (destExists) return cb(new Error('dest already exists.')) + return rename(src, dest, overwrite, cb) + }) +} + +function rename (src, dest, overwrite, cb) { + fs.rename(src, dest, err => { + if (!err) return cb() + if (err.code !== 'EXDEV') return cb(err) + return moveAcrossDevice(src, dest, overwrite, cb) + }) +} + +function moveAcrossDevice (src, dest, overwrite, cb) { + const opts = { + overwrite, + errorOnExist: true + } + copy(src, dest, opts, err => { + if (err) return cb(err) + return remove(src, cb) + }) +} + +module.exports = move diff --git a/lib/remove/__tests__/remove.test.js b/lib/remove/__tests__/remove.test.js index 92979d57..5add5997 100644 --- a/lib/remove/__tests__/remove.test.js +++ b/lib/remove/__tests__/remove.test.js @@ -4,7 +4,7 @@ const assert = require('assert') const fs = require('fs') const os = require('os') const path = require('path') -const sr = require('secure-random') +const randomBytes = require('crypto').randomBytes const fse = require(process.cwd()) /* global afterEach, beforeEach, describe, it */ @@ -12,7 +12,7 @@ const fse = require(process.cwd()) let TEST_DIR function buildFixtureDir () { - const buf = sr.randomBuffer(5) + const buf = randomBytes(5) const baseDir = path.join(TEST_DIR, `TEST_fs-extra_remove-${Date.now()}`) fs.mkdirSync(baseDir) diff --git a/lib/util/stat.js b/lib/util/stat.js new file mode 100644 index 00000000..5896a2bf --- /dev/null +++ b/lib/util/stat.js @@ -0,0 +1,175 @@ +'use strict' + +// TODO: enable this once graceful-fs supports bigint option. +// const fs = require('graceful-fs') +const fs = require('fs') +const path = require('path') + +const NODE_VERSION_MAJOR_WITH_BIGINT = 10 +const NODE_VERSION_MINOR_WITH_BIGINT = 5 +const NODE_VERSION_PATCH_WITH_BIGINT = 0 +const nodeVersion = process.versions.node.split('.') +const nodeVersionMajor = Number.parseInt(nodeVersion[0], 10) +const nodeVersionMinor = Number.parseInt(nodeVersion[1], 10) +const nodeVersionPatch = Number.parseInt(nodeVersion[2], 10) + +function nodeSupportsBigInt () { + if (nodeVersionMajor > NODE_VERSION_MAJOR_WITH_BIGINT) { + return true + } else if (nodeVersionMajor === NODE_VERSION_MAJOR_WITH_BIGINT) { + if (nodeVersionMinor > NODE_VERSION_MINOR_WITH_BIGINT) { + return true + } else if (nodeVersionMinor === NODE_VERSION_MINOR_WITH_BIGINT) { + if (nodeVersionPatch >= NODE_VERSION_PATCH_WITH_BIGINT) { + return true + } + } + } + return false +} + +function getStats (src, dest, cb) { + if (nodeSupportsBigInt()) { + fs.stat(src, { bigint: true }, (err, srcStat) => { + if (err) return cb(err) + fs.stat(dest, { bigint: true }, (err, destStat) => { + if (err) { + if (err.code === 'ENOENT') return cb(null, { srcStat, destStat: null }) + return cb(err) + } + return cb(null, { srcStat, destStat }) + }) + }) + } else { + fs.stat(src, (err, srcStat) => { + if (err) return cb(err) + fs.stat(dest, (err, destStat) => { + if (err) { + if (err.code === 'ENOENT') return cb(null, { srcStat, destStat: null }) + return cb(err) + } + return cb(null, { srcStat, destStat }) + }) + }) + } +} + +function getStatsSync (src, dest) { + let srcStat, destStat + if (nodeSupportsBigInt()) { + srcStat = fs.statSync(src, { bigint: true }) + } else { + srcStat = fs.statSync(src) + } + try { + if (nodeSupportsBigInt()) { + destStat = fs.statSync(dest, { bigint: true }) + } else { + destStat = fs.statSync(dest) + } + } catch (err) { + if (err.code === 'ENOENT') return { srcStat, destStat: null } + throw err + } + return { srcStat, destStat } +} + +function checkPaths (src, dest, funcName, cb) { + getStats(src, dest, (err, stats) => { + if (err) return cb(err) + const { srcStat, destStat } = stats + if (destStat && destStat.ino && destStat.dev && destStat.ino === srcStat.ino && destStat.dev === srcStat.dev) { + return cb(new Error('Source and destination must not be the same.')) + } + if (srcStat.isDirectory() && isSrcSubdir(src, dest)) { + return cb(new Error(errMsg(src, dest, funcName))) + } + return cb(null, { srcStat, destStat }) + }) +} + +function checkPathsSync (src, dest, funcName) { + const { srcStat, destStat } = getStatsSync(src, dest) + if (destStat && destStat.ino && destStat.dev && destStat.ino === srcStat.ino && destStat.dev === srcStat.dev) { + throw new Error('Source and destination must not be the same.') + } + if (srcStat.isDirectory() && isSrcSubdir(src, dest)) { + throw new Error(errMsg(src, dest, funcName)) + } + return { srcStat, destStat } +} + +// recursively check if dest parent is a subdirectory of src. +// It works for all file types including symlinks since it +// checks the src and dest inodes. It starts from the deepest +// parent and stops once it reaches the src parent or the root path. +function checkParentPaths (src, srcStat, dest, funcName, cb) { + const destParent = path.dirname(dest) + if (destParent && + (destParent === path.dirname(src) || + destParent === path.parse(destParent).root) + ) return cb() + if (nodeSupportsBigInt()) { + fs.stat(destParent, { bigint: true }, (err, destStat) => { + if (err) { + if (err.code === 'ENOENT') return cb() + return cb(err) + } + if (destStat.ino && destStat.dev && destStat.ino === srcStat.ino && destStat.dev === srcStat.dev) { + return cb(new Error(errMsg(src, dest, funcName))) + } + return checkParentPaths(src, srcStat, destParent, funcName, cb) + }) + } else { + fs.stat(destParent, (err, destStat) => { + if (err) { + if (err.code === 'ENOENT') return cb() + return cb(err) + } + if (destStat.ino && destStat.dev && destStat.ino === srcStat.ino && destStat.dev === srcStat.dev) { + return cb(new Error(errMsg(src, dest, funcName))) + } + return checkParentPaths(src, srcStat, destParent, funcName, cb) + }) + } +} + +function checkParentPathsSync (src, srcStat, dest, funcName) { + const destParent = path.dirname(dest) + if (destParent && (destParent === path.dirname(src) || destParent === path.parse(destParent).root)) return + let destStat + try { + if (nodeSupportsBigInt()) { + destStat = fs.statSync(destParent, { bigint: true }) + } else { + destStat = fs.statSync(destParent) + } + } catch (err) { + if (err.code === 'ENOENT') return + throw err + } + if (destStat.ino && destStat.dev && destStat.ino === srcStat.ino && destStat.dev === srcStat.dev) { + throw new Error(errMsg(src, dest, funcName)) + } + return checkParentPathsSync(src, srcStat, destParent, funcName) +} + +// return true if dest is a subdir of src, otherwise false. +// It only checks the path strings. +function isSrcSubdir (src, dest) { + const srcArr = path.resolve(src).split(path.sep).filter(i => i) + const destArr = path.resolve(dest).split(path.sep).filter(i => i) + return srcArr.reduce((acc, cur, i) => acc && destArr[i] === cur, true) +} + +function errMsg (src, dest, funcName) { + return `Cannot ${funcName} '${src}' to a subdirectory of itself, '${dest}'.` +} + +module.exports = { + checkPaths, + checkPathsSync, + checkParentPaths, + checkParentPathsSync, + isSrcSubdir +} diff --git a/package.json b/package.json index 9d992b88..c584ce30 100644 --- a/package.json +++ b/package.json @@ -49,11 +49,8 @@ "mocha": "^5.0.5", "proxyquire": "^2.0.1", "read-dir-files": "^0.1.1", - "rimraf": "^2.2.8", - "secure-random": "^1.1.1", "semver": "^5.3.0", - "standard": "^11.0.1", - "standard-markdown": "^4.0.1" + "standard": "^11.0.1" }, "main": "./lib/index.js", "scripts": {