Skip to content

Commit

Permalink
Preserve file attributes from source file (#20)
Browse files Browse the repository at this point in the history
Fixes #16
  • Loading branch information
kevva authored and sindresorhus committed Sep 20, 2017
1 parent fdba27e commit 22ef37b
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 5 deletions.
24 changes: 24 additions & 0 deletions fs.js
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
});
Expand Down
12 changes: 9 additions & 3 deletions index.js
Expand Up @@ -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)
]));
}
});

Expand Down Expand Up @@ -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);
};
49 changes: 48 additions & 1 deletion test/async.js
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
});
5 changes: 5 additions & 0 deletions test/helpers/fs-errors.js
Expand Up @@ -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'
});
45 changes: 44 additions & 1 deletion test/sync.js
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
});

0 comments on commit 22ef37b

Please sign in to comment.