├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── package.json └── test ├── a-file ├── a-folder ├── another-file └── another-folder │ └── file3 └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | - "0.10" 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Andrew Kelley 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation files 5 | (the "Software"), to deal in the Software without restriction, 6 | including without limitation the rights to use, copy, modify, merge, 7 | publish, distribute, sublicense, and/or sell copies of the Software, 8 | and to permit persons to whom the Software is furnished to do so, 9 | subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 18 | BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 19 | ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://secure.travis-ci.org/andrewrk/node-mv.png)](http://travis-ci.org/andrewrk/node-mv) 2 | 3 | Usage: 4 | ------ 5 | 6 | ```js 7 | var mv = require('mv'); 8 | 9 | mv('source/file', 'dest/file', function(err) { 10 | // done. it tried fs.rename first, and then falls back to 11 | // piping the source file to the dest file and then unlinking 12 | // the source file. 13 | }); 14 | ``` 15 | 16 | Another example: 17 | 18 | ```js 19 | mv('source/dir', 'dest/a/b/c/dir', {mkdirp: true}, function(err) { 20 | // done. it first created all the necessary directories, and then 21 | // tried fs.rename, then falls back to using ncp to copy the dir 22 | // to dest and then rimraf to remove the source dir 23 | }); 24 | ``` 25 | 26 | Another example: 27 | 28 | ```js 29 | mv('source/file', 'dest/file', {clobber: false}, function(err) { 30 | // done. If 'dest/file' exists, an error is returned 31 | // with err.code === 'EEXIST'. 32 | }); 33 | ``` 34 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var ncp = require('ncp').ncp; 3 | var path = require('path'); 4 | var rimraf = require('rimraf'); 5 | var mkdirp = require('mkdirp'); 6 | 7 | module.exports = mv; 8 | 9 | function mv(source, dest, options, cb){ 10 | if (typeof options === 'function') { 11 | cb = options; 12 | options = {}; 13 | } 14 | var shouldMkdirp = !!options.mkdirp; 15 | var clobber = options.clobber !== false; 16 | var limit = options.limit || 16; 17 | 18 | if (shouldMkdirp) { 19 | mkdirs(); 20 | } else { 21 | doRename(); 22 | } 23 | 24 | function mkdirs() { 25 | mkdirp(path.dirname(dest), function(err) { 26 | if (err) return cb(err); 27 | doRename(); 28 | }); 29 | } 30 | 31 | function doRename() { 32 | if (clobber) { 33 | fs.rename(source, dest, function(err) { 34 | if (!err) return cb(); 35 | if (err.code !== 'EXDEV') return cb(err); 36 | moveFileAcrossDevice(source, dest, clobber, limit, cb); 37 | }); 38 | } else { 39 | fs.link(source, dest, function(err) { 40 | if (err) { 41 | if (err.code === 'EXDEV') { 42 | moveFileAcrossDevice(source, dest, clobber, limit, cb); 43 | return; 44 | } 45 | if (err.code === 'EISDIR' || err.code === 'EPERM') { 46 | moveDirAcrossDevice(source, dest, clobber, limit, cb); 47 | return; 48 | } 49 | cb(err); 50 | return; 51 | } 52 | fs.unlink(source, cb); 53 | }); 54 | } 55 | } 56 | } 57 | 58 | function moveFileAcrossDevice(source, dest, clobber, limit, cb) { 59 | var outFlags = clobber ? 'w' : 'wx'; 60 | var ins = fs.createReadStream(source); 61 | var outs = fs.createWriteStream(dest, {flags: outFlags}); 62 | ins.on('error', function(err){ 63 | ins.destroy(); 64 | outs.destroy(); 65 | outs.removeListener('close', onClose); 66 | if (err.code === 'EISDIR' || err.code === 'EPERM') { 67 | moveDirAcrossDevice(source, dest, clobber, limit, cb); 68 | } else { 69 | cb(err); 70 | } 71 | }); 72 | outs.on('error', function(err){ 73 | ins.destroy(); 74 | outs.destroy(); 75 | outs.removeListener('close', onClose); 76 | cb(err); 77 | }); 78 | outs.once('close', onClose); 79 | ins.pipe(outs); 80 | function onClose(){ 81 | fs.unlink(source, cb); 82 | } 83 | } 84 | 85 | function moveDirAcrossDevice(source, dest, clobber, limit, cb) { 86 | var options = { 87 | stopOnErr: true, 88 | clobber: false, 89 | limit: limit, 90 | }; 91 | if (clobber) { 92 | rimraf(dest, { disableGlob: true }, function(err) { 93 | if (err) return cb(err); 94 | startNcp(); 95 | }); 96 | } else { 97 | startNcp(); 98 | } 99 | function startNcp() { 100 | ncp(source, dest, options, function(errList) { 101 | if (errList) return cb(errList[0]); 102 | rimraf(source, { disableGlob: true }, cb); 103 | }); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mv", 3 | "version": "2.1.1", 4 | "description": "fs.rename but works across devices. same as the unix utility 'mv'", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha test/test.js --reporter spec" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/andrewrk/node-mv.git" 12 | }, 13 | "keywords": [ 14 | "mv", 15 | "move", 16 | "rename", 17 | "device", 18 | "recursive", 19 | "folder" 20 | ], 21 | "author": "Andrew Kelley", 22 | "license": "MIT", 23 | "engines": { 24 | "node": ">=0.8.0" 25 | }, 26 | "devDependencies": { 27 | "mocha": "~2.2.5" 28 | }, 29 | "dependencies": { 30 | "mkdirp": "~0.5.1", 31 | "ncp": "~2.0.0", 32 | "rimraf": "~2.4.0" 33 | }, 34 | "bugs": { 35 | "url": "https://github.com/andrewrk/node-mv/issues" 36 | }, 37 | "homepage": "https://github.com/andrewrk/node-mv", 38 | "directories": { 39 | "test": "test" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/a-file: -------------------------------------------------------------------------------- 1 | sonic the hedgehog 2 | -------------------------------------------------------------------------------- /test/a-folder/another-file: -------------------------------------------------------------------------------- 1 | tails 2 | -------------------------------------------------------------------------------- /test/a-folder/another-folder/file3: -------------------------------------------------------------------------------- 1 | knuckles 2 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var fs = require('fs'); 3 | var rimraf = require('rimraf'); 4 | var describe = global.describe; 5 | var it = global.it; 6 | var mv = require('../'); 7 | 8 | var realFsRename = fs.rename; 9 | function overrideFsRename() { 10 | // makes fs.rename return cross-device error. 11 | fs.rename = function(src, dest, cb) { 12 | setTimeout(function() { 13 | var err = new Error(); 14 | err.code = 'EXDEV'; 15 | cb(err); 16 | }, 10); 17 | }; 18 | } 19 | 20 | function restoreFsRename() { 21 | fs.rename = realFsRename; 22 | } 23 | 24 | describe("mv", function() { 25 | it("should rename a file on the same device", function (done) { 26 | mv("test/a-file", "test/a-file-dest", function (err) { 27 | assert.ifError(err); 28 | fs.readFile("test/a-file-dest", 'utf8', function (err, contents) { 29 | assert.ifError(err); 30 | assert.strictEqual(contents, "sonic the hedgehog\n"); 31 | // move it back 32 | mv("test/a-file-dest", "test/a-file", done); 33 | }); 34 | }); 35 | }); 36 | 37 | it("should not overwrite if clobber = false", function (done) { 38 | mv("test/a-file", "test/a-folder/another-file", {clobber: false}, function (err) { 39 | assert.ok(err && err.code === 'EEXIST', "throw EEXIST"); 40 | done(); 41 | }); 42 | }); 43 | 44 | it("should not create directory structure by default", function (done) { 45 | mv("test/a-file", "test/does/not/exist/a-file-dest", function (err) { 46 | assert.strictEqual(err.code, 'ENOENT'); 47 | done(); 48 | }); 49 | }); 50 | 51 | it("should create directory structure when mkdirp option set", function (done) { 52 | mv("test/a-file", "test/does/not/exist/a-file-dest", {mkdirp: true}, function (err) { 53 | assert.ifError(err); 54 | fs.readFile("test/does/not/exist/a-file-dest", 'utf8', function (err, contents) { 55 | assert.ifError(err); 56 | assert.strictEqual(contents, "sonic the hedgehog\n"); 57 | // move it back 58 | mv("test/does/not/exist/a-file-dest", "test/a-file", function(err) { 59 | assert.ifError(err); 60 | rimraf("test/does", { disableGlob: true }, done); 61 | }); 62 | }); 63 | }); 64 | }); 65 | 66 | it("should work across devices", function (done) { 67 | overrideFsRename(); 68 | mv("test/a-file", "test/a-file-dest", function (err) { 69 | assert.ifError(err); 70 | fs.readFile("test/a-file-dest", 'utf8', function (err, contents) { 71 | assert.ifError(err); 72 | assert.strictEqual(contents, "sonic the hedgehog\n"); 73 | // move it back 74 | mv("test/a-file-dest", "test/a-file", function(err) { 75 | restoreFsRename(); 76 | done(err); 77 | }); 78 | }); 79 | }); 80 | }); 81 | 82 | it("should work across devices, even with special characters", function (done) { 83 | overrideFsRename(); 84 | mv("test/a-file", "test/a-*", function (err) { 85 | assert.ifError(err); 86 | fs.readFile("test/a-*", 'utf8', function (err, contents) { 87 | assert.ifError(err); 88 | assert.strictEqual(contents, "sonic the hedgehog\n"); 89 | // move it back 90 | mv("test/a-*", "test/a-file", function(err) { 91 | assert.ifError(err); 92 | fs.readFile("test/a-file", 'utf8', function (err, contents) { 93 | assert.ifError(err); 94 | assert.strictEqual(contents, "sonic the hedgehog\n"); 95 | restoreFsRename(); 96 | done(err); 97 | }); 98 | }); 99 | }); 100 | }); 101 | }); 102 | 103 | it("should move folders", function (done) { 104 | mv("test/a-folder", "test/a-folder-dest", function (err) { 105 | assert.ifError(err); 106 | fs.readFile("test/a-folder-dest/another-file", 'utf8', function (err, contents) { 107 | assert.ifError(err); 108 | assert.strictEqual(contents, "tails\n"); 109 | // move it back 110 | mv("test/a-folder-dest", "test/a-folder", done); 111 | }); 112 | }); 113 | }); 114 | 115 | it("should move folders across devices", function (done) { 116 | overrideFsRename(); 117 | mv("test/a-folder", "test/a-folder-dest", function (err) { 118 | assert.ifError(err); 119 | fs.readFile("test/a-folder-dest/another-folder/file3", 'utf8', function (err, contents) { 120 | assert.ifError(err); 121 | assert.strictEqual(contents, "knuckles\n"); 122 | // move it back 123 | mv("test/a-folder-dest", "test/a-folder", function(err) { 124 | restoreFsRename(); 125 | done(err); 126 | }); 127 | }); 128 | }); 129 | }); 130 | 131 | it("should move folders across devices, even with special characters", function (done) { 132 | overrideFsRename(); 133 | mv("test/a-folder", "test/a-*", function (err) { 134 | assert.ifError(err); 135 | fs.readFile("test/a-*/another-folder/file3", 'utf8', function (err, contents) { 136 | assert.ifError(err); 137 | assert.strictEqual(contents, "knuckles\n"); 138 | // move it back 139 | mv("test/a-*", "test/a-folder", function(err) { 140 | assert.ifError(err); 141 | fs.readFile("test/a-folder/another-folder/file3", 'utf8', function (err, contents) { 142 | assert.ifError(err); 143 | assert.strictEqual(contents, "knuckles\n"); 144 | restoreFsRename(); 145 | done(err); 146 | }); 147 | }); 148 | }); 149 | }); 150 | }); 151 | }); 152 | --------------------------------------------------------------------------------