diff --git a/README.md b/README.md index fec7d2d..5235071 100644 --- a/README.md +++ b/README.md @@ -1131,6 +1131,7 @@ var sh = fs.Shell(); * [sh.touch(path, [options], callback)](#touch) * [sh.cat(files, callback)](#cat) * [sh.rm(path, [options], callback)](#rm) +* [sh.mv(source, destination, callback)](#mv) * [sh.tempDir(callback)](#tempDir) * [sh.mkdirp(path, callback)](#mkdirp) @@ -1297,6 +1298,30 @@ sh.rm('/dir', { recursive: true }, function(err) { }); ``` +#### sh.mv(source, destination, callback) + +Moves the file or directory located at `source` to `destination`. Overwrites files +which share names by default. + +Example + +```javascript +sh.mv('./file', './renamed', function(err) { + if(err) throw err; + // ./file has been moved to the same directory under a new name +}); + +sh.mv('./dir', './otherdir', function(err) { + if(err) throw err; + // ./dir has been moved to ./otherdir/dir +}); + +sh.mv('./file', './dir', function(err) { + if(err) throw err; + // ./file has been moved to ./dir/file +}); +``` + #### sh.tempDir(callback) Gets the path to the shell's temporary directory, creating it if it diff --git a/src/shell/shell.js b/src/shell/shell.js index bccd9f8..97f91a1 100644 --- a/src/shell/shell.js +++ b/src/shell/shell.js @@ -5,6 +5,7 @@ define(function(require) { var Errors = require('src/errors'); var Environment = require('src/shell/environment'); var async = require('async'); + var Constants = require('src/constants'); function Shell(fs, options) { options = options || {}; @@ -336,6 +337,139 @@ define(function(require) { remove(path, callback); }; + /** + * Moves the file or directory at the `source` path to the + * `destination` path by relinking the source to the destination + * path. + */ + Shell.prototype.mv = function(source, destination, callback) { + var fs = this.fs; + var shell = this; + + callback = callback || function() {}; + + if(!source) { + callback(new Errors.EINVAL('missing source path argument')); + return; + } + else if(source === Constants.ROOT_DIRECTORY_NAME) { + callback(new Errors.EINVAL('the root directory is not a valid source argument')); + return; + } + + if(!destination) { + callback(new Errors.EINVAL('missing destination path argument')); + return; + } + + function move(sourcepath, destpath, callback) { + sourcepath = Path.resolve(this.cwd, sourcepath); + destpath = Path.resolve(this.cwd, destpath); + var destdir = Path.dirname(destpath); + + // Recursively create any directories on the destination path which do not exist + shell.mkdirp(destdir, function(error) { + if(error) { + callback(error); + return; + } + + // If there is no node at the source path, error and quit + fs.lstat(sourcepath, function(error, sourcestats) { + if(error) { + callback(error); + return; + } + + fs.lstat(destpath, function(error, deststats) { + // If there is an error unrelated to the existence of the destination, exit + if(error && error.code !== 'ENOENT') { + callback(error); + return; + } + + if(deststats) { + // If the destination is a directory, new destination is destpath/source.basename + if(deststats.isDirectory()) { + destpath = Path.join(destpath, Path.basename(sourcepath)); + } + } + + // Unlink existing destinations + fs.unlink(destpath, function(error) { + if (error && error.code !== 'ENOENT') { + callback(error); + return; + } + // If the source is a file, link it to destination and remove the source, then done + if(sourcestats.isFile() || sourcestats.isSymbolicLink()) { + fs.link(sourcepath, destpath, function(error) { + if (error) { + callback(error); + return; + } + shell.rm(sourcepath, {recursive:true}, function(error) { + if (error) { + callback(error); + return; + } + callback(); + }); + }); + } + // If the source is a directory, create a directory at destination and then recursively + // move every dir entry. + else if(sourcestats.isDirectory()) { + fs.mkdir(destpath, function(error) { + if (error) { + callback(error); + return; + } + + fs.readdir(sourcepath, function(error, entries) { + if(error) { + callback(error); + return; + } + + // Asychronously applies move to all nodes in the source directory + async.each(entries, + function(entry, callback) { + move(Path.join(sourcepath, entry), Path.join(destpath, entry), function(error) { + if(error) { + callback(error); + return; + } + callback(); + }); + }, + function(error) { + if(error) { + callback(error); + return; + } + // Remove source links after relocating + shell.rm(sourcepath, {recursive:true}, function(error) { + if (error) { + callback(error); + return; + } + callback(); + }); + } + ); + }); + }); + } + }); + }); + }); + }); + } + + move(source, destination, callback); + }; + /** * Gets the path to the temporary directory, creating it if not * present. The directory used is the one specified in diff --git a/tests/spec/shell/mv.spec.js b/tests/spec/shell/mv.spec.js new file mode 100644 index 0000000..7c87059 --- /dev/null +++ b/tests/spec/shell/mv.spec.js @@ -0,0 +1,488 @@ +define(["Filer", "util"], function(Filer, util) { + + describe('FileSystemShell.mv', function() { + beforeEach(util.setup); + afterEach(util.cleanup); + + it('should be a function', function() { + var shell = util.shell(); + expect(shell.mv).to.be.a('function'); + }); + + it('should fail when source argument is absent', function(done) { + var fs = util.fs(); + var shell = fs.Shell(); + + shell.mv(null, null, function(error) { + expect(error).to.exist; + done(); + }); + }); + + it('should fail when destination argument is absent', function(done) { + var fs = util.fs(); + var shell = fs.Shell(); + var contents = "a"; + + fs.writeFile('/file', contents, function(error) { + if(error) throw error; + + shell.mv('/file', null, function(error) { + expect(error).to.exist; + done(); + }); + }); + }); + + it('should fail when arguments are empty strings', function(done) { + var fs = util.fs(); + var shell = fs.Shell(); + + shell.mv('', '', function(error) { + expect(error).to.exist; + done(); + }); + }); + + it('should fail when the node at source path does not exist', function(done) { + var fs = util.fs(); + var shell = fs.Shell(); + + fs.mkdir('/dir', function(error) { + expect(error).to.not.exist; + }); + + shell.mv('/file', '/dir', function(error) { + expect(error).to.exist; + done(); + }); + }); + + it('should fail when root is provided as source argument', function(done) { + var fs = util.fs(); + var shell = fs.Shell(); + + fs.mkdir('/dir', function(error) { + expect(error).to.not.exist; + }); + + shell.mv('/', '/dir', function(error) { + expect(error).to.exist; + done(); + }); + }); + + it('should rename a file which is moved to the same directory under a different name', function(done) { + var fs = util.fs(); + var shell = fs.Shell(); + var contents = "a"; + + fs.writeFile('/file', contents, function(error) { + expect(error).to.not.exist; + + shell.mv('/file', '/newfile', function(error) { + expect(error).to.not.exist; + + fs.stat('/file', function(error, stats) { + expect(error).to.exist; + expect(stats).to.not.exist; + + fs.stat('/newfile', function(error, stats) { + expect(error).to.not.exist; + expect(stats).to.exist; + done(); + }); + }); + }); + }); + }); + + it('should rename a symlink which is moved to the same directory under a different name', function(done) { + var fs = util.fs(); + var shell = fs.Shell(); + var contents = "a"; + + fs.writeFile('/file', contents, function(error) { + expect(error).to.not.exist; + + fs.symlink('/file', '/newfile', function(error) { + expect(error).to.not.exist; + + shell.mv('/newfile', '/newerfile', function(error) { + expect(error).to.not.exist; + + fs.stat('/file', function(error, stats) { + expect(error).to.not.exist; + expect(stats).to.exist; + + fs.stat('/newfile', function(error, stats) { + expect(error).to.exist; + expect(stats).to.not.exist; + + fs.stat('/newerfile', function(error, stats) { + expect(error).to.not.exist; + expect(stats).to.exist; + done(); + }); + }); + }); + }); + }); + }); + }); + + it('should move a file to a directory which does not currently exist', function(done) { + var fs = util.fs(); + var shell = fs.Shell(); + var contents = "a"; + + fs.writeFile('/file', contents, function(error) { + expect(error).to.not.exist; + + shell.mv('/file', '/dir/newfile', function(error) { + expect(error).to.not.exist; + + fs.stat('/file', function(error, stats) { + expect(error).to.exist; + expect(stats).to.not.exist; + + fs.stat('/dir/newfile', function(error, stats) { + expect(error).to.not.exist; + expect(stats).to.exist; + done(); + }); + }); + }); + }); + }); + + it('should move a file into an empty directory', function(done) { + var fs = util.fs(); + var shell = fs.Shell(); + var contents = "a"; + + fs.mkdir('/dir', function(error) { + expect(error).to.not.exist; + + fs.stat('/dir', function(error, stats) { + expect(error).to.not.exist; + expect(stats).to.exist; + + fs.writeFile('/file', contents, function(error) { + expect(error).to.not.exist; + + fs.stat('/file', function(error, stats) { + expect(error).to.not.exist; + expect(stats).to.exist; + + shell.mv('/file', '/dir', function(error) { + expect(error).to.not.exist; + + fs.stat('/file', function(error, stats) { + expect(error).to.exist; + expect(stats).to.not.exist; + + fs.stat('/dir/file', function(error, stats) { + expect(error).to.not.exist; + expect(stats).to.exist; + done(); + }); + }); + }); + }); + }); + }); + }); + }); + + it('should move a file into a directory that has a file of the same name', function(done) { + var fs = util.fs(); + var shell = fs.Shell(); + var contents = "a"; + var contents2 = "b"; + + fs.mkdir('/dir', function(error) { + expect(error).to.not.exist; + + fs.writeFile('/file', contents, function(error) { + expect(error).to.not.exist; + + fs.writeFile('/dir/file', contents2, function(error) { + expect(error).to.not.exist; + + shell.mv('/file', '/dir/file', function(error) { + expect(error).to.not.exist; + + fs.stat('/file', function(error, stats) { + expect(error).to.exist; + expect(stats).to.not.exist; + + fs.stat('/dir/file', function(error, stats) { + expect(error).to.not.exist; + expect(stats).to.exist; + + fs.readFile('/dir/file', 'utf8', function(error, data) { + expect(error).not.to.exist; + expect(data).to.equal(contents); + done(); + }); + }); + }); + }); + }); + }); + }); + }); + + it('should move an empty directory to a destination that does not currently exist', function(done) { + var fs = util.fs(); + var shell = fs.Shell(); + + fs.mkdir('/dir', function(error) { + expect(error).to.not.exist; + + shell.mv('/dir', '/newdir', function(error) { + expect(error).to.not.exist; + + fs.stat('/dir', function(error, stats) { + expect(error).to.exist; + expect(stats).to.not.exist; + + fs.stat('/newdir', function(error, stats) { + expect(error).to.not.exist; + expect(stats).to.exist; + done(); + }); + }); + }); + }); + }); + + it('should move an empty directory to another empty directory', function(done) { + var fs = util.fs(); + var shell = fs.Shell(); + + fs.mkdir('/dir', function(error) { + expect(error).to.not.exist; + + fs.mkdir('/otherdir', function(error) { + expect(error).to.not.exist; + + shell.mv('/dir', '/otherdir', function(error) { + expect(error).to.not.exist; + + fs.stat('/dir', function(error, stats) { + expect(error).to.exist; + expect(stats).to.not.exist; + + fs.stat('/otherdir/dir', function(error, stats) { + expect(error).to.not.exist; + expect(stats).to.exist; + done(); + }); + }); + }); + }); + }); + }); + + it('should move an empty directory to a populated directory', function(done) { + var fs = util.fs(); + var shell = fs.Shell(); + var contents = "a"; + + fs.mkdir('/dir', function(error) { + expect(error).to.not.exist; + + fs.mkdir('/otherdir', function(error) { + expect(error).to.not.exist; + + fs.writeFile('/otherdir/file', contents, function(error) { + expect(error).to.not.exist; + + shell.mv('/dir', '/otherdir', function(error) { + expect(error).to.not.exist; + + fs.stat('/dir', function(error, stats) { + expect(error).to.exist; + expect(stats).to.not.exist; + + fs.stat('/otherdir/file', function(error, stats) { + expect(error).to.not.exist; + expect(stats).to.exist; + + fs.stat('/otherdir/dir', function(error, stats) { + expect(error).to.not.exist; + expect(stats).to.exist; + done(); + }); + }); + }); + }); + }); + }); + }); + }); + + it('should move a populated directory to a populated directory', function(done) { + var fs = util.fs(); + var shell = fs.Shell(); + var contents = "a"; + + fs.mkdir('/dir', function(error) { + expect(error).to.not.exist; + + fs.mkdir('/otherdir', function(error) { + expect(error).to.not.exist; + + fs.writeFile('/otherdir/file', contents, function(error) { + expect(error).to.not.exist; + + fs.writeFile('/dir/file', contents, function(error) { + expect(error).to.not.exist; + + shell.mv('/dir', '/otherdir', function(error) { + expect(error).to.not.exist; + + fs.stat('/dir', function(error, stats) { + expect(error).to.exist; + expect(stats).to.not.exist; + + fs.stat('/otherdir/file', function(error, stats) { + expect(error).to.not.exist; + expect(stats).to.exist; + + fs.stat('/otherdir/dir', function(error, stats) { + expect(error).to.not.exist; + expect(stats).to.exist; + + fs.stat('/otherdir/dir/file', function(error, stats) { + expect(error).to.not.exist; + expect(stats).to.exist; + done(); + }) + }); + }); + }); + }); + }); + }); + }); + }); + }); + + it('should move an empty directory to another empty directory', function(done) { + var fs = util.fs(); + var shell = fs.Shell(); + + fs.mkdir('/dir', function(error) { + expect(error).to.not.exist; + + fs.mkdir('/otherdir', function(error) { + expect(error).to.not.exist; + + shell.mv('/dir', '/otherdir', function(error) { + expect(error).to.not.exist; + + fs.stat('/dir', function(error, stats) { + expect(error).to.exist; + expect(stats).to.not.exist; + + fs.stat('/otherdir/dir', function(error, stats) { + expect(error).to.not.exist; + expect(stats).to.exist; + done(); + }); + }); + }); + }); + }); + }); + + it('should move an empty directory to a populated directory', function(done) { + var fs = util.fs(); + var shell = fs.Shell(); + var contents = "a"; + + fs.mkdir('/dir', function(error) { + expect(error).to.not.exist; + + fs.mkdir('/otherdir', function(error) { + expect(error).to.not.exist; + + fs.writeFile('/otherdir/file', contents, function(error) { + expect(error).to.not.exist; + + shell.mv('/dir', '/otherdir', function(error) { + expect(error).to.not.exist; + + fs.stat('/dir', function(error, stats) { + expect(error).to.exist; + expect(stats).to.not.exist; + + fs.stat('/otherdir/file', function(error, stats) { + expect(error).to.not.exist; + expect(stats).to.exist; + + fs.stat('/otherdir/dir', function(error, stats) { + expect(error).to.not.exist; + expect(stats).to.exist; + done(); + }); + }); + }); + }); + }); + }); + }); + }); + + it('should move a populated directory to a populated directory', function(done) { + var fs = util.fs(); + var shell = fs.Shell(); + var contents = "a"; + + fs.mkdir('/dir', function(error) { + expect(error).to.not.exist; + + fs.mkdir('/otherdir', function(error) { + expect(error).to.not.exist; + + fs.writeFile('/otherdir/file', contents, function(error) { + expect(error).to.not.exist; + + fs.writeFile('/dir/file', contents, function(error) { + expect(error).to.not.exist; + + shell.mv('/dir', '/otherdir', function(error) { + expect(error).to.not.exist; + + fs.stat('/dir', function(error, stats) { + expect(error).to.exist; + expect(stats).to.not.exist; + + fs.stat('/otherdir/file', function(error, stats) { + expect(error).to.not.exist; + expect(stats).to.exist; + + fs.stat('/otherdir/dir', function(error, stats) { + expect(error).to.not.exist; + expect(stats).to.exist; + + fs.stat('/otherdir/dir/file', function(error, stats) { + expect(error).to.not.exist; + expect(stats).to.exist; + done(); + }) + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); +}); + diff --git a/tests/test-manifest.js b/tests/test-manifest.js index 1e5a908..f7b256f 100644 --- a/tests/test-manifest.js +++ b/tests/test-manifest.js @@ -55,6 +55,7 @@ define([ "spec/shell/cat.spec", "spec/shell/ls.spec", "spec/shell/rm.spec", + "spec/shell/mv.spec", "spec/shell/env.spec", "spec/shell/mkdirp.spec",