diff --git a/doc/api.txt b/doc/api.txt index 071b88a9dc..73f4f54c2e 100644 --- a/doc/api.txt +++ b/doc/api.txt @@ -622,6 +622,14 @@ The callback gets two arguments +(err, resolvedPath)+. Synchronous readlink(2). Returns the resolved path. ++fs.realpath(path, callback)+ :: +Asynchronous realpath(2). +The callback gets two arguments +(err, resolvedPath)+. + ++fs.realpathSync(path)+ :: +Synchronous realpath(2). Returns the resolved path. + + +fs.unlink(path, callback)+ :: Asynchronous unlink(2). No arguments other than a possible exception are given to the completion callback. diff --git a/lib/fs.js b/lib/fs.js index 0bc68a58bb..5687424dc2 100644 --- a/lib/fs.js +++ b/lib/fs.js @@ -289,4 +289,89 @@ exports.unwatchFile = function (filename) { } }; +// Realpath + +var path = require('path'); +var dirname = path.dirname, + basename = path.basename, + normalize = path.normalize; + +function readlinkDeepSync(path, stats) { + var seen_links = {}, resolved_link, stats, file_id; + while (true) { + stats = stats || exports.lstatSync(path); + file_id = stats.dev.toString(32)+":"+stats.ino.toString(32); + if (file_id in seen_links) { + throw new Error("cyclic symbolic link at "+path); + } else { + seen_links[file_id] = 1; + if (stats.isSymbolicLink()) { + var newpath = exports.readlinkSync(path); + if (newpath.charAt(0) === '/') { + path = newpath; + } else { + var dir = dirname(path); + path = (dir !== '') ? dir + '/' + newpath : newpath; + } + } else { + return normalize(path); + } + } + stats = null; + } +} + +function readlinkDeep(path, stats, callback) { + var seen_links = {}, resolved_link, file_id; + function next(stats) { + file_id = stats.dev.toString(32)+":"+stats.ino.toString(32); + if (file_id in seen_links) { + callback(new Error("cyclic symbolic link at "+path)); + } else { + seen_links[file_id] = 1; + if (stats.isSymbolicLink()) { + exports.readlink(path, function(err, newpath) { + if (err) callback(err); + if (newpath.charAt(0) === '/') { + path = newpath; + } else { + var dir = dirname(path); + path = (dir !== '') ? dir + '/' + newpath : newpath; + } + _next(); + }); + } else { + callback(null, normalize(path)); + } + } + } + function _next() { + exports.lstat(path, function(err, stats){ + if (err) callback(err); + else next(stats); + }); + } + if (stats) next(stats); + else _next(); +} +exports.realpathSync = function(path) { + var stats = exports.lstatSync(path); + if (stats.isSymbolicLink()) + return readlinkDeepSync(path, stats); + else + return normalize(path); +} + +exports.realpath = function(path, callback) { + var resolved_path = path; + if (!callback) return; + exports.lstat(path, function(err, stats){ + if (err) + callback(err); + else if (stats.isSymbolicLink()) + readlinkDeep(path, stats, callback); + else + callback(null, normalize(path)); + }); +} diff --git a/test/simple/test-fs-realpath.js b/test/simple/test-fs-realpath.js new file mode 100644 index 0000000000..384faebf4f --- /dev/null +++ b/test/simple/test-fs-realpath.js @@ -0,0 +1,56 @@ +process.mixin(require("../common")); + +var async_completed = 0, async_expected = 0; + +// a. deep relative file symlink +var dstPath = path.join(fixturesDir, 'cycles', 'root.js'); +var linkData1 = "../../cycles/root.js"; +var linkPath1 = path.join(fixturesDir, "nested-index", 'one', 'symlink1.js'); +try {fs.unlinkSync(linkPath1);}catch(e){} +fs.symlinkSync(linkData1, linkPath1); + +var linkData2 = "../one/symlink1.js"; +var linkPath2 = path.join(fixturesDir, "nested-index", 'two', 'symlink1-b.js'); +try {fs.unlinkSync(linkPath2);}catch(e){} +fs.symlinkSync(linkData2, linkPath2); + +// b. deep relative directory symlink +var dstPath_b = path.join(fixturesDir, 'cycles', 'folder'); +var linkData1b = "../../cycles/folder"; +var linkPath1b = path.join(fixturesDir, "nested-index", 'one', 'symlink1-dir'); +try {fs.unlinkSync(linkPath1b);}catch(e){} +fs.symlinkSync(linkData1b, linkPath1b); + +var linkData2b = "../one/symlink1-dir"; +var linkPath2b = path.join(fixturesDir, "nested-index", 'two', 'symlink12-dir'); +try {fs.unlinkSync(linkPath2b);}catch(e){} +fs.symlinkSync(linkData2b, linkPath2b); + +assert.equal(fs.realpathSync(linkPath2), dstPath); +assert.equal(fs.realpathSync(linkPath2b), dstPath_b); + +async_expected++; +fs.realpath(linkPath2, function(err, rpath) { + if (err) throw err; + assert.equal(rpath, dstPath); + async_completed++; +}); + +async_expected++; +fs.realpath(linkPath2b, function(err, rpath) { + if (err) throw err; + assert.equal(rpath, dstPath_b); + async_completed++; +}); + +// todo: test shallow symlinks (file & dir) +// todo: test non-symlinks (file & dir) +// todo: test error on cyclic symlinks + +process.addListener("exit", function () { + try {fs.unlinkSync(linkPath1);}catch(e){} + try {fs.unlinkSync(linkPath2);}catch(e){} + try {fs.unlinkSync(linkPath1b);}catch(e){} + try {fs.unlinkSync(linkPath2b);}catch(e){} + assert.equal(async_completed, async_expected); +});