index.js 4.03 KB
'use strict';
const path = require('path');
const {constants: fsConstants} = require('fs');
const {Buffer} = require('safe-buffer');
const CpFileError = require('./cp-file-error');
const fs = require('./fs');
const ProgressEmitter = require('./progress-emitter');

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

	options = Object.assign({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)
				]));
			}
		});

	promise.on = (...args) => {
		progressEmitter.on(...args);
		return promise;
	};

	return promise;
};

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

const checkSourceIsFile = (stat, source) => {
	if (stat.isDirectory()) {
		throw Object.assign(new CpFileError(`EISDIR: illegal operation on a directory '${source}'`), {
			errno: -21,
			code: 'EISDIR',
			source
		});
	}
};

const fixupAttributes = (destination, stat) => {
	fs.chmodSync(destination, stat.mode);
	fs.chownSync(destination, stat.uid, stat.gid);
};

const copySyncNative = (source, destination, options) => {
	const stat = fs.statSync(source);
	checkSourceIsFile(stat, source);
	fs.makeDirSync(path.dirname(destination));

	const flags = options.overwrite ? null : fsConstants.COPYFILE_EXCL;
	try {
		fs.copyFileSync(source, destination, flags);
	} catch (error) {
		if (!options.overwrite && error.code === 'EEXIST') {
			return;
		}

		throw error;
	}

	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);
	}
};