Skip to content

Commit

Permalink
Require Node.js 8, detect read IO errors after open (#33)
Browse files Browse the repository at this point in the history
Fixes #30
Closes #31
  • Loading branch information
coreyfarrell authored and sindresorhus committed Apr 19, 2019
1 parent 25a50ff commit 5e42b4a
Show file tree
Hide file tree
Showing 9 changed files with 142 additions and 295 deletions.
1 change: 0 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,5 @@ language: node_js
node_js:
- '10'
- '8'
- '6'
after_success:
- './node_modules/.bin/nyc report --reporter=text-lcov | ./node_modules/.bin/coveralls'
101 changes: 27 additions & 74 deletions fs.js
Original file line number Diff line number Diff line change
@@ -1,80 +1,51 @@
'use strict';
const {promisify} = require('util');
const fs = require('graceful-fs');
const makeDir = require('make-dir');
const pify = require('pify');
const pEvent = require('p-event');
const CpFileError = require('./cp-file-error');

const fsP = pify(fs);
const stat = promisify(fs.stat);
const lstat = promisify(fs.lstat);
const utimes = promisify(fs.utimes);
const chmod = promisify(fs.chmod);
const chown = promisify(fs.chown);

exports.closeSync = fs.closeSync.bind(fs);
exports.createWriteStream = fs.createWriteStream.bind(fs);

exports.createReadStream = (path, options) => new Promise((resolve, reject) => {
exports.createReadStream = async (path, options) => {
const read = fs.createReadStream(path, options);

read.once('error', error => {
reject(new CpFileError(`Cannot read from \`${path}\`: ${error.message}`, error));
});

read.once('readable', () => {
resolve(read);
});
try {
await pEvent(read, ['readable', 'end']);
} catch (error) {
throw new CpFileError(`Cannot read from \`${path}\`: ${error.message}`, error);
}

read.once('end', () => {
resolve(read);
});
});
return read;
};

exports.stat = path => fsP.stat(path).catch(error => {
exports.stat = path => stat(path).catch(error => {
throw new CpFileError(`Cannot stat path \`${path}\`: ${error.message}`, error);
});

exports.lstat = path => fsP.lstat(path).catch(error => {
exports.lstat = path => lstat(path).catch(error => {
throw new CpFileError(`lstat \`${path}\` failed: ${error.message}`, error);
});

exports.utimes = (path, atime, mtime) => fsP.utimes(path, atime, mtime).catch(error => {
exports.utimes = (path, atime, mtime) => utimes(path, atime, mtime).catch(error => {
throw new CpFileError(`utimes \`${path}\` failed: ${error.message}`, error);
});

exports.chmod = (path, mode) => fsP.chmod(path, mode).catch(error => {
exports.chmod = (path, mode) => chmod(path, mode).catch(error => {
throw new CpFileError(`chmod \`${path}\` failed: ${error.message}`, error);
});

exports.chown = (path, uid, gid) => fsP.chown(path, uid, gid).catch(error => {
exports.chown = (path, uid, gid) => chown(path, uid, gid).catch(error => {
throw new CpFileError(`chown \`${path}\` failed: ${error.message}`, error);
});

exports.openSync = (path, flags, mode) => {
try {
return fs.openSync(path, flags, mode);
} catch (error) {
if (flags.includes('w')) {
throw new CpFileError(`Cannot write to \`${path}\`: ${error.message}`, error);
}

throw new CpFileError(`Cannot open \`${path}\`: ${error.message}`, error);
}
};

// eslint-disable-next-line max-params
exports.readSync = (fileDescriptor, buffer, offset, length, position, path) => {
try {
return fs.readSync(fileDescriptor, buffer, offset, length, position);
} catch (error) {
throw new CpFileError(`Cannot read from \`${path}\`: ${error.message}`, error);
}
};

// eslint-disable-next-line max-params
exports.writeSync = (fileDescriptor, buffer, offset, length, position, path) => {
try {
return fs.writeSync(fileDescriptor, buffer, offset, length, position);
} catch (error) {
throw new CpFileError(`Cannot write to \`${path}\`: ${error.message}`, error);
}
};

exports.statSync = path => {
try {
return fs.statSync(path);
Expand All @@ -83,22 +54,6 @@ exports.statSync = path => {
}
};

exports.fstatSync = (fileDescriptor, path) => {
try {
return fs.fstatSync(fileDescriptor);
} catch (error) {
throw new CpFileError(`fstat \`${path}\` failed: ${error.message}`, error);
}
};

exports.futimesSync = (fileDescriptor, atime, mtime, path) => {
try {
return fs.futimesSync(fileDescriptor, atime, mtime, path);
} catch (error) {
throw new CpFileError(`futimes \`${path}\` failed: ${error.message}`, error);
}
};

exports.utimesSync = (path, atime, mtime) => {
try {
return fs.utimesSync(path, atime, mtime);
Expand Down Expand Up @@ -135,12 +90,10 @@ exports.makeDirSync = path => {
}
};

if (fs.copyFileSync) {
exports.copyFileSync = (source, destination, flags) => {
try {
fs.copyFileSync(source, destination, flags);
} catch (error) {
throw new CpFileError(`Cannot copy from \`${source}\` to \`${destination}\`: ${error.message}`, error);
}
};
}
exports.copyFileSync = (source, destination, flags) => {
try {
fs.copyFileSync(source, destination, flags);
} catch (error) {
throw new CpFileError(`Cannot copy from \`${source}\` to \`${destination}\`: ${error.message}`, error);
}
};
3 changes: 0 additions & 3 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,6 @@ declare const cpFile: {
@param destination - Where you want the file copied.
*/
sync(source: string, destination: string, options?: cpFile.Options): void;

// TODO: Remove this for the next major release
default: typeof cpFile;
};

export = cpFile;
160 changes: 60 additions & 100 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,60 +1,67 @@
'use strict';
const path = require('path');
const {constants: fsConstants} = require('fs');
const {Buffer} = require('safe-buffer');
const pEvent = require('p-event');
const CpFileError = require('./cp-file-error');
const fs = require('./fs');
const ProgressEmitter = require('./progress-emitter');

const cpFileAsync = async (source, destination, options, progressEmitter) => {
let readError;
const stat = await fs.stat(source);
progressEmitter.size = stat.size;

const read = await fs.createReadStream(source);
await fs.makeDir(path.dirname(destination));
const write = fs.createWriteStream(destination, {flags: options.overwrite ? 'w' : 'wx'});
read.on('data', () => {
progressEmitter.written = write.bytesWritten;
});
read.once('error', error => {
readError = new CpFileError(`Cannot read from \`${source}\`: ${error.message}`, error);
write.end();
});

let updateStats = false;
try {
const writePromise = pEvent(write, 'close');
read.pipe(write);
await writePromise;
progressEmitter.written = progressEmitter.size;
updateStats = true;
} catch (error) {
if (options.overwrite || error.code !== 'EEXIST') {
throw new CpFileError(`Cannot write to \`${destination}\`: ${error.message}`, error);
}
}

if (readError) {
throw readError;
}

if (updateStats) {
const stats = await fs.lstat(source);

return Promise.all([
fs.utimes(destination, stats.atime, stats.mtime),
fs.chmod(destination, stats.mode),
fs.chown(destination, stats.uid, stats.gid)
]);
}
};

const cpFile = (source, destination, options) => {
if (!source || !destination) {
return Promise.reject(new CpFileError('`source` and `destination` required'));
}

options = Object.assign({overwrite: true}, options);
options = {
overwrite: true,
...options
};

const progressEmitter = new ProgressEmitter(path.resolve(source), path.resolve(destination));

const promise = fs
.stat(source)
.then(stat => {
progressEmitter.size = stat.size;
})
.then(() => fs.createReadStream(source))
.then(read => fs.makeDir(path.dirname(destination)).then(() => read))
.then(read => new Promise((resolve, reject) => {
const write = fs.createWriteStream(destination, {flags: options.overwrite ? 'w' : 'wx'});

read.on('data', () => {
progressEmitter.written = write.bytesWritten;
});

write.on('error', error => {
if (!options.overwrite && error.code === 'EEXIST') {
resolve(false);
return;
}

reject(new CpFileError(`Cannot write to \`${destination}\`: ${error.message}`, error));
});

write.on('close', () => {
progressEmitter.written = progressEmitter.size;
resolve(true);
});

read.pipe(write);
}))
.then(updateStats => {
if (updateStats) {
return fs.lstat(source).then(stats => Promise.all([
fs.utimes(destination, stats.atime, stats.mtime),
fs.chmod(destination, stats.mode),
fs.chown(destination, stats.uid, stats.gid)
]));
}
});

const promise = cpFileAsync(source, destination, options, progressEmitter);
promise.on = (...args) => {
progressEmitter.on(...args);
return promise;
Expand All @@ -64,8 +71,6 @@ const cpFile = (source, destination, options) => {
};

module.exports = cpFile;
// TODO: Remove this for the next major release
module.exports.default = cpFile;

const checkSourceIsFile = (stat, source) => {
if (stat.isDirectory()) {
Expand All @@ -82,7 +87,16 @@ const fixupAttributes = (destination, stat) => {
fs.chownSync(destination, stat.uid, stat.gid);
};

const copySyncNative = (source, destination, options) => {
module.exports.sync = (source, destination, options) => {
if (!source || !destination) {
throw new CpFileError('`source` and `destination` required');
}

options = {
overwrite: true,
...options
};

const stat = fs.statSync(source);
checkSourceIsFile(stat, source);
fs.makeDirSync(path.dirname(destination));
Expand All @@ -101,57 +115,3 @@ const copySyncNative = (source, destination, options) => {
fs.utimesSync(destination, stat.atime, stat.mtime);
fixupAttributes(destination, stat);
};

const copySyncFallback = (source, destination, options) => {
let bytesRead;
let position;
let read; // eslint-disable-line prefer-const
let write;
const BUF_LENGTH = 100 * 1024;
const buffer = Buffer.alloc(BUF_LENGTH);
const readSync = position => fs.readSync(read, buffer, 0, BUF_LENGTH, position, source);
const writeSync = () => fs.writeSync(write, buffer, 0, bytesRead, undefined, destination);

read = fs.openSync(source, 'r');
bytesRead = readSync(0);
position = bytesRead;
fs.makeDirSync(path.dirname(destination));

try {
write = fs.openSync(destination, options.overwrite ? 'w' : 'wx');
} catch (error) {
if (!options.overwrite && error.code === 'EEXIST') {
return;
}

throw error;
}

writeSync();

while (bytesRead === BUF_LENGTH) {
bytesRead = readSync(position);
writeSync();
position += bytesRead;
}

const stat = fs.fstatSync(read, source);
fs.futimesSync(write, stat.atime, stat.mtime, destination);
fs.closeSync(read);
fs.closeSync(write);
fixupAttributes(destination, stat);
};

module.exports.sync = (source, destination, options) => {
if (!source || !destination) {
throw new CpFileError('`source` and `destination` required');
}

options = Object.assign({overwrite: true}, options);

if (fs.copyFileSync) {
copySyncNative(source, destination, options);
} else {
copySyncFallback(source, destination, options);
}
};
7 changes: 3 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
}
],
"engines": {
"node": ">=6"
"node": ">=8"
},
"scripts": {
"test": "xo && nyc ava && tsd"
Expand Down Expand Up @@ -46,10 +46,9 @@
],
"dependencies": {
"graceful-fs": "^4.1.2",
"make-dir": "^2.0.0",
"make-dir": "^3.0.0",
"nested-error-stacks": "^2.0.0",
"pify": "^4.0.1",
"safe-buffer": "^5.0.1"
"p-event": "^4.1.0"
},
"devDependencies": {
"ava": "^1.4.1",
Expand Down
2 changes: 1 addition & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

## Highlights

- Fast by using streams in the async version and [`fs.copyFileSync()`](https://nodejs.org/api/fs.html#fs_fs_copyfilesync_src_dest_flags) (when available) in the synchronous version.
- Fast by using streams in the async version and [`fs.copyFileSync()`](https://nodejs.org/api/fs.html#fs_fs_copyfilesync_src_dest_flags) in the synchronous version.
- Resilient by using [graceful-fs](https://github.com/isaacs/node-graceful-fs).
- User-friendly by creating non-existent destination directories for you.
- Can be safe by turning off [overwriting](#optionsoverwrite).
Expand Down

0 comments on commit 5e42b4a

Please sign in to comment.