diff --git a/lib/move/__tests__/move.test.js b/lib/move/__tests__/move.test.js index 342a8d26..96a4b1d5 100644 --- a/lib/move/__tests__/move.test.js +++ b/lib/move/__tests__/move.test.js @@ -5,7 +5,6 @@ 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 */ @@ -24,19 +23,16 @@ function createAsyncErrFn (errCode) { } const originalRename = fs.rename -const originalLink = fs.link function setUpMockFs (errCode) { fs.rename = createAsyncErrFn(errCode) - fs.link = createAsyncErrFn(errCode) } function tearDownMockFs () { fs.rename = originalRename - fs.link = originalLink } -describe('move', () => { +describe('+ move()', () => { let TEST_DIR beforeEach(() => { @@ -52,256 +48,233 @@ describe('move', () => { fs.writeFileSync(path.join(TEST_DIR, 'a-folder/another-folder/file3'), 'knuckles\n') }) - afterEach(done => rimraf(TEST_DIR, done)) + afterEach(done => fse.remove(TEST_DIR, done)) - it('should rename a file on the same device', done => { - const src = `${TEST_DIR}/a-file` - const dest = `${TEST_DIR}/a-file-dest` + describe('> when overwrite = true', () => { + it('should overwrite file', done => { + const src = path.join(TEST_DIR, 'a-file') + const dest = path.join(TEST_DIR, 'a-folder', 'another-file') - fse.move(src, dest, err => { - assert.ifError(err) - fs.readFile(dest, 'utf8', (err, contents) => { - const expected = /^sonic the hedgehog\r?\n$/ + // verify file exists already + assert(fs.existsSync(dest)) + + fse.move(src, dest, {overwrite: true}, err => { assert.ifError(err) - assert.ok(contents.match(expected), `${contents} match ${expected}`) - done() + fs.readFile(dest, 'utf8', (err, contents) => { + const expected = /^sonic the hedgehog\r?\n$/ + assert.ifError(err) + assert.ok(contents.match(expected), `${contents} match ${expected}`) + done() + }) }) }) - }) - it('should not move a file if source and destination are the same', done => { - const src = `${TEST_DIR}/a-file` - const dest = src + it('should overwrite the destination directory', done => { + // Create src + const src = path.join(TEST_DIR, 'src') + fse.ensureDirSync(src) + fse.mkdirsSync(path.join(src, 'some-folder')) + fs.writeFileSync(path.join(src, 'some-file'), 'hi') - fse.move(src, dest, err => { - assert.ifError(err) - done() - }) - }) + const dest = path.join(TEST_DIR, 'a-folder') - it('should error if source and destination are the same and source does not exist', done => { - const src = `${TEST_DIR}/non-existent` - const dest = src + // verify dest has stuff in it + const paths = fs.readdirSync(dest) + assert(paths.indexOf('another-file') >= 0) + assert(paths.indexOf('another-folder') >= 0) - fse.move(src, dest, err => { - assert(err) - done() - }) - }) + fse.move(src, dest, {overwrite: true}, err => { + assert.ifError(err) - it('should not move a directory if source and destination are the same', done => { - const src = `${TEST_DIR}/a-folder` - const dest = src + // verify dest does not have old stuff + const paths = fs.readdirSync(dest) + assert.strictEqual(paths.indexOf('another-file'), -1) + assert.strictEqual(paths.indexOf('another-folder'), -1) - fse.move(src, dest, err => { - assert.ifError(err) - done() - }) - }) + // verify dest has new stuff + assert(paths.indexOf('some-file') >= 0) + assert(paths.indexOf('some-folder') >= 0) - it('should not overwrite the destination by default', done => { - const src = `${TEST_DIR}/a-file` - const dest = `${TEST_DIR}/a-folder/another-file` + done() + }) + }) - // verify file exists already - assert(fs.existsSync(dest)) + it('should overwrite folders across devices', done => { + const src = path.join(TEST_DIR, 'a-folder') + const dest = path.join(TEST_DIR, 'a-folder-dest') - fse.move(src, dest, err => { - assert.ok(err && err.code === 'EEXIST', 'throw EEXIST') - done() - }) - }) + fs.mkdirSync(dest) - it('should not overwrite if overwrite = false', done => { - const src = `${TEST_DIR}/a-file` - const dest = `${TEST_DIR}/a-folder/another-file` + setUpMockFs('EXDEV') - // verify file exists already - assert(fs.existsSync(dest)) + fse.move(src, dest, {overwrite: true}, err => { + assert.ifError(err) + assert.strictEqual(fs.rename.callCount, 1) - fse.move(src, dest, {overwrite: false}, err => { - assert.ok(err && err.code === 'EEXIST', 'throw EEXIST') - done() + fs.readFile(path.join(dest, 'another-folder', 'file3'), 'utf8', (err, contents) => { + const expected = /^knuckles\r?\n$/ + assert.ifError(err) + assert.ok(contents.match(expected), `${contents} match ${expected}`) + tearDownMockFs() + done() + }) + }) }) }) - it('should overwrite file if overwrite = true', done => { - const src = `${TEST_DIR}/a-file` - const dest = `${TEST_DIR}/a-folder/another-file` - - // verify file exists already - assert(fs.existsSync(dest)) + describe('> when overwrite = false', () => { + it('should rename a file on the same device', done => { + const src = path.join(TEST_DIR, 'a-file') + const dest = path.join(TEST_DIR, 'a-file-dest') - fse.move(src, dest, {overwrite: true}, err => { - assert.ifError(err) - fs.readFile(dest, 'utf8', (err, contents) => { - const expected = /^sonic the hedgehog\r?\n$/ + fse.move(src, dest, err => { assert.ifError(err) - assert.ok(contents.match(expected), `${contents} match ${expected}`) - done() + fs.readFile(dest, 'utf8', (err, contents) => { + const expected = /^sonic the hedgehog\r?\n$/ + assert.ifError(err) + assert.ok(contents.match(expected), `${contents} match ${expected}`) + done() + }) }) }) - }) - - it('should overwrite the destination directory if overwrite = true', function (done) { - // Create src - const src = path.join(TEST_DIR, 'src') - fse.ensureDirSync(src) - fse.mkdirsSync(path.join(src, 'some-folder')) - fs.writeFileSync(path.join(src, 'some-file'), 'hi') - const dest = path.join(TEST_DIR, 'a-folder') + it('should not move a file if source and destination are the same', done => { + const src = path.join(TEST_DIR, 'a-file') + const dest = src - // verify dest has stuff in it - const paths = fs.readdirSync(dest) - assert(paths.indexOf('another-file') >= 0) - assert(paths.indexOf('another-folder') >= 0) - - fse.move(src, dest, {overwrite: true}, err => { - assert.ifError(err) - - // verify dest does not have old stuff - const paths = fs.readdirSync(dest) - assert.strictEqual(paths.indexOf('another-file'), -1) - assert.strictEqual(paths.indexOf('another-folder'), -1) + fse.move(src, dest, err => { + assert.ifError(err) + done() + }) + }) - // verify dest has new stuff - assert(paths.indexOf('some-file') >= 0) - assert(paths.indexOf('some-folder') >= 0) + it('should error if source and destination are the same and source does not exist', done => { + const src = path.join(TEST_DIR, 'non-existent') + const dest = src - done() + fse.move(src, dest, err => { + assert(err) + done() + }) }) - }) - - it('should create directory structure by default', done => { - const src = `${TEST_DIR}/a-file` - const dest = `${TEST_DIR}/does/not/exist/a-file-dest` - // verify dest directory does not exist - assert(!fs.existsSync(path.dirname(dest))) + it('should not move a directory if source and destination are the same', done => { + const src = path.join(TEST_DIR, 'a-folder') + const dest = src - fse.move(src, dest, err => { - assert.ifError(err) - fs.readFile(dest, 'utf8', (err, contents) => { - const expected = /^sonic the hedgehog\r?\n$/ + fse.move(src, dest, err => { assert.ifError(err) - assert.ok(contents.match(expected), `${contents} match ${expected}`) done() }) }) - }) - it('should work across devices', done => { - const src = `${TEST_DIR}/a-file` - const dest = `${TEST_DIR}/a-file-dest` + it('should not overwrite the destination by default', done => { + const src = path.join(TEST_DIR, 'a-file') + const dest = path.join(TEST_DIR, 'a-folder', 'another-file') - setUpMockFs('EXDEV') - - fse.move(src, dest, err => { - assert.ifError(err) - assert.strictEqual(fs.link.callCount, 1) - - fs.readFile(dest, 'utf8', (err, contents) => { - const expected = /^sonic the hedgehog\r?\n$/ - assert.ifError(err) - assert.ok(contents.match(expected), `${contents} match ${expected}`) + // verify file exists already + assert(fs.existsSync(dest)) - tearDownMockFs() + fse.move(src, dest, err => { + assert.strictEqual(err.message, 'dest already exists.') done() }) }) - }) - it('should move folders', done => { - const src = `${TEST_DIR}/a-folder` - const dest = `${TEST_DIR}/a-folder-dest` + it('should not overwrite if overwrite = false', done => { + const src = path.join(TEST_DIR, 'a-file') + const dest = path.join(TEST_DIR, 'a-folder', 'another-file') - // verify it doesn't exist - assert(!fs.existsSync(dest)) + // verify file exists already + assert(fs.existsSync(dest)) - fse.move(src, dest, err => { - assert.ifError(err) - fs.readFile(dest + '/another-file', 'utf8', (err, contents) => { - const expected = /^tails\r?\n$/ - assert.ifError(err) - assert.ok(contents.match(expected), `${contents} match ${expected}`) + fse.move(src, dest, {overwrite: false}, err => { + assert.strictEqual(err.message, 'dest already exists.') done() }) }) - }) - - it('should move folders across devices with EISDIR error', done => { - const src = `${TEST_DIR}/a-folder` - const dest = `${TEST_DIR}/a-folder-dest` - setUpMockFs('EISDIR') + it('should create directory structure by default', done => { + const src = path.join(TEST_DIR, 'a-file') + const dest = path.join(TEST_DIR, 'does', 'not', 'exist', 'a-file-dest') - fse.move(src, dest, err => { - assert.ifError(err) - assert.strictEqual(fs.link.callCount, 1) + // verify dest directory does not exist + assert(!fs.existsSync(path.dirname(dest))) - fs.readFile(dest + '/another-folder/file3', 'utf8', (err, contents) => { - const expected = /^knuckles\r?\n$/ + fse.move(src, dest, err => { assert.ifError(err) - assert.ok(contents.match(expected), `${contents} match ${expected}`) - - tearDownMockFs('EISDIR') - - done() + fs.readFile(dest, 'utf8', (err, contents) => { + const expected = /^sonic the hedgehog\r?\n$/ + assert.ifError(err) + assert.ok(contents.match(expected), `${contents} match ${expected}`) + done() + }) }) }) - }) - it('should overwrite folders across devices', done => { - const src = `${TEST_DIR}/a-folder` - const dest = `${TEST_DIR}/a-folder-dest` + it('should work across devices', done => { + const src = path.join(TEST_DIR, 'a-file') + const dest = path.join(TEST_DIR, 'a-file-dest') - fs.mkdirSync(dest) + setUpMockFs('EXDEV') - setUpMockFs('EXDEV') + fse.move(src, dest, err => { + assert.ifError(err) + assert.strictEqual(fs.rename.callCount, 1) - fse.move(src, dest, {overwrite: true}, err => { - assert.ifError(err) - assert.strictEqual(fs.rename.callCount, 1) + fs.readFile(dest, 'utf8', (err, contents) => { + const expected = /^sonic the hedgehog\r?\n$/ + assert.ifError(err) + assert.ok(contents.match(expected), `${contents} match ${expected}`) + tearDownMockFs() + done() + }) + }) + }) - fs.readFile(dest + '/another-folder/file3', 'utf8', (err, contents) => { - const expected = /^knuckles\r?\n$/ - assert.ifError(err) - assert.ok(contents.match(expected), `${contents} match ${expected}`) + it('should move folders', done => { + const src = path.join(TEST_DIR, 'a-folder') + const dest = path.join(TEST_DIR, 'a-folder-dest') - tearDownMockFs('EXDEV') + // verify it doesn't exist + assert(!fs.existsSync(dest)) - done() + fse.move(src, dest, err => { + assert.ifError(err) + fs.readFile(path.join(dest, 'another-file'), 'utf8', (err, contents) => { + const expected = /^tails\r?\n$/ + assert.ifError(err) + assert.ok(contents.match(expected), `${contents} match ${expected}`) + done() + }) }) }) - }) - - it('should move folders across devices with EXDEV error', done => { - const src = `${TEST_DIR}/a-folder` - const dest = `${TEST_DIR}/a-folder-dest` - setUpMockFs('EXDEV') + it('should move folders across devices with EXDEV error', done => { + const src = path.join(TEST_DIR, 'a-folder') + const dest = path.join(TEST_DIR, 'a-folder-dest') - fse.move(src, dest, err => { - assert.ifError(err) - assert.strictEqual(fs.link.callCount, 1) + setUpMockFs('EXDEV') - fs.readFile(dest + '/another-folder/file3', 'utf8', (err, contents) => { - const expected = /^knuckles\r?\n$/ + fse.move(src, dest, err => { assert.ifError(err) - assert.ok(contents.match(expected), `${contents} match ${expected}`) + assert.strictEqual(fs.rename.callCount, 1) - tearDownMockFs() - - done() + fs.readFile(path.join(dest, 'another-folder', 'file3'), 'utf8', (err, contents) => { + const expected = /^knuckles\r?\n$/ + assert.ifError(err) + assert.ok(contents.match(expected), `${contents} match ${expected}`) + tearDownMockFs() + done() + }) }) }) }) - describe('clobber', () => { + describe('> clobber', () => { it('should be an alias for overwrite', done => { - const src = `${TEST_DIR}/a-file` - const dest = `${TEST_DIR}/a-folder/another-file` + const src = path.join(TEST_DIR, 'a-file') + const dest = path.join(TEST_DIR, 'a-folder', 'another-file') // verify file exists already assert(fs.existsSync(dest)) @@ -358,7 +331,7 @@ describe('move', () => { const _it = __skipTests ? it.skip : it - describe('> just the folder', () => { + describe('>> just the folder', () => { _it('should move the folder', done => { const src = '/mnt/some/weird/dir-really-weird' const dest = path.join(TEST_DIR, 'device-weird') @@ -374,7 +347,6 @@ describe('move', () => { fse.move(src, dest, err => { assert.ifError(err) assert(fs.existsSync(dest)) - // console.log(path.normalize(dest)) assert(fs.lstatSync(dest).isDirectory()) done() }) diff --git a/lib/move/index.js b/lib/move/index.js index a7181351..5eef6043 100644 --- a/lib/move/index.js +++ b/lib/move/index.js @@ -1,170 +1,82 @@ 'use strict' -// most of this code was written by Andrew Kelley -// licensed under the BSD license: see -// https://github.com/andrewrk/node-mv/blob/master/package.json - -// this needs a cleanup - const u = require('universalify').fromCallback const fs = require('graceful-fs') -const copy = require('../copy/copy') const path = require('path') +const copy = require('../copy').copy const remove = require('../remove').remove -const mkdirp = require('../mkdirs').mkdirs +const mkdirp = require('../mkdirs').mkdirp +const pathExists = require('../path-exists').pathExists -function move (src, dest, options, callback) { - if (typeof options === 'function') { - callback = options - options = {} +function move (src, dest, opts, cb) { + if (typeof opts === 'function') { + cb = opts + opts = {} } - const overwrite = options.overwrite || options.clobber || false - - isSrcSubdir(src, dest, (err, itIs) => { - if (err) return callback(err) - if (itIs) return callback(new Error(`Cannot move '${src}' to a subdirectory of itself, '${dest}'.`)) - mkdirp(path.dirname(dest), err => { - if (err) return callback(err) - doRename() - }) - }) - - function doRename () { - if (path.resolve(src) === path.resolve(dest)) { - fs.access(src, callback) - } else if (overwrite) { - fs.rename(src, dest, err => { - if (!err) return callback() + const overwrite = opts.overwrite || opts.clobber || false - if (err.code === 'ENOTEMPTY' || err.code === 'EEXIST') { - remove(dest, err => { - if (err) return callback(err) - options.overwrite = false // just overwriteed it, no need to do it again - move(src, dest, options, callback) - }) - return - } + src = path.resolve(src) + dest = path.resolve(dest) - // weird Windows shit - if (err.code === 'EPERM') { - setTimeout(() => { - remove(dest, err => { - if (err) return callback(err) - options.overwrite = false - move(src, dest, options, callback) - }) - }, 200) - return - } + if (src === dest) return fs.access(src, cb) - if (err.code !== 'EXDEV') return callback(err) - moveAcrossDevice(src, dest, overwrite, callback) - }) - } else { - fs.link(src, dest, err => { - if (err) { - if (err.code === 'EXDEV' || err.code === 'EISDIR' || err.code === 'EPERM' || err.code === 'ENOTSUP') { - return moveAcrossDevice(src, dest, overwrite, callback) - } - return callback(err) - } - return fs.unlink(src, callback) - }) - } - } -} - -function moveAcrossDevice (src, dest, overwrite, callback) { - fs.stat(src, (err, stat) => { - if (err) return callback(err) + fs.stat(src, (err, st) => { + if (err) return cb(err) - if (stat.isDirectory()) { - moveDirAcrossDevice(src, dest, overwrite, callback) - } else { - moveFileAcrossDevice(src, dest, overwrite, callback) + 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 moveFileAcrossDevice (src, dest, overwrite, callback) { - const flags = overwrite ? 'w' : 'wx' - const ins = fs.createReadStream(src) - const outs = fs.createWriteStream(dest, { flags }) - - ins.on('error', err => { - ins.destroy() - outs.destroy() - outs.removeListener('close', onClose) - - // may want to create a directory but `out` line above - // creates an empty file for us: See #108 - // don't care about error here - fs.unlink(dest, () => { - // note: `err` here is from the input stream errror - if (err.code === 'EISDIR' || err.code === 'EPERM') { - moveDirAcrossDevice(src, dest, overwrite, callback) - } else { - callback(err) - } +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) }) +} - outs.on('error', err => { - ins.destroy() - outs.destroy() - outs.removeListener('close', onClose) - callback(err) +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) }) - - outs.once('close', onClose) - ins.pipe(outs) - - function onClose () { - fs.unlink(src, callback) - } } -function moveDirAcrossDevice (src, dest, overwrite, callback) { - const options = { - overwrite: false - } - - if (overwrite) { - remove(dest, err => { - if (err) return callback(err) - startCopy() - }) - } else { - startCopy() - } - - function startCopy () { - copy(src, dest, options, err => { - if (err) return callback(err) - remove(src, callback) - }) +function moveAcrossDevice (src, dest, overwrite, cb) { + const opts = { + overwrite: overwrite, + errorOnExist: true } -} -// 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, cb) { - fs.stat(src, (err, st) => { + copy(src, dest, opts, err => { if (err) return cb(err) - if (st.isDirectory()) { - const baseDir = dest.split(path.dirname(src) + path.sep)[1] - if (baseDir) { - const destBasename = baseDir.split(path.sep)[0] - if (destBasename) return cb(null, src !== dest && dest.indexOf(src) > -1 && destBasename === path.basename(src)) - return cb(null, false) - } - return cb(null, false) - } - return cb(null, false) + 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) }