diff --git a/fs.js b/fs.js index d4831e2..c9aa8a4 100644 --- a/fs.js +++ b/fs.js @@ -37,6 +37,14 @@ exports.utimes = (path, atime, mtime) => fsP.utimes(path, atime, mtime).catch(er throw new CpFileError(`utimes \`${path}\` failed: ${err.message}`, err); }); +exports.chmod = (path, mode) => fsP.chmod(path, mode).catch(err => { + throw new CpFileError(`chmod \`${path}\` failed: ${err.message}`, err); +}); + +exports.chown = (path, uid, gid) => fsP.chown(path, uid, gid).catch(err => { + throw new CpFileError(`chown \`${path}\` failed: ${err.message}`, err); +}); + exports.openSync = (path, flags, mode) => { try { return fs.openSync(path, flags, mode); @@ -83,6 +91,22 @@ exports.futimesSync = (fd, atime, mtime, path) => { } }; +exports.chmodSync = (path, mode) => { + try { + return fs.chmodSync(path, mode); + } catch (err) { + throw new CpFileError(`chmod \`${path}\` failed: ${err.message}`, err); + } +}; + +exports.chownSync = (path, uid, gid) => { + try { + return fs.chownSync(path, uid, gid); + } catch (err) { + throw new CpFileError(`chown \`${path}\` failed: ${err.message}`, err); + } +}; + exports.makeDir = path => makeDir(path, {fs}).catch(err => { throw new CpFileError(`Cannot create directory \`${path}\`: ${err.message}`, err); }); diff --git a/index.js b/index.js index 4cf5f2e..9672bfa 100644 --- a/index.js +++ b/index.js @@ -44,9 +44,13 @@ module.exports = (src, dest, opts) => { read.pipe(write); })) - .then(updateTimes => { - if (updateTimes) { - return fs.lstat(src).then(stats => fs.utimes(dest, stats.atime, stats.mtime)); + .then(updateStats => { + if (updateStats) { + return fs.lstat(src).then(stats => Promise.all([ + fs.utimes(dest, stats.atime, stats.mtime), + fs.chmod(dest, stats.mode), + fs.chown(dest, stats.uid, stats.gid) + ])); } }); @@ -99,6 +103,8 @@ module.exports.sync = function (src, dest, opts) { const stat = fs.fstatSync(read, src); fs.futimesSync(write, stat.atime, stat.mtime, dest); + fs.chmodSync(dest, stat.mode); + fs.chownSync(dest, stat.uid, stat.gid); fs.closeSync(read); fs.closeSync(write); }; diff --git a/test/async.js b/test/async.js index 46412ab..374850e 100644 --- a/test/async.js +++ b/test/async.js @@ -9,7 +9,7 @@ import uuid from 'uuid'; import sinon from 'sinon'; import m from '..'; import assertDateEqual from './helpers/assert'; -import {buildEACCES, buildENOSPC, buildENOENT} from './helpers/fs-errors'; +import {buildEACCES, buildENOSPC, buildENOENT, buildEPERM} from './helpers/fs-errors'; const THREE_HUNDRED_KILO = (100 * 3 * 1024) + 1; @@ -99,6 +99,21 @@ test('preserve timestamps', async t => { assertDateEqual(t, licenseStats.mtime, tmpStats.mtime); }); +test('preserve mode', async t => { + await m('license', t.context.dest); + const licenseStats = fs.lstatSync('license'); + const tmpStats = fs.lstatSync(t.context.dest); + t.is(licenseStats.mode, tmpStats.mode); +}); + +test('preserve ownership', async t => { + await m('license', t.context.dest); + const licenseStats = fs.lstatSync('license'); + const tmpStats = fs.lstatSync(t.context.dest); + t.is(licenseStats.gid, tmpStats.gid); + t.is(licenseStats.uid, tmpStats.uid); +}); + test('throw an Error if `src` does not exists', async t => { const err = await t.throws(m('NO_ENTRY', t.context.dest)); t.is(err.name, 'CpFileError', err); @@ -182,3 +197,35 @@ test.serial('rethrow utimes errors', async t => { fs.utimes.restore(); }); + +test.serial('rethrow chmod errors', async t => { + const chmodError = buildEPERM(t.context.dest, 'chmod'); + + fs.chmod = sinon.stub(fs, 'chmod').throws(chmodError); + + clearModule('../fs'); + const uncached = importFresh('..'); + const err = await t.throws(uncached('license', t.context.dest)); + t.is(err.name, 'CpFileError', err); + t.is(err.code, chmodError.code, err); + t.is(err.path, chmodError.path, err); + t.true(fs.chmod.called); + + fs.chmod.restore(); +}); + +test.serial('rethrow chown errors', async t => { + const chownError = buildEPERM(t.context.dest, 'chown'); + + fs.chown = sinon.stub(fs, 'chown').throws(chownError); + + clearModule('../fs'); + const uncached = importFresh('..'); + const err = await t.throws(uncached('license', t.context.dest)); + t.is(err.name, 'CpFileError', err); + t.is(err.code, chownError.code, err); + t.is(err.path, chownError.path, err); + t.true(fs.chown.called); + + fs.chown.restore(); +}); diff --git a/test/helpers/fs-errors.js b/test/helpers/fs-errors.js index f31fd9a..2f58854 100644 --- a/test/helpers/fs-errors.js +++ b/test/helpers/fs-errors.js @@ -21,3 +21,8 @@ exports.buildEBADF = () => Object.assign(new Error(`EBADF: bad file descriptor`) errno: -9, code: 'EBADF' }); + +exports.buildEPERM = (path, method) => Object.assign(new Error(`EPERM: ${method} '${path}''`), { + errno: 50, + code: 'EPERM' +}); diff --git a/test/sync.js b/test/sync.js index 81dd4df..1197126 100644 --- a/test/sync.js +++ b/test/sync.js @@ -7,7 +7,7 @@ import uuid from 'uuid'; import sinon from 'sinon'; import m from '..'; import assertDateEqual from './helpers/assert'; -import {buildEACCES, buildENOSPC, buildEBADF} from './helpers/fs-errors'; +import {buildEACCES, buildENOSPC, buildEBADF, buildEPERM} from './helpers/fs-errors'; const THREE_HUNDRED_KILO = (100 * 3 * 1024) + 1; @@ -97,6 +97,21 @@ test('preserve timestamps', t => { assertDateEqual(t, licenseStats.mtime, tmpStats.mtime); }); +test('preserve mode', t => { + m.sync('license', t.context.dest); + const licenseStats = fs.lstatSync('license'); + const tmpStats = fs.lstatSync(t.context.dest); + t.is(licenseStats.mode, tmpStats.mode); +}); + +test('preserve ownership', t => { + m.sync('license', t.context.dest); + const licenseStats = fs.lstatSync('license'); + const tmpStats = fs.lstatSync(t.context.dest); + t.is(licenseStats.gid, tmpStats.gid); + t.is(licenseStats.uid, tmpStats.uid); +}); + test('throw an Error if `src` does not exists', t => { const err = t.throws(() => m.sync('NO_ENTRY', t.context.dest)); t.is(err.name, 'CpFileError', err); @@ -180,3 +195,31 @@ test('rethrow EACCES errors of dest', t => { fs.openSync.restore(); }); + +test('rethrow chmod errors', t => { + const chmodError = buildEPERM(t.context.dest, 'chmod'); + + fs.chmodSync = sinon.stub(fs, 'chmodSync').throws(chmodError); + + const err = t.throws(() => m.sync('license', t.context.dest)); + t.is(err.name, 'CpFileError', err); + t.is(err.errno, chmodError.errno, err); + t.is(err.code, chmodError.code, err); + t.true(fs.chmodSync.called); + + fs.chmodSync.restore(); +}); + +test('rethrow chown errors', t => { + const chownError = buildEPERM(t.context.dest, 'chown'); + + fs.chownSync = sinon.stub(fs, 'chownSync').throws(chownError); + + const err = t.throws(() => m.sync('license', t.context.dest)); + t.is(err.name, 'CpFileError', err); + t.is(err.errno, chownError.errno, err); + t.is(err.code, chownError.code, err); + t.true(fs.chownSync.called); + + fs.chownSync.restore(); +});