diff --git a/lib/move/__tests__/move.preserveTimestamps.test.js b/lib/move/__tests__/move.preserveTimestamps.test.js new file mode 100644 index 00000000..879b1a8a --- /dev/null +++ b/lib/move/__tests__/move.preserveTimestamps.test.js @@ -0,0 +1,132 @@ +/** + * Created by user on 2017/3/8. + */ +'use strict' + +const fs = require('graceful-fs') +const os = require('os') +const fse = require(process.cwd()) +const path = require('path') +const assert = require('assert') +const rimraf = require('rimraf') + +const utimes = require('../../util/utimes') + +/* global afterEach, beforeEach, describe, it */ + +function createAsyncErrFn (errCode) { + const fn = function () { + fn.callCount++ + const callback = arguments[arguments.length - 1] + setTimeout(() => { + const err = new Error() + err.code = errCode + callback(err) + }, 10) + } + fn.callCount = 0 + return fn +} + +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 +} + +function testFile (src, dest, options) { + return function (file) { + const a = src + const b = dest + const fromStat = fs.statSync(a) + const toStat = fs.statSync(b) + if (options.preserveTimestamps) { + // https://github.com/nodejs/io.js/issues/2069 + if (process.platform !== 'win32') { + assert.strictEqual(toStat.mtime.getTime(), fromStat.mtime.getTime()) + assert.strictEqual(toStat.atime.getTime(), fromStat.atime.getTime()) + } else { + assert.strictEqual(toStat.mtime.getTime(), utimes.timeRemoveMillis(fromStat.mtime.getTime())) + assert.strictEqual(toStat.atime.getTime(), utimes.timeRemoveMillis(fromStat.atime.getTime())) + } + } else { + assert.notEqual(toStat.mtime.getTime(), fromStat.mtime.getTime()) + // the access time might actually be the same, so check only modification time + } + } +} + +describe('move', () => { + let TEST_DIR + + beforeEach(() => { + TEST_DIR = path.join(os.tmpdir(), 'fs-extra', 'move') + + 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') + }) + + afterEach(done => rimraf(TEST_DIR, done)) + + it('should preserve timestamps', done => { + const src = `${TEST_DIR}/a-file` + const dest = `${TEST_DIR}/a-folder/another-folder/a-file-dest` + + const options = { + preserveTimestamps: true + } + + fs.stat(src, (srcErr, srcStat) => { + assert.ifError(srcErr) + setTimeout(() => { + fse.move(src, dest, options, err => { + assert.ifError(err) + fs.stat(dest, (descErr, destStat) => { + assert.ifError(descErr) + testFile(src, dest, options) + done() + }) + }) + }) + }, 1500) + }) + + it('should preserve timestamps across devices', done => { + const src = `${TEST_DIR}/a-file` + const dest = `${TEST_DIR}/a-folder/another-folder/a-file-dest` + + const options = { + preserveTimestamps: true + } + + setUpMockFs('EXDEV') + + fs.stat(src, (srcErr, srcStat) => { + assert.ifError(srcErr) + setTimeout(() => { + fse.move(src, dest, options, err => { + assert.ifError(err) + fs.stat(dest, (descErr, destStat) => { + assert.ifError(descErr) + testFile(src, dest, options) + tearDownMockFs() + done() + }) + }) + }) + }, 1500) + }) +}) diff --git a/lib/move/index.js b/lib/move/index.js index 195ab9f7..44fe13be 100644 --- a/lib/move/index.js +++ b/lib/move/index.js @@ -1,159 +1,176 @@ -'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 fs = require('graceful-fs') -const ncp = require('../copy/ncp') -const path = require('path') -const remove = require('../remove').remove -const mkdirp = require('../mkdirs').mkdirs - -function move (source, dest, options, callback) { - if (typeof options === 'function') { - callback = options - options = {} - } - - const shouldMkdirp = ('mkdirp' in options) ? options.mkdirp : true - const overwrite = options.overwrite || options.clobber || false - - if (shouldMkdirp) { - mkdirs() - } else { - doRename() - } - - function mkdirs () { - mkdirp(path.dirname(dest), err => { - if (err) return callback(err) - doRename() - }) - } - - function doRename () { - if (overwrite) { - fs.rename(source, dest, err => { - if (!err) return callback() - - 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(source, dest, options, callback) - }) - return - } - - // weird Windows shit - if (err.code === 'EPERM') { - setTimeout(() => { - remove(dest, err => { - if (err) return callback(err) - options.overwrite = false - move(source, dest, options, callback) - }) - }, 200) - return - } - - if (err.code !== 'EXDEV') return callback(err) - moveAcrossDevice(source, dest, overwrite, callback) - }) - } else { - fs.link(source, dest, err => { - if (err) { - if (err.code === 'EXDEV' || err.code === 'EISDIR' || err.code === 'EPERM' || err.code === 'ENOTSUP') { - moveAcrossDevice(source, dest, overwrite, callback) - return - } - callback(err) - return - } - fs.unlink(source, callback) - }) - } - } -} - -function moveAcrossDevice (source, dest, overwrite, callback) { - fs.stat(source, (err, stat) => { - if (err) { - callback(err) - return - } - - if (stat.isDirectory()) { - moveDirAcrossDevice(source, dest, overwrite, callback) - } else { - moveFileAcrossDevice(source, dest, overwrite, callback) - } - }) -} - -function moveFileAcrossDevice (source, dest, overwrite, callback) { - const flags = overwrite ? 'w' : 'wx' - const ins = fs.createReadStream(source) - 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(source, dest, overwrite, callback) - } else { - callback(err) - } - }) - }) - - outs.on('error', err => { - ins.destroy() - outs.destroy() - outs.removeListener('close', onClose) - callback(err) - }) - - outs.once('close', onClose) - ins.pipe(outs) - - function onClose () { - fs.unlink(source, callback) - } -} - -function moveDirAcrossDevice (source, dest, overwrite, callback) { - const options = { - overwrite: false - } - - if (overwrite) { - remove(dest, err => { - if (err) return callback(err) - startNcp() - }) - } else { - startNcp() - } - - function startNcp () { - ncp(source, dest, options, err => { - if (err) return callback(err) - remove(source, callback) - }) - } -} - -module.exports = { - move -} +'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 fs = require('graceful-fs') +const ncp = require('../copy/ncp') +const path = require('path') +const remove = require('../remove').remove +const mkdirp = require('../mkdirs').mkdirs + +const utimes = require('../util/utimes') + +function move (source, dest, options, callback) { + if (typeof options === 'function') { + callback = options + options = {} + } + + const shouldMkdirp = ('mkdirp' in options) ? options.mkdirp : true + const overwrite = options.overwrite || options.clobber || false + + let optionsCopy = { + preserveTimestamps: ('preserveTimestamps' in options) ? options.preserveTimestamps : true + } + + if (shouldMkdirp) { + mkdirs() + } else { + doRename() + } + + function mkdirs () { + mkdirp(path.dirname(dest), err => { + if (err) return callback(err) + doRename() + }) + } + + function doRename () { + if (overwrite) { + fs.rename(source, dest, err => { + if (!err) return callback() + + 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(source, dest, options, callback) + }) + return + } + + // weird Windows shit + if (err.code === 'EPERM') { + setTimeout(() => { + remove(dest, err => { + if (err) return callback(err) + options.overwrite = false + move(source, dest, options, callback) + }) + }, 200) + return + } + + if (err.code !== 'EXDEV') return callback(err) + moveAcrossDevice(source, dest, overwrite, optionsCopy, callback) + }) + } else { + fs.link(source, dest, err => { + if (err) { + if (err.code === 'EXDEV' || err.code === 'EISDIR' || err.code === 'EPERM' || err.code === 'ENOTSUP') { + moveAcrossDevice(source, dest, overwrite, optionsCopy, callback) + return + } + callback(err) + return + } + fs.unlink(source, callback) + }) + } + } +} + +function moveAcrossDevice (source, dest, overwrite, optionsCopy, callback) { + fs.stat(source, (err, stat) => { + if (err) { + callback(err) + return + } + + let optionsTemp = Object.assign({}, optionsCopy, { + stat: stat + }) + + if (stat.isDirectory()) { + moveDirAcrossDevice(source, dest, overwrite, optionsTemp, callback) + } else { + moveFileAcrossDevice(source, dest, overwrite, optionsTemp, callback) + } + }) +} + +function moveFileAcrossDevice (source, dest, overwrite, optionsTemp, callback) { + const flags = overwrite ? 'w' : 'wx' + const ins = fs.createReadStream(source) + 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(source, dest, overwrite, optionsTemp, callback) + } else { + if (optionsTemp.preserveTimestamps) { + utimes.utimesMillis(dest, optionsTemp.stat.atime, optionsTemp.stat.mtime, function (err) { + return callback(err) + }) + } else { + callback(err) + } + } + }) + }) + + outs.on('error', err => { + ins.destroy() + outs.destroy() + outs.removeListener('close', onClose) + callback(err) + }) + + outs.once('close', onClose) + ins.pipe(outs) + + function onClose () { + fs.unlink(source, callback) + } +} + +function moveDirAcrossDevice (source, dest, overwrite, optionsTemp, callback) { + const options = { + preserveTimestamps: optionsTemp.preserveTimestamps, + overwrite: false + } + + if (overwrite) { + remove(dest, err => { + if (err) return callback(err) + startNcp() + }) + } else { + startNcp() + } + + function startNcp () { + ncp(source, dest, options, err => { + if (err) return callback(err) + remove(source, callback) + }) + } +} + +module.exports = { + move +}