const fs = require('graceful-fs'); const path = require('path'); const utils = require('./utils'); const { mkdirs, utimes } = require('fs-extra'); const notExist = Symbol('notExist'); const existsReg = Symbol('existsReg'); module.exports = function copy(src, dest, cryptDecryptStream) { return new Promise(async (resolve, reject) => { src = path.resolve(src); dest = path.resolve(dest); // Do not allow src and dest to be the same if (src === dest) { return reject(new Error('Source and destination must not be the same.')); } try { await checkParentDir(src, dest, cryptDecryptStream); return resolve(); } catch (err) { return reject(err); } }); }; function checkParentDir (src, dest, cryptDecryptStream) { return new Promise(async (resolve, reject) => { const destParent = path.dirname(dest); try { // Verify if dir exists const dirExists = await utils.exists(destParent); // Create dir if (!dirExists) { await mkdirs(destParent); } // Copy items await startCopy(src, dest, cryptDecryptStream); return resolve(); } catch (err) { return reject(err); } }); } function startCopy (src, dest, cryptDecryptStream) { return new Promise(async (resolve, reject) => { try { await getStats(src, dest, cryptDecryptStream); return resolve(); } catch (err) { return reject(err); } }); } function getStats (src, dest, cryptDecryptStream) { return new Promise((resolve, reject) => { return fs.lstat(src, async (err, st) => { try { if (err) { return reject(err); } if (st.isDirectory()) { await onDir(st, src, dest, cryptDecryptStream); } else if (st.isFile() || st.isCharacterDevice() || st.isBlockDevice()) { await onFile(st, src, dest, cryptDecryptStream); } else if (st.isSymbolicLink()) { return reject(new Error('Symbolic links are not supported.')); } return resolve(); } catch (err) { return reject(err); } }); }); } function onFile (srcStat, src, dest, cryptDecryptStream) { return new Promise(async (resolve, reject) => { try { const resolvedPath = await checkDest(dest); if (resolvedPath === notExist) { await copyFile(srcStat, src, dest, cryptDecryptStream); } else if (resolvedPath === existsReg) { await mayCopyFile(srcStat, src, dest, cryptDecryptStream); } else { if (src === resolvedPath) { return resolve(); } await mayCopyFile(srcStat, src, dest, cryptDecryptStream); } } catch (err) { return reject(err); } return resolve(); }); } function mayCopyFile (srcStat, src, dest, cryptDecryptStream) { return new Promise((resolve, reject) => { return fs.unlink(dest, async err => { if (err) { return reject(err); } try { await copyFile(srcStat, src, dest, cryptDecryptStream); } catch (err) { return reject(err); } return resolve(); }); }); } function copyFile (srcStat, src, dest, cryptDecryptStream) { return new Promise((resolve, reject) => { const rs = fs.createReadStream(src); rs.on('error', err => reject(err)) .once('open', () => { const ws = fs.createWriteStream(dest, { mode: srcStat.mode }); ws.on('error', err => reject(err)) .on('open', () => rs.pipe(cryptDecryptStream).pipe(ws)) .once('close', async () => { try { await setDestModeAndTimestamps(srcStat, dest); } catch (err) { return resolve(); } return resolve(); }); }); }); } function setDestModeAndTimestamps (srcStat, dest) { return new Promise((resolve, reject) => { return fs.chmod(dest, srcStat.mode, err => { if (err) { return reject(err); } return utimes(dest, srcStat.atime, srcStat.mtime, () => { if (err) { return reject(err); } return resolve(); }); }); }); } function onDir (srcStat, src, dest, cryptDecryptStream) { return new Promise(async (resolve, reject) => { try { const resolvedPath = await checkDest(dest); if (resolvedPath === notExist) { if (isSrcSubdir(src, dest)) { return reject(new Error(`Cannot copy '${src}' to a subdirectory of itself, '${dest}'.`)); } await mkDirAndCopy(srcStat, src, dest, cryptDecryptStream); } else if (resolvedPath === existsReg) { if (isSrcSubdir(src, dest)) { return reject(new Error(`Cannot copy '${src}' to a subdirectory of itself, '${dest}'.`)); } await mayCopyDir(src, dest, cryptDecryptStream); } else { if (src === resolvedPath) { return reject(); } await copyDir(src, dest, cryptDecryptStream); } } catch (err) { return reject(err); } return resolve(); }); } function mayCopyDir (src, dest, cryptDecryptStream) { return new Promise((resolve, reject) => { return fs.stat(dest, async (err, st) => { if (err) { return reject(err); } if (!st.isDirectory()) { return reject(new Error(`Cannot overwrite non-directory '${dest}' with directory '${src}'.`)); } try { await copyDir(src, dest, cryptDecryptStream); } catch (err) { return reject(err); } return resolve(); }); }); } function mkDirAndCopy (srcStat, src, dest, cryptDecryptStream) { return new Promise((resolve, reject) => { return fs.mkdir(dest, srcStat.mode, err => { if (err) { return reject(err); } return fs.chmod(dest, srcStat.mode, async err => { if (err) { return reject(err); } try { await copyDir(src, dest, cryptDecryptStream); } catch (err) { return reject(err); } return resolve(); }); }); }); } function copyDir (src, dest, cryptDecryptStream) { return new Promise((resolve, reject) => { return fs.readdir(src, async (err, items) => { if (err) { return reject(err); } try { await copyDirItems(items, src, dest, cryptDecryptStream); } catch (err) { return reject(err); } return resolve(); }); }); } function copyDirItems (items, src, dest, cryptDecryptStream) { return new Promise(async (resolve, reject) => { const item = items.pop(); if (!item) { return reject(); } try { await startCopy(path.join(src, item), path.join(dest, item), cryptDecryptStream); return copyDirItems(items, src, dest, cryptDecryptStream); } catch (err) { return reject(err); } }); } // Check if dest exists and/or is a symlink const checkDest = dest => new Promise((resolve, reject) => { return fs.readlink(dest, (err, resolvedPath) => { if (err) { if (err.code === 'ENOENT') { return resolve(notExist); } // Dest exists and is a regular file or directory, Windows may throw UNKNOWN error if (err.code === 'EINVAL' || err.code === 'UNKNOWN') { return resolve(existsReg); } return reject(err); } // Dest exists and is a symlink return resolve(resolvedPath); }); }); // Return true if dest is a subdir of src, otherwise false // Extract dest base dir and check if that is the same as src basename const isSrcSubdir = (src, dest) => { const srcArray = src.split(path.sep); const destArray = dest.split(path.sep); return srcArray.reduce((acc, current, i) => acc && destArray[i] === current, true); };