Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Require Node.js 8, detect read IO errors after open #33

Merged
merged 2 commits into from
Apr 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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');
coreyfarrell marked this conversation as resolved.
Show resolved Hide resolved
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
Loading