diff --git a/README.md b/README.md index df15f8ff..73c34be9 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,10 @@ Methods - [ensureFileSync](#ensurefilefile-callback) - [ensureDir](#ensuredirdir-callback) - [ensureDirSync](#ensuredirdir-callback) +- [ensureLink](#ensurelinksrcpath-dstpath-callback) +- [ensureLinkSync](#ensurelinksrcpath-dstpath-callback) +- [ensureSymlink](#ensuresymlinksrcpath-dstpath-type-callback) +- [ensureSymlinkSync](#ensuresymlinksrcpath-dstpath-type-callback) - [mkdirs](#mkdirsdir-callback) - [mkdirsSync](#mkdirsdir-callback) - [move](#movesrc-dest-options-callback) @@ -137,7 +141,6 @@ word `output` to denote that if the containing directory does not exist, it shou better succinct nomenclature for these methods, please open an issue for discussion. Thanks. - ### emptyDir(dir, [callback]) Ensures that a directory is empty. If the directory does not exist, it is created. The directory itself is not deleted. @@ -158,7 +161,6 @@ fs.emptyDir('/tmp/some/dir', function (err) { ``` - ### ensureFile(file, callback) Ensures that the file exists. If the file that is requested to be created is in directories that do not exist, these directories are created. If the file already exists, it is **NOT MODIFIED**. @@ -201,6 +203,47 @@ fs.ensureDir(dir, function (err) { ``` +### ensureLink(srcpath, dstpath, callback) + +Ensures that the link exists. If the directory structure does not exist, it is created. + +Sync: `ensureLinkSync()` + + +Example: + +```js +var fs = require('fs-extra') + +var srcpath = '/tmp/file.txt' +var dstpath = '/tmp/this/path/does/not/exist/file.txt' +fs.ensureLink(srcpath, dstpath, function (err) { + console.log(err) // => null + // link has now been created, including the directory it is to be placed in +}) +``` + + +### ensureSymlink(srcpath, dstpath, [type], callback) + +Ensures that the symlink exists. If the directory structure does not exist, it is created. + +Sync: `ensureSymlinkSync()` + + +Example: + +```js +var fs = require('fs-extra') + +var srcpath = '/tmp/file.txt' +var dstpath = '/tmp/this/path/does/not/exist/file.txt' +fs.ensureSymlink(srcpath, dstpath, function (err) { + console.log(err) // => null + // symlink has now been created, including the directory it is to be placed in +}) +``` + ### mkdirs(dir, callback) @@ -473,11 +516,3 @@ Copyright (c) 2011-2015 [JP Richardson](https://github.com/jprichardson) [jsonfile]: https://github.com/jprichardson/node-jsonfile - - - - - - - - diff --git a/lib/ensure/__tests__/link.test.js b/lib/ensure/__tests__/link.test.js new file mode 100644 index 00000000..e482a846 --- /dev/null +++ b/lib/ensure/__tests__/link.test.js @@ -0,0 +1,195 @@ +var util = require('util') +var path = require('path') +var chai = require('chai') +var os = require('os') +var fs = require('graceful-fs') +var expect = chai.expect +var CWD = process.cwd() +var fse = require(CWD) +var ensureLink = fse.ensureLink +var ensureLinkSync = fse.ensureLinkSync + +/* global afterEach, beforeEach, describe, it, after, before */ + +var TEST_DIR +var TESTS + +describe('fse-ensure-link', function () { + TEST_DIR = path.join(os.tmpdir(), 'fs-extra', 'ensure-symlink') + + var linkErrorString = 'ENOENT: no such file or directory, link \'%s\' -> \'%s\'' + + TESTS = { + native: { + fileSuccess: [ // creates working symlinks + // srcpath, dstpath, fileContent + ['./foo.txt', './link.txt', 'foo\n'], + // ['../foo.txt', './empty-dir/link.txt', 'foo\n'], + // the following uses `./dir-foo/foo.txt` not `foo.txt` & passes + ['./foo.txt', './dir-foo/link.txt', 'foo\n'], + ['./foo.txt', './empty-dir/link.txt', 'foo\n'], + ['./foo.txt', './real-alpha/link.txt', 'foo\n'], + ['./foo.txt', './real-alpha/real-beta/link.txt', 'foo\n'], + ['./foo.txt', './real-alpha/real-beta/real-gamma/link.txt', 'foo\n'] + ], + fileError: [ + ['./missing.txt', './link.txt', util.format(linkErrorString, './missing.txt', './link.txt')], + // native doesn't ensure directories + // (if this did work these symlinks would be broken shown above) + [path.resolve(path.join(TEST_DIR, './alpha/link.txt')), './link.txt', util.format(linkErrorString, path.resolve(path.join(TEST_DIR, './alpha/link.txt')), './link.txt')], + [path.resolve(path.join(TEST_DIR, './alpha/beta/link.txt')), './link.txt', util.format(linkErrorString, path.resolve(path.join(TEST_DIR, './alpha/beta/link.txt')), './link.txt')], + [path.resolve(path.join(TEST_DIR, './alpha/beta/gamma/link.txt')), './link.txt', util.format(linkErrorString, path.resolve(path.join(TEST_DIR, './alpha/beta/gamma/link.txt')), './link.txt')] + ] + }, + ensure: { + fileSuccess: [ // creates working symlinks + // srcpath, dstpath, fileContent + ['./foo.txt', './link.txt', 'foo\n'], + // ['../foo.txt', './empty-dir/link.txt', 'foo\n'], + ['./foo.txt', './dir-foo/link.txt', 'foo\n'], + // the following can't be handled by native + // symlinkPath pulls the file relative and supplies + // ensureSymlink with correct file note the content + ['./foo.txt', './empty-dir/link.txt', 'foo\n'], + ['./foo.txt', './real-alpha/link.txt', 'foo\n'], + ['./foo.txt', './real-alpha/real-beta/real-link.txt', 'foo\n'], + ['./foo.txt', './real-alpha/real-beta/real-gamma/real-link.txt', 'foo\n'], + // the following shows that nested directores are created (with a relative-to-CWD-srcpath) + ['./foo.txt', './alpha/link.txt', 'foo\n'], + ['./foo.txt', './alpha/beta/link.txt', 'foo\n'], + ['./foo.txt', './alpha/beta/gamma/link.txt', 'foo\n'] + // [path.resolve('./foo.txt'), './link.txt', 'foo\n'], + // [path.resolve('./dir-foo/foo.txt'), './link.txt', 'dir-foo\n'] + ], + fileError: [ + // srcpath, dstpath, message + ['./missing.txt', './link.txt', util.format(linkErrorString, './missing.txt', './link.txt')], + [path.resolve(path.join(TEST_DIR, './missing.txt')), './link.txt', util.format(linkErrorString, path.resolve(path.join(TEST_DIR, './missing.txt')), './link.txt')], + [path.resolve(path.join(TEST_DIR, '../foo.txt')), './link.txt', util.format(linkErrorString, path.resolve(path.join(TEST_DIR, '../foo.txt')), './link.txt')], + [path.resolve(path.join(TEST_DIR, '../dir-foo/foo.txt')), './link.txt', util.format(linkErrorString, path.resolve(path.join(TEST_DIR, '../dir-foo/foo.txt')), './link.txt')] + ] + } + } + + before(function () { + fse.emptyDirSync(TEST_DIR) + process.chdir(TEST_DIR) + }) + + beforeEach(function () { + fs.writeFileSync('./foo.txt', 'foo\n') + fse.mkdirsSync('empty-dir') + fse.mkdirsSync('dir-foo') + fs.writeFileSync('dir-foo/foo.txt', 'dir-foo\n') + fse.mkdirsSync('dir-bar') + fs.writeFileSync('dir-bar/bar.txt', 'dir-bar\n') + fse.mkdirsSync('real-alpha/real-beta/real-gamma') + }) + + afterEach(function (done) { + fse.emptyDir(TEST_DIR, done) + }) + + after(function () { + process.chdir(CWD) + fse.removeSync(TEST_DIR) + }) + + function fileSuccess (tests, fn, signature) { + tests.forEach(function (test) { + var srcpath = test[0] + var dstpath = test[1] + var fileContent = test[2] + var should = util.format('should create link using src `%s` and dst `%s`', srcpath, dstpath) + it(should, function (done) { + var callback = function (err) { + if (err) return done(err) + // the symlink exists in the file system + var dstDir = path.dirname(dstpath) + var dstBasename = path.basename(dstpath) + expect(fs.readdirSync(dstDir)).to.contain(dstBasename) + // only works if it's a symlink file + expect(fs.lstatSync(dstpath).isFile()).to.equal(true) + // the symlink is unreadable + expect(fs.readFileSync(dstpath, 'utf8')).to.equal(fileContent) + return done() + } + var _signature = (signature) ? signature(srcpath, dstpath, callback) : [srcpath, dstpath, callback] + return fn.apply(null, _signature) + }) + }) + } + + function fileError (tests, fn, signature) { + tests.forEach(function (test) { + var srcpath = test[0] + var dstpath = test[1] + var errorMessage = test[2] + var should = util.format('should return error when creating link using src `%s` and dst `%s`', srcpath, dstpath) + it(should, function (done) { + var callback = function (err) { + expect(err).to.have.property('message') + expect(err.message).to.equal(errorMessage) + return done() + } + var _signature = (signature) ? signature(srcpath, dstpath, callback) : [srcpath, dstpath, callback] + return fn.apply(null, _signature) + }) + }) + } + + function fileSuccessSync (tests, fn, signature) { + tests.forEach(function (test) { + var srcpath = test[0] + var dstpath = test[1] + var fileContent = test[2] + var should = util.format('should create link using src `%s` and dst `%s`', srcpath, dstpath) + it(should, function () { + var _signature = (signature) ? signature(srcpath, dstpath) : [srcpath, dstpath] + fn.apply(null, _signature) + var dstDir = path.dirname(dstpath) + var dstBasename = path.basename(dstpath) + expect(fs.readdirSync(dstDir)).to.contain(dstBasename) + // only works if it's a symlink file + expect(fs.lstatSync(dstpath).isFile()).to.equal(true) + // the symlink is unreadable + expect(fs.readFileSync(dstpath, 'utf8')).to.equal(fileContent) + }) + }) + } + + function fileErrorSync (tests, fn, signature) { + tests.forEach(function (test) { + var srcpath = test[0] + var dstpath = test[1] + var should = util.format('should throw error using` src `%s` and dst `%s`', srcpath, dstpath) + it(should, function () { + var _signature = (signature) ? signature(srcpath, dstpath) : [srcpath, dstpath] + expect(function () { + return fn.apply(null, _signature) + }).to.throw + }) + }) + } + + describe('fs.link()', function () { + fileSuccess(TESTS.native.fileSuccess, fs.link) + fileError(TESTS.native.fileError, fs.link) + }) + + describe('ensureLink()', function () { + fileSuccess(TESTS.ensure.fileSuccess, ensureLink) + fileError(TESTS.ensure.fileError, ensureLink) + }) + + describe('fs.linkSync()', function () { + fileSuccessSync(TESTS.native.fileSuccess, fs.linkSync) + fileErrorSync(TESTS.native.fileError, fs.linkSync) + }) + + describe('ensureLinkSync()', function () { + fileSuccessSync(TESTS.ensure.fileSuccess, ensureLinkSync) + fileErrorSync(TESTS.ensure.fileError, ensureLinkSync) + }) + +}) diff --git a/lib/ensure/__tests__/symlink-paths.test.js b/lib/ensure/__tests__/symlink-paths.test.js new file mode 100644 index 00000000..b87d80f6 --- /dev/null +++ b/lib/ensure/__tests__/symlink-paths.test.js @@ -0,0 +1,86 @@ +var util = require('util') +var path = require('path') +var chai = require('chai') +var os = require('os') +var fs = require('graceful-fs') +var expect = chai.expect +var CWD = process.cwd() +var fse = require(CWD) +var _symlinkPaths = require('../symlink-paths') +var symlinkPaths = _symlinkPaths.symlinkPaths +var symlinkPathsSync = _symlinkPaths.symlinkPathsSync +var TEST_DIR = path.join(os.tmpdir(), 'fs-extra', 'ensure-symlink') + +/* global afterEach, beforeEach, describe, it, after, before */ + +describe('symlink-type', function () { + + before(function () { + fse.emptyDirSync(TEST_DIR) + process.chdir(TEST_DIR) + }) + + beforeEach(function () { + fs.writeFileSync('./foo.txt', 'foo\n') + fse.mkdirsSync('./empty-dir') + fse.mkdirsSync('./dir-foo') + fs.writeFileSync('./dir-foo/foo.txt', 'dir-foo\n') + fse.mkdirsSync('./dir-bar') + fs.writeFileSync('./dir-bar/bar.txt', 'dir-bar\n') + fse.mkdirsSync('./real-alpha/real-beta/real-gamma') + }) + + afterEach(function (done) { + fse.emptyDir(TEST_DIR, done) + }) + + after(function () { + process.chdir(CWD) + fse.removeSync(TEST_DIR) + }) + + var tests = { + success: [ + [['foo.txt', 'symlink.txt'], {toCwd: 'foo.txt', toDst: 'foo.txt'}], // smart && nodestyle + [['foo.txt', 'empty-dir/symlink.txt'], {toCwd: 'foo.txt', toDst: '../foo.txt'}], // smart + [['../foo.txt', 'empty-dir/symlink.txt'], {toCwd: 'foo.txt', toDst: '../foo.txt'}], // nodestyle + [['foo.txt', 'dir-bar/symlink.txt'], {toCwd: 'foo.txt', toDst: '../foo.txt'}], // smart + [['../foo.txt', 'dir-bar/symlink.txt'], {toCwd: 'foo.txt', toDst: '../foo.txt'}], // nodestyle + // this is to preserve node's symlink capability these arguments say create + // a link to 'dir-foo/foo.txt' this works because it exists this is unlike + // the previous example with 'empty-dir' because 'empty-dir/foo.txt' does not exist. + [['foo.txt', 'dir-foo/symlink.txt' ], {toCwd: 'dir-foo/foo.txt', toDst: 'foo.txt'}], // nodestyle + [['foo.txt', 'real-alpha/real-beta/real-gamma/symlink.txt' ], {toCwd: 'foo.txt', toDst: '../../../foo.txt'}] + ] + } + + describe('symlinkPaths()', function () { + tests.success.forEach(function (test) { + var args = test[0].slice(0) + var expectedType = test[1] + var should = util.format('should return \'%s\' when src \'%s\' and dst is \'%s\'', JSON.stringify(expectedType), args[0], args[1]) + it(should, function (done) { + var callback = function (err, type) { + if (err) done(err) + expect(type).to.deep.equal(expectedType) + done() + } + args.push(callback) + return symlinkPaths.apply(null, args) + }) + }) + }) + + describe('symlinkPathsSync()', function () { + tests.success.forEach(function (test) { + var args = test[0].slice(0) + var expectedType = test[1] + var should = util.format('should return \'%s\' when src \'%s\' and dst is \'%s\'', JSON.stringify(expectedType), args[0], args[1]) + it(should, function () { + var value = symlinkPathsSync.apply(null, args) + expect(value).to.deep.equal(expectedType) + }) + }) + }) + +}) diff --git a/lib/ensure/__tests__/symlink-type.test.js b/lib/ensure/__tests__/symlink-type.test.js new file mode 100644 index 00000000..b92c1c05 --- /dev/null +++ b/lib/ensure/__tests__/symlink-type.test.js @@ -0,0 +1,125 @@ +var util = require('util') +var path = require('path') +var chai = require('chai') +var os = require('os') +var fs = require('graceful-fs') +var expect = chai.expect +var CWD = process.cwd() +var fse = require(CWD) +var _symlinkType = require('../symlink-type') +var symlinkType = _symlinkType.symlinkType +var symlinkTypeSync = _symlinkType.symlinkTypeSync +var TEST_DIR = path.join(os.tmpdir(), 'fs-extra', 'ensure-symlink') + +/* global afterEach, beforeEach, describe, it, after, before */ + +describe('symlink-type', function () { + + before(function () { + fse.emptyDirSync(TEST_DIR) + process.chdir(TEST_DIR) + }) + + beforeEach(function () { + fs.writeFileSync('./foo.txt', 'foo\n') + fse.mkdirsSync('./empty-dir') + fse.mkdirsSync('./dir-foo') + fs.writeFileSync('./dir-foo/foo.txt', 'dir-foo\n') + fse.mkdirsSync('./dir-bar') + fs.writeFileSync('./dir-bar/bar.txt', 'dir-bar\n') + fse.mkdirsSync('./real-alpha/real-beta/real-gamma') + }) + + afterEach(function (done) { + fse.emptyDir(TEST_DIR, done) + }) + + after(function () { + process.chdir(CWD) + fse.removeSync(TEST_DIR) + }) + + var tests = { + success: [ + // [{arguments} [srcpath, dirpath, [type] , result] + // smart file type checking + [['./foo.txt'], 'file'], + [['./empty-dir'], 'dir'], + [['./dir-foo/foo.txt'], 'file'], + [['./dir-bar'], 'dir'], + [['./dir-bar/bar.txt'], 'file'], + [['./real-alpha/real-beta/real-gamma'], 'dir'], + // force dir + [['./foo.txt', 'dir'], 'dir'], + [['./empty-dir', 'dir'], 'dir'], + [['./dir-foo/foo.txt', 'dir'], 'dir'], + [['./dir-bar', 'dir'], 'dir'], + [['./dir-bar/bar.txt', 'dir'], 'dir'], + [['./real-alpha/real-beta/real-gamma', 'dir'], 'dir'], + // force file + [['./foo.txt', 'file'], 'file'], + [['./empty-dir', 'file'], 'file'], + [['./dir-foo/foo.txt', 'file'], 'file'], + [['./dir-bar', 'file'], 'file'], + [['./dir-bar/bar.txt', 'file'], 'file'], + [['./real-alpha/real-beta/real-gamma', 'file'], 'file'], + // default for files or dirs that don't exist is file + [['./missing.txt'], 'file'], + [['./missing'], 'file'], + [['./missing.txt'], 'file'], + [['./missing'], 'file'], + [['./empty-dir/missing.txt'], 'file'], + [['./empty-dir/missing'], 'file'], + [['./empty-dir/missing.txt'], 'file'], + [['./empty-dir/missing'], 'file'], + // when src doesnt exist and provided type 'file' + [['./missing.txt', 'file'], 'file'], + [['./missing', 'file'], 'file'], + [['./missing.txt', 'file'], 'file'], + [['./missing', 'file'], 'file'], + [['./empty-dir/missing.txt', 'file'], 'file'], + [['./empty-dir/missing', 'file'], 'file'], + [['./empty-dir/missing.txt', 'file'], 'file'], + [['./empty-dir/missing', 'file'], 'file'], + // when src doesnt exist and provided type 'dir' + [['./missing.txt', 'dir'], 'dir'], + [['./missing', 'dir'], 'dir'], + [['./missing.txt', 'dir'], 'dir'], + [['./missing', 'dir'], 'dir'], + [['./empty-dir/missing.txt', 'dir'], 'dir'], + [['./empty-dir/missing', 'dir'], 'dir'], + [['./empty-dir/missing.txt', 'dir'], 'dir'], + [['./empty-dir/missing', 'dir'], 'dir'] + ] + } + + describe('symlinkType()', function () { + tests.success.forEach(function (test) { + var args = test[0].slice(0) + var expectedType = test[1] + var should = util.format('should return \'%s\' when src \'%s\'', expectedType, args[0]) + it(should, function (done) { + var callback = function (err, type) { + if (err) done(err) + expect(type).to.equal(expectedType) + done() + } + args.push(callback) + return symlinkType.apply(null, args) + }) + }) + }) + + describe('symlinkTypeSync()', function () { + tests.success.forEach(function (test) { + var args = test[0] + var expectedType = test[1] + var should = util.format('should return \'%s\' when src \'%s\'', expectedType, args[0]) + it(should, function () { + var value = symlinkTypeSync.apply(null, args) + expect(value).to.equal(expectedType) + }) + }) + }) + +}) diff --git a/lib/ensure/__tests__/symlink.test.js b/lib/ensure/__tests__/symlink.test.js new file mode 100644 index 00000000..70b8bbfb --- /dev/null +++ b/lib/ensure/__tests__/symlink.test.js @@ -0,0 +1,341 @@ +var util = require('util') +var path = require('path') +var chai = require('chai') +var os = require('os') +var fs = require('graceful-fs') +var expect = chai.expect +var CWD = process.cwd() +var fse = require(CWD) + +var _symlinkPaths = require('../symlink-paths') +var symlinkPathsSync = _symlinkPaths.symlinkPathsSync + +var ensureSymlink = fse.ensureSymlink +var ensureSymlinkSync = fse.ensureSymlinkSync + +/* global afterEach, beforeEach, describe, it, after, before */ + +var TEST_DIR + +describe('fse-ensure-symlink', function () { + TEST_DIR = path.join(os.tmpdir(), 'fs-extra', 'ensure-symlink') + + var tests = [ + // [[srcpath, dstpath], fs.symlink expect, fse.ensureSymlink expect] + [['./foo.txt', './symlink.txt'], 'file-success', 'file-success'], + [['../foo.txt', './empty-dir/symlink.txt'], 'file-success', 'file-success'], + [['../foo.txt', './empty-dir/symlink.txt'], 'file-success', 'file-success'], + [['./foo.txt', './dir-foo/symlink.txt'], 'file-success', 'file-success'], + [['./foo.txt', './empty-dir/symlink.txt'], 'file-broken', 'file-success'], + [['./foo.txt', './real-alpha/symlink.txt'], 'file-broken', 'file-success'], + [['./foo.txt', './real-alpha/real-beta/symlink.txt'], 'file-broken', 'file-success'], + [['./foo.txt', './real-alpha/real-beta/real-gamma/symlink.txt'], 'file-broken', 'file-success'], + [['./foo.txt', './alpha/symlink.txt'], 'file-error', 'file-success'], + [['./foo.txt', './alpha/beta/symlink.txt'], 'file-error', 'file-success'], + [['./foo.txt', './alpha/beta/gamma/symlink.txt'], 'file-error', 'file-success'], + [['./missing.txt', './symlink.txt'], 'file-broken', 'file-error'], + [['./dir-foo', './symlink-dir-foo'], 'dir-success', 'dir-success'], + [['../dir-bar', './dir-foo/symlink-dir-bar'], 'dir-success', 'dir-success'], + [['./dir-bar', './dir-foo/symlink-dir-bar'], 'dir-broken', 'dir-success'], + [['./dir-bar', './empty-dir/symlink-dir-bar'], 'dir-broken', 'dir-success'], + [['./dir-bar', './real-alpha/symlink-dir-bar'], 'dir-broken', 'dir-success'], + [['./dir-bar', './real-alpha/real-beta/symlink-dir-bar'], 'dir-broken', 'dir-success'], + [['./dir-bar', './real-alpha/real-beta/real-gamma/symlink-dir-bar'], 'dir-broken', 'dir-success'], + [['./dir-foo', './alpha/dir-foo'], 'dir-error', 'dir-success'], + [['./dir-foo', './alpha/beta/dir-foo'], 'dir-error', 'dir-success'], + [['./dir-foo', './alpha/beta/gamma/dir-foo'], 'dir-error', 'dir-success'], + [['./missing', './dir-foo/symlink-dir-missing'], 'dir-broken', 'dir-error'], + [[path.resolve(path.join(TEST_DIR, './foo.txt')), './symlink.txt'], 'file-success', 'file-success'], + [[path.resolve(path.join(TEST_DIR, './dir-foo/foo.txt')), './symlink.txt'], 'file-success', 'file-success'], + [[path.resolve(path.join(TEST_DIR, './missing.txt')), './symlink.txt'], 'file-broken', 'file-error'], + [[path.resolve(path.join(TEST_DIR, '../foo.txt')), './symlink.txt'], 'file-broken', 'file-error'], + [[path.resolve(path.join(TEST_DIR, '../dir-foo/foo.txt')), './symlink.txt'], 'file-broken', 'file-error'] + ] + + before(function () { + fse.emptyDirSync(TEST_DIR) + process.chdir(TEST_DIR) + }) + + beforeEach(function () { + fs.writeFileSync('./foo.txt', 'foo\n') + fse.mkdirsSync('empty-dir') + fse.mkdirsSync('dir-foo') + fs.writeFileSync('dir-foo/foo.txt', 'dir-foo\n') + fse.mkdirsSync('dir-bar') + fs.writeFileSync('dir-bar/bar.txt', 'dir-bar\n') + fse.mkdirsSync('real-alpha/real-beta/real-gamma') + }) + + afterEach(function (done) { + fse.emptyDir(TEST_DIR, done) + }) + + after(function () { + process.chdir(CWD) + fse.removeSync(TEST_DIR) + }) + + function fileSuccess (args, fn) { + var srcpath = args[0] + var dstpath = args[1] + var should = util.format('should create symlink file using src `%s` and dst `%s`', srcpath, dstpath) + it(should, function (done) { + var callback = function (err) { + if (err) return done(err) + var relative = symlinkPathsSync(srcpath, dstpath) + var fileContent = fs.readFileSync(relative.toCwd, 'utf8') + var dstDir = path.dirname(dstpath) + var dstBasename = path.basename(dstpath) + expect(fs.readdirSync(dstDir)).to.contain(dstBasename) + expect(fs.lstatSync(dstpath).isSymbolicLink()).to.equal(true) + expect(fs.readFileSync(dstpath, 'utf8')).to.equal(fileContent) + return done() + } + args.push(callback) + return fn.apply(null, args) + }) + } + + function fileBroken (args, fn) { + var srcpath = args[0] + var dstpath = args[1] + var should = util.format('should create broken symlink file using src `%s` and dst `%s`', srcpath, dstpath) + it(should, function (done) { + var callback = function (err) { + if (err) return done(err) + var dstDir = path.dirname(dstpath) + var dstBasename = path.basename(dstpath) + expect(fs.readdirSync(dstDir)).to.contain(dstBasename) + expect(fs.lstatSync(dstpath).isSymbolicLink()).to.equal(true) + expect(function () { + return fs.readFileSync(dstpath, 'utf8') + }).to.throw + return done() + } + args.push(callback) + return fn.apply(null, args) + }) + } + + function fileError (args, fn) { + var srcpath = args[0] + var dstpath = args[1] + var should = util.format('should return error when creating symlink file using src `%s` and dst `%s`', srcpath, dstpath) + it(should, function (done) { + var callback = function (err) { + expect(err).to.be.instanceof(Error) + return done() + } + args.push(callback) + return fn.apply(null, args) + }) + } + + function dirSuccess (args, fn) { + var srcpath = args[0] + var dstpath = args[1] + var should = util.format('should create symlink dir using src `%s` and dst `%s`', srcpath, dstpath) + it(should, function (done) { + var callback = function (err) { + if (err) return done(err) + var relative = symlinkPathsSync(srcpath, dstpath) + var dstDir = path.dirname(dstpath) + var dstBasename = path.basename(dstpath) + var dstpathContents = fs.readdirSync(dstpath) + var srcpathContents = fs.readdirSync(relative.toCwd) + expect(fs.readdirSync(dstDir)).to.contain(dstBasename) + expect(fs.lstatSync(dstpath).isSymbolicLink()).to.equal(true) + expect(dstpathContents).to.deep.equal(srcpathContents) + return done() + } + args.push(callback) + return fn.apply(null, args) + }) + } + + function dirBroken (args, fn) { + var srcpath = args[0] + var dstpath = args[1] + var should = util.format('should create broken symlink dir using src `%s` and dst `%s`', srcpath, dstpath) + it(should, function (done) { + var callback = function (err) { + if (err) return done(err) + var dstDir = path.dirname(dstpath) + var dstBasename = path.basename(dstpath) + expect(fs.lstatSync(dstpath).isSymbolicLink()).to.equal(true) + expect(fs.readdirSync(dstDir)).to.contain(dstBasename) + expect(function () { + return fs.readdirSync(dstpath) + }).to.throw + return done() + } + args.push(callback) + return fn.apply(null, args) + }) + } + + function dirError (args, fn) { + var srcpath = args[0] + var dstpath = args[1] + var should = util.format('should return error when creating symlink dir using src `%s` and dst `%s`', srcpath, dstpath) + it(should, function (done) { + var callback = function (err) { + expect(err).to.be.instanceof(Error) + return done() + } + args.push(callback) + return fn.apply(null, args) + }) + } + + function fileSuccessSync (args, fn) { + var srcpath = args[0] + var dstpath = args[1] + var should = util.format('should create symlink file using src `%s` and dst `%s`', srcpath, dstpath) + it(should, function () { + fn.apply(null, args) + var relative = symlinkPathsSync(srcpath, dstpath) + var fileContent = fs.readFileSync(relative.toCwd, 'utf8') + var dstDir = path.dirname(dstpath) + var dstBasename = path.basename(dstpath) + expect(fs.readdirSync(dstDir)).to.contain(dstBasename) + expect(fs.lstatSync(dstpath).isSymbolicLink()).to.equal(true) + expect(fs.readFileSync(dstpath, 'utf8')).to.equal(fileContent) + }) + } + + function fileBrokenSync (args, fn) { + var srcpath = args[0] + var dstpath = args[1] + var should = util.format('should create broken symlink file using src `%s` and dst `%s`', srcpath, dstpath) + it(should, function () { + fn.apply(null, args) + var dstDir = path.dirname(dstpath) + var dstBasename = path.basename(dstpath) + expect(fs.readdirSync(dstDir)).to.contain(dstBasename) + expect(fs.lstatSync(dstpath).isSymbolicLink()).to.equal(true) + expect(function () { + return fs.readFileSync(dstpath, 'utf8') + }).to.throw + }) + } + + function fileErrorSync (args, fn) { + var srcpath = args[0] + var dstpath = args[1] + var should = util.format('should throw error using` src `%s` and dst `%s`', srcpath, dstpath) + it(should, function () { + expect(function () { + return fn.apply(null, args) + }).to.throw + }) + } + + function dirSuccessSync (args, fn) { + var srcpath = args[0] + var dstpath = args[1] + var should = util.format('should create symlink dir using src `%s` and dst `%s`', srcpath, dstpath) + it(should, function () { + fn.apply(null, args) + var relative = symlinkPathsSync(srcpath, dstpath) + var dstDir = path.dirname(dstpath) + var dstBasename = path.basename(dstpath) + var dstpathContents = fs.readdirSync(dstpath) + var srcpathContents = fs.readdirSync(relative.toCwd) + expect(fs.readdirSync(dstDir)).to.contain(dstBasename) + expect(fs.lstatSync(dstpath).isSymbolicLink()).to.equal(true) + expect(dstpathContents).to.deep.equal(srcpathContents) + }) + } + + function dirBrokenSync (args, fn) { + var srcpath = args[0] + var dstpath = args[1] + var should = util.format('should create broken symlink dir using src `%s` and dst `%s`', srcpath, dstpath) + it(should, function () { + fn.apply(null, args) + var dstDir = path.dirname(dstpath) + var dstBasename = path.basename(dstpath) + expect(fs.readdirSync(dstDir)).to.contain(dstBasename) + expect(fs.lstatSync(dstpath).isSymbolicLink()).to.equal(true) + expect(function () { + return fs.readdirSync(dstpath, 'utf8') + }).to.throw + }) + } + + function dirErrorSync (args, fn) { + var srcpath = args[0] + var dstpath = args[1] + var should = util.format('should throw error when creating symlink dir using src `%s` and dst `%s`', srcpath, dstpath) + it(should, function () { + expect(function () { + return fn.apply(null, args) + }).to.throw + }) + } + + describe('fs.symlink()', function () { + var fn = fs.symlink + tests.forEach(function (test) { + var args = test[0].slice(0) + var nativeBehavior = test[1] + // var newBehavior = test[2] + if (nativeBehavior === 'file-success') fileSuccess(args, fn) + if (nativeBehavior === 'file-broken') fileBroken(args, fn) + if (nativeBehavior === 'file-error') fileError(args, fn) + args.push('dir') + if (nativeBehavior === 'dir-success') dirSuccess(args, fn) + if (nativeBehavior === 'dir-broken') dirBroken(args, fn) + if (nativeBehavior === 'dir-error') dirError(args, fn) + }) + }) + + describe('ensureSymlink()', function () { + var fn = ensureSymlink + tests.forEach(function (test) { + var args = test[0] + // var nativeBehavior = test[1] + var newBehavior = test[2] + if (newBehavior === 'file-success') fileSuccess(args, fn) + if (newBehavior === 'file-broken') fileBroken(args, fn) + if (newBehavior === 'file-error') fileError(args, fn) + if (newBehavior === 'dir-success') dirSuccess(args, fn) + if (newBehavior === 'dir-broken') dirBroken(args, fn) + if (newBehavior === 'dir-error') dirError(args, fn) + }) + }) + + describe('fs.symlinkSync()', function () { + var fn = fs.symlinkSync + tests.forEach(function (test) { + var args = test[0].slice(0) + var nativeBehavior = test[1] + // var newBehavior = test[2] + if (nativeBehavior === 'file-success') fileSuccessSync(args, fn) + if (nativeBehavior === 'file-broken') fileBrokenSync(args, fn) + if (nativeBehavior === 'file-error') fileErrorSync(args, fn) + args.push('dir') + if (nativeBehavior === 'dir-success') dirSuccessSync(args, fn) + if (nativeBehavior === 'dir-broken') dirBrokenSync(args, fn) + if (nativeBehavior === 'dir-error') dirErrorSync(args, fn) + }) + }) + + describe('ensureSymlinkSync()', function () { + var fn = ensureSymlinkSync + tests.forEach(function (test) { + var args = test[0] + // var nativeBehavior = test[1] + var newBehavior = test[2] + if (newBehavior === 'file-success') fileSuccessSync(args, fn) + if (newBehavior === 'file-broken') fileBrokenSync(args, fn) + if (newBehavior === 'file-error') fileErrorSync(args, fn) + if (newBehavior === 'dir-success') dirSuccessSync(args, fn) + if (newBehavior === 'dir-broken') dirBrokenSync(args, fn) + if (newBehavior === 'dir-error') dirErrorSync(args, fn) + }) + }) + +}) diff --git a/lib/ensure/file.js b/lib/ensure/file.js new file mode 100644 index 00000000..1c9c2de0 --- /dev/null +++ b/lib/ensure/file.js @@ -0,0 +1,43 @@ +var path = require('path') +var fs = require('graceful-fs') +var mkdir = require('../mkdirs') + +function createFile (file, callback) { + function makeFile () { + fs.writeFile(file, '', function (err) { + if (err) return callback(err) + callback() + }) + } + + fs.exists(file, function (fileExists) { + if (fileExists) return callback() + var dir = path.dirname(file) + fs.exists(dir, function (dirExists) { + if (dirExists) return makeFile() + mkdir.mkdirs(dir, function (err) { + if (err) return callback(err) + makeFile() + }) + }) + }) +} + +function createFileSync (file) { + if (fs.existsSync(file)) return + + var dir = path.dirname(file) + if (!fs.existsSync(dir)) { + mkdir.mkdirsSync(dir) + } + + fs.writeFileSync(file, '') +} + +module.exports = { + createFile: createFile, + createFileSync: createFileSync, + // alias + ensureFile: createFile, + ensureFileSync: createFileSync +} diff --git a/lib/ensure/index.js b/lib/ensure/index.js index 1c9c2de0..26e8705a 100644 --- a/lib/ensure/index.js +++ b/lib/ensure/index.js @@ -1,43 +1,21 @@ -var path = require('path') -var fs = require('graceful-fs') -var mkdir = require('../mkdirs') - -function createFile (file, callback) { - function makeFile () { - fs.writeFile(file, '', function (err) { - if (err) return callback(err) - callback() - }) - } - - fs.exists(file, function (fileExists) { - if (fileExists) return callback() - var dir = path.dirname(file) - fs.exists(dir, function (dirExists) { - if (dirExists) return makeFile() - mkdir.mkdirs(dir, function (err) { - if (err) return callback(err) - makeFile() - }) - }) - }) -} - -function createFileSync (file) { - if (fs.existsSync(file)) return - - var dir = path.dirname(file) - if (!fs.existsSync(dir)) { - mkdir.mkdirsSync(dir) - } - - fs.writeFileSync(file, '') -} +var file = require('./file') +var link = require('./link') +var symlink = require('./symlink') module.exports = { - createFile: createFile, - createFileSync: createFileSync, - // alias - ensureFile: createFile, - ensureFileSync: createFileSync + // file + createFile: file.createFile, + createFileSync: file.createFileSync, + ensureFile: file.createFile, + ensureFileSync: file.createFileSync, + // link + createLink: link.createLink, + createLinkSync: link.createLinkSync, + ensureLink: link.createLink, + ensureLinkSync: link.createLinkSync, + // symlink + createSymlink: symlink.createSymlink, + createSymlinkSync: symlink.createSymlinkSync, + ensureSymlink: symlink.createSymlink, + ensureSymlinkSync: symlink.createSymlinkSync } diff --git a/lib/ensure/link.js b/lib/ensure/link.js new file mode 100644 index 00000000..12edbda2 --- /dev/null +++ b/lib/ensure/link.js @@ -0,0 +1,37 @@ +var path = require('path') +var fs = require('graceful-fs') +var mkdir = require('../mkdirs') + +function createLink (srcpath, dstpath, callback) { + function makeLink (srcpath, dstpath) { + fs.link(srcpath, dstpath, function (err) { + if (err) return callback(err) + callback(null) + }) + } + + var dir = path.dirname(dstpath) + fs.exists(dir, function (dirExists) { + if (dirExists) return makeLink(srcpath, dstpath) + mkdir.mkdirs(dir, function (err) { + if (err) return callback(err) + makeLink(srcpath, dstpath) + }) + }) +} + +function createLinkSync (srcpath, dstpath, callback) { + var dir = path.dirname(dstpath) + var dirExists = fs.existsSync(dir) + if (dirExists) return fs.linkSync(srcpath, dstpath) + mkdir.mkdirsSync(dir) + return fs.linkSync(srcpath, dstpath) +} + +module.exports = { + createLink: createLink, + createLinkSync: createLinkSync, + // alias + ensureLink: createLink, + ensureLinkSync: createLinkSync +} diff --git a/lib/ensure/symlink-paths.js b/lib/ensure/symlink-paths.js new file mode 100644 index 00000000..d112908e --- /dev/null +++ b/lib/ensure/symlink-paths.js @@ -0,0 +1,89 @@ +var path = require('path') +var fs = require('graceful-fs') + +/** + * Function that returns two types of paths, one relative to symlink, and one + * relative to the current working directory. Checks if path is absolute or + * relative. If the path is relative, this function checks if the path is + * relative to symlink or relative to current working directory. This is an + * initiative to find a smarter `srcpath` to supply when building symlinks. + * This allows you to determine which path to use out of one of three possible + * types of source paths. The first is an absolute path. This is detected by + * `path.isAbsolute()`. When an absolute path is provided, it is checked to + * see if it exists. If it does it's used, if not an error is returned + * (callback)/ thrown (sync). The other two options for `srcpath` are a + * relative url. By default Node's `fs.symlink` works by creating a symlink + * using `dstpath` and expects the `srcpath` to be relative to the newly + * created symlink. If you provide a `srcpath` that does not exist on the file + * system it results in a broken symlink. To minimize this, the function + * checks to see if the 'relative to symlink' source file exists, and if it + * does it will use it. If it does not, it checks if there's a file that + * exists that is relative to the current working directory, if does its used. + * This preserves the expectations of the original fs.symlink spec and adds + * the ability to pass in `relative to current working direcotry` paths. + */ + +function symlinkPaths (srcpath, dstpath, callback) { + if (path.isAbsolute(srcpath)) { + return fs.exists(srcpath, function (exists) { + if (!exists) return callback(new Error('absolute srcpath does not exist')) + return callback(null, { + 'toCwd': srcpath, + 'toDst': srcpath + }) + }) + } else { + var dstdir = path.dirname(dstpath) + var relativeToDst = path.join(dstdir, srcpath) + return fs.exists(relativeToDst, function (exists) { + if (exists) { + return callback(null, { + 'toCwd': relativeToDst, + 'toDst': srcpath + }) + } else { + return fs.exists(srcpath, function (exists) { + if (!exists) return callback(new Error('relative srcpath does not exist')) + return callback(null, { + 'toCwd': srcpath, + 'toDst': path.relative(dstdir, srcpath) + }) + }) + } + }) + } +} + +function symlinkPathsSync (srcpath, dstpath) { + var exists + if (path.isAbsolute(srcpath)) { + exists = fs.existsSync(srcpath) + if (!exists) throw new Error('absolute srcpath does not exist') + return { + 'toCwd': srcpath, + 'toDst': srcpath + } + } else { + var dstdir = path.dirname(dstpath) + var relativeToDst = path.join(dstdir, srcpath) + exists = fs.existsSync(relativeToDst) + if (exists) { + return { + 'toCwd': relativeToDst, + 'toDst': srcpath + } + } else { + exists = fs.existsSync(srcpath) + if (!exists) throw new Error('relative srcpath does not exist') + return { + 'toCwd': srcpath, + 'toDst': path.relative(dstdir, srcpath) + } + } + } +} + +module.exports = { + 'symlinkPaths': symlinkPaths, + 'symlinkPathsSync': symlinkPathsSync +} diff --git a/lib/ensure/symlink-type.js b/lib/ensure/symlink-type.js new file mode 100644 index 00000000..8bca23dd --- /dev/null +++ b/lib/ensure/symlink-type.js @@ -0,0 +1,27 @@ +var fs = require('graceful-fs') + +function symlinkType (srcpath, type, callback) { + callback = (typeof type === 'function') ? type : callback + type = (typeof type === 'function') ? false : type + if (type) return callback(null, type) + fs.lstat(srcpath, function (err, stats) { + if (err) return callback(null, 'file') + type = (stats && stats.isDirectory()) ? 'dir' : 'file' + callback(null, type) + }) +} + +function symlinkTypeSync (srcpath, type) { + if (type) return type + try { + var stats = fs.lstatSync(srcpath) + } catch(e) { + return 'file' + } + return (stats && stats.isDirectory()) ? 'dir' : 'file' +} + +module.exports = { + symlinkType: symlinkType, + symlinkTypeSync: symlinkTypeSync +} diff --git a/lib/ensure/symlink.js b/lib/ensure/symlink.js new file mode 100644 index 00000000..473385e9 --- /dev/null +++ b/lib/ensure/symlink.js @@ -0,0 +1,54 @@ +var path = require('path') +var fs = require('graceful-fs') +var _mkdirs = require('../mkdirs') +var mkdirs = _mkdirs.mkdirs +var mkdirsSync = _mkdirs.mkdirsSync + +var _symlinkPaths = require('./symlink-paths') +var symlinkPaths = _symlinkPaths.symlinkPaths +var symlinkPathsSync = _symlinkPaths.symlinkPathsSync + +var _symlinkType = require('./symlink-type') +var symlinkType = _symlinkType.symlinkType +var symlinkTypeSync = _symlinkType.symlinkTypeSync + +function createSymlink (srcpath, dstpath, type, callback) { + callback = (typeof type === 'function') ? type : callback + type = (typeof type === 'function') ? false : type + symlinkPaths(srcpath, dstpath, function (err, relative) { + if (err) return callback(err) + srcpath = relative.toDst + symlinkType(relative.toCwd, type, function (err, type) { + if (err) return callback(err) + var dir = path.dirname(dstpath) + fs.exists(dir, function (dirExists) { + if (dirExists) return fs.symlink(srcpath, dstpath, type, callback) + mkdirs(dir, function (err) { + if (err) return callback(err) + fs.symlink(srcpath, dstpath, type, callback) + }) + }) + }) + }) +} + +function createSymlinkSync (srcpath, dstpath, type, callback) { + callback = (typeof type === 'function') ? type : callback + type = (typeof type === 'function') ? false : type + var relative = symlinkPathsSync(srcpath, dstpath) + srcpath = relative.toDst + type = symlinkTypeSync(relative.toCwd, type) + var dir = path.dirname(dstpath) + var exists = fs.existsSync(dir) + if (exists) return fs.symlinkSync(srcpath, dstpath, type) + mkdirsSync(dir) + return fs.symlinkSync(srcpath, dstpath, type) +} + +module.exports = { + createSymlink: createSymlink, + createSymlinkSync: createSymlinkSync, + // alias + ensureSymlink: createSymlink, + ensureSymlinkSync: createSymlinkSync +}