Add moveSync and its tests

This commit is contained in:
Mani Maghsoudlou 2017-03-06 23:02:55 -08:00
parent 9bef553728
commit 00063917f7
3 changed files with 528 additions and 0 deletions

View File

@ -18,6 +18,7 @@ assign(fs, require('./mkdirs'))
assign(fs, require('./remove'))
assign(fs, require('./json'))
assign(fs, require('./move'))
assign(fs, require('./move-sync'))
assign(fs, require('./empty'))
assign(fs, require('./ensure'))
assign(fs, require('./output'))

View File

@ -0,0 +1,411 @@
'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')
/* global afterEach, beforeEach, describe, it */
function createSyncErrFn (errCode) {
const fn = function () {
const err = new Error()
err.code = errCode
throw err
}
return fn
}
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()', () => {
let TEST_DIR
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')
})
afterEach(done => rimraf(TEST_DIR, done))
it('should not move if src and dest are the same', () => {
const src = `${TEST_DIR}/a-file`
const dest = `${TEST_DIR}/a-file`
try {
fse.moveSync(src, dest)
} catch (err) {
assert.ifError(err)
}
// 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}`)
})
it('should rename a file on the same device', () => {
const src = `${TEST_DIR}/a-file`
const dest = `${TEST_DIR}/a-file-dest`
try {
fse.moveSync(src, dest)
} catch (err) {
assert.ifError(err)
}
const contents = fs.readFileSync(dest, 'utf8')
const expected = /^sonic the hedgehog\r?\n$/
assert.ok(contents.match(expected), `${contents} match ${expected}`)
})
it('should not overwrite the destination by default', () => {
const src = `${TEST_DIR}/a-file`
const dest = `${TEST_DIR}/a-folder/another-file`
// verify file exists already
assert(fs.existsSync(dest))
try {
fse.moveSync(src, dest)
} catch (err) {
assert.ok(err && err.code === 'EEXIST', 'throw EEXIST')
}
})
it('should not overwrite if overwrite = false', () => {
const src = `${TEST_DIR}/a-file`
const dest = `${TEST_DIR}/a-folder/another-file`
// verify file exists already
assert(fs.existsSync(dest))
try {
fse.moveSync(src, dest, {overwrite: false})
} catch (err) {
assert.ok(err && err.code === 'EEXIST', 'throw EEXIST')
}
})
it('should overwrite file if overwrite = true', () => {
const src = `${TEST_DIR}/a-file`
const dest = `${TEST_DIR}/a-folder/another-file`
// verify file exists already
assert(fs.existsSync(dest))
try {
fse.moveSync(src, dest, {overwrite: true})
} catch (err) {
assert.ifError(err)
}
const contents = fs.readFileSync(dest, 'utf8')
const expected = /^sonic the hedgehog\r?\n$/
assert.ok(contents.match(expected), `${contents} match ${expected}`)
})
it('should overwrite the destination directory if overwrite = true', function (done) {
// Tests fail on appveyor/Windows due to
// https://github.com/isaacs/node-graceful-fs/issues/98.
// Workaround by increasing the timeout by a minute (because
// graceful times out after a minute).
this.timeout(90000)
// 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')
// verify dest has stuff in it
const pathsBefore = fs.readdirSync(dest)
assert(pathsBefore.indexOf('another-file') >= 0)
assert(pathsBefore.indexOf('another-folder') >= 0)
try {
fse.moveSync(src, dest, {overwrite: true})
} catch (err) {
assert.ifError(err)
}
// verify dest does not have old stuff
const pathsAfter = fs.readdirSync(dest)
assert.strictEqual(pathsAfter.indexOf('another-file'), -1)
assert.strictEqual(pathsAfter.indexOf('another-folder'), -1)
// verify dest has new stuff
assert(pathsAfter.indexOf('some-file') >= 0)
assert(pathsAfter.indexOf('some-folder') >= 0)
done()
})
/*
it('should not create directory structure if mkdirp is false', 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)))
fse.move(src, dest, {mkdirp: false}, err => {
assert.strictEqual(err.code, 'ENOENT')
done()
})
})
*/
it('should create directory structure by default', () => {
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)))
try {
fse.moveSync(src, dest)
} catch (err) {
assert.ifError(err)
}
const contents = fs.readFileSync(dest, 'utf8')
const expected = /^sonic the hedgehog\r?\n$/
assert.ok(contents.match(expected), `${contents} match ${expected}`)
})
it('should work across devices', () => {
const src = `${TEST_DIR}/a-file`
const dest = `${TEST_DIR}/a-file-dest`
setUpMockFs('EXDEV')
try {
fse.moveSync(src, dest)
} catch (err) {
assert.ifError(err)
}
const contents = fs.readFileSync(dest, 'utf8')
const expected = /^sonic the hedgehog\r?\n$/
assert.ok(contents.match(expected), `${contents} match ${expected}`)
tearDownMockFs('EXDEV')
})
it('should move folders', () => {
const src = `${TEST_DIR}/a-folder`
const dest = `${TEST_DIR}/a-folder-dest`
// verify it doesn't exist
assert(!fs.existsSync(dest))
try {
fse.moveSync(src, dest)
} catch (err) {
assert.ifError(err)
}
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')
try {
fse.moveSync(src, dest)
} catch (err) {
assert.ifError(err)
}
const contents = fs.readFileSync(dest + '/another-folder/file3', 'utf8')
const expected = /^knuckles\r?\n$/
assert.ok(contents.match(expected), `${contents} match ${expected}`)
tearDownMockFs('EISDIR')
})
it('should overwrite folders across devices', () => {
const src = `${TEST_DIR}/a-folder`
const dest = `${TEST_DIR}/a-folder-dest`
fs.mkdirSync(dest)
setUpMockFs('EXDEV')
try {
fse.moveSync(src, dest, {overwrite: true})
} catch (err) {
assert.ifError(err)
}
const contents = fs.readFileSync(dest + '/another-folder/file3', 'utf8')
const expected = /^knuckles\r?\n$/
assert.ok(contents.match(expected), `${contents} match ${expected}`)
tearDownMockFs('EXDEV')
})
it('should move folders across devices with EXDEV error', () => {
const src = `${TEST_DIR}/a-folder`
const dest = `${TEST_DIR}/a-folder-dest`
setUpMockFs('EXDEV')
try {
fse.moveSync(src, dest)
} catch (err) {
assert.ifError(err)
}
const contents = fs.readFileSync(dest + '/another-folder/file3', 'utf8')
const expected = /^knuckles\r?\n$/
assert.ok(contents.match(expected), `${contents} match ${expected}`)
tearDownMockFs('EXDEV')
})
it('should move folders across devices with EPERM error', () => {
const src = `${TEST_DIR}/a-folder`
const dest = `${TEST_DIR}/a-folder-dest`
setUpMockFs('EPERM')
try {
fse.moveSync(src, dest)
} catch (err) {
assert.ifError(err)
}
const contents = fs.readFileSync(dest + '/another-folder/file3', 'utf8')
const expected = /^knuckles\r?\n$/
assert.ok(contents.match(expected), `${contents} match ${expected}`)
tearDownMockFs('EPERM')
})
it('should move folders across devices with ENOTSUP error', () => {
const src = `${TEST_DIR}/a-folder`
const dest = `${TEST_DIR}/a-folder-dest`
setUpMockFs('ENOTSUP')
try {
fse.moveSync(src, dest)
} catch (err) {
assert.ifError(err)
}
const contents = fs.readFileSync(dest + '/another-folder/file3', 'utf8')
const expected = /^knuckles\r?\n$/
assert.ok(contents.match(expected), `${contents} match ${expected}`)
tearDownMockFs('ENOTSUP')
})
describe('clobber', () => {
it('should be an alias for overwrite', () => {
const src = `${TEST_DIR}/a-file`
const dest = `${TEST_DIR}/a-folder/another-file`
// verify file exists already
assert(fs.existsSync(dest))
try {
fse.moveSync(src, dest, {clobber: true})
} catch (err) {
assert.ifError(err)
}
const contents = fs.readFileSync(dest, 'utf8')
const expected = /^sonic the hedgehog\r?\n$/
assert.ok(contents.match(expected), `${contents} match ${expected}`)
})
})
describe.skip('> when trying to a move a folder into itself', () => {
it('should produce an error', () => {
const SRC_DIR = path.join(TEST_DIR, 'test')
const DEST_DIR = path.join(TEST_DIR, 'test', 'test')
assert(!fs.existsSync(SRC_DIR))
fs.mkdirSync(SRC_DIR)
assert(fs.existsSync(SRC_DIR))
try {
fse.moveSync(SRC_DIR, DEST_DIR)
} catch (err) {
assert(err)
assert(fs.existsSync(SRC_DIR))
}
})
})
// tested on Linux ubuntu 3.13.0-32-generic #57-Ubuntu SMP i686 i686 GNU/Linux
// this won't trigger a bug on Mac OS X Yosimite with a USB drive (/Volumes)
// see issue #108
describe('> when actually trying to a move a folder across devices', () => {
const differentDevice = '/mnt'
let __skipTests = false
// must set this up, if not, exit silently
if (!fs.existsSync(differentDevice)) {
console.log('Skipping cross-device move test')
__skipTests = true
}
// make sure we have permission on device
try {
fs.writeFileSync(path.join(differentDevice, 'file'), 'hi')
} catch (err) {
console.log("Can't write to device. Skipping moveSync test.")
__skipTests = true
}
const _it = __skipTests ? it.skip : it
describe('> just the folder', () => {
_it('should move the folder', () => {
const src = '/mnt/some/weird/dir-really-weird'
const dest = path.join(TEST_DIR, 'device-weird')
if (!fs.existsSync(src)) {
fse.mkdirpSync(src)
}
assert(!fs.existsSync(dest))
assert(fs.lstatSync(src).isDirectory())
try {
fse.moveSync(src, dest)
} catch (err) {
assert.ifError(err)
}
assert(fs.existsSync(dest))
assert(fs.lstatSync(dest).isDirectory())
})
})
})
})

116
lib/move-sync/index.js Normal file
View File

@ -0,0 +1,116 @@
'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 is the sync version that somehow follows the same pattern.
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
function moveSync (src, dest, options) {
options = options || {}
const overwrite = options.overwrite || options.clobber || false
if (path.resolve(src) === path.resolve(dest)) return
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.alloc(BUF_LENGTH)
const flags = overwrite ? 'w' : 'wx'
try {
const fdr = fs.openSync(src, 'r')
const stat = fs.fstatSync(fdr)
const fdw = fs.openSync(dest, flags, stat.mode)
let bytesRead = 1
let pos = 0
while (bytesRead > 0) {
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)
} catch (err) {
// 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.unlinkSync(dest)
// note: `err` here is from the fdr error
if (err.code === 'EISDIR' || err.code === 'EPERM') {
return moveDirSyncAcrossDevice(src, dest, overwrite)
}
throw err
}
}
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)
}
}
module.exports = {
moveSync
}