| 'use strict' |
| |
| const fs = require('graceful-fs') |
| const path = require('path') |
| |
| const NODE_VERSION_MAJOR_WITH_BIGINT = 10 |
| const NODE_VERSION_MINOR_WITH_BIGINT = 5 |
| const NODE_VERSION_PATCH_WITH_BIGINT = 0 |
| const nodeVersion = process.versions.node.split('.') |
| const nodeVersionMajor = Number.parseInt(nodeVersion[0], 10) |
| const nodeVersionMinor = Number.parseInt(nodeVersion[1], 10) |
| const nodeVersionPatch = Number.parseInt(nodeVersion[2], 10) |
| |
| function nodeSupportsBigInt () { |
| if (nodeVersionMajor > NODE_VERSION_MAJOR_WITH_BIGINT) { |
| return true |
| } else if (nodeVersionMajor === NODE_VERSION_MAJOR_WITH_BIGINT) { |
| if (nodeVersionMinor > NODE_VERSION_MINOR_WITH_BIGINT) { |
| return true |
| } else if (nodeVersionMinor === NODE_VERSION_MINOR_WITH_BIGINT) { |
| if (nodeVersionPatch >= NODE_VERSION_PATCH_WITH_BIGINT) { |
| return true |
| } |
| } |
| } |
| return false |
| } |
| |
| function getStats (src, dest, cb) { |
| if (nodeSupportsBigInt()) { |
| fs.stat(src, { bigint: true }, (err, srcStat) => { |
| if (err) return cb(err) |
| fs.stat(dest, { bigint: true }, (err, destStat) => { |
| if (err) { |
| if (err.code === 'ENOENT') return cb(null, { srcStat, destStat: null }) |
| return cb(err) |
| } |
| return cb(null, { srcStat, destStat }) |
| }) |
| }) |
| } else { |
| fs.stat(src, (err, srcStat) => { |
| if (err) return cb(err) |
| fs.stat(dest, (err, destStat) => { |
| if (err) { |
| if (err.code === 'ENOENT') return cb(null, { srcStat, destStat: null }) |
| return cb(err) |
| } |
| return cb(null, { srcStat, destStat }) |
| }) |
| }) |
| } |
| } |
| |
| function getStatsSync (src, dest) { |
| let srcStat, destStat |
| if (nodeSupportsBigInt()) { |
| srcStat = fs.statSync(src, { bigint: true }) |
| } else { |
| srcStat = fs.statSync(src) |
| } |
| try { |
| if (nodeSupportsBigInt()) { |
| destStat = fs.statSync(dest, { bigint: true }) |
| } else { |
| destStat = fs.statSync(dest) |
| } |
| } catch (err) { |
| if (err.code === 'ENOENT') return { srcStat, destStat: null } |
| throw err |
| } |
| return { srcStat, destStat } |
| } |
| |
| function checkPaths (src, dest, funcName, cb) { |
| getStats(src, dest, (err, stats) => { |
| if (err) return cb(err) |
| const { srcStat, destStat } = stats |
| if (destStat && destStat.ino && destStat.dev && destStat.ino === srcStat.ino && destStat.dev === srcStat.dev) { |
| return cb(new Error('Source and destination must not be the same.')) |
| } |
| if (srcStat.isDirectory() && isSrcSubdir(src, dest)) { |
| return cb(new Error(errMsg(src, dest, funcName))) |
| } |
| return cb(null, { srcStat, destStat }) |
| }) |
| } |
| |
| function checkPathsSync (src, dest, funcName) { |
| const { srcStat, destStat } = getStatsSync(src, dest) |
| if (destStat && destStat.ino && destStat.dev && destStat.ino === srcStat.ino && destStat.dev === srcStat.dev) { |
| throw new Error('Source and destination must not be the same.') |
| } |
| if (srcStat.isDirectory() && isSrcSubdir(src, dest)) { |
| throw new Error(errMsg(src, dest, funcName)) |
| } |
| return { srcStat, destStat } |
| } |
| |
| // recursively check if dest parent is a subdirectory of src. |
| // It works for all file types including symlinks since it |
| // checks the src and dest inodes. It starts from the deepest |
| // parent and stops once it reaches the src parent or the root path. |
| function checkParentPaths (src, srcStat, dest, funcName, cb) { |
| const srcParent = path.resolve(path.dirname(src)) |
| const destParent = path.resolve(path.dirname(dest)) |
| if (destParent === srcParent || destParent === path.parse(destParent).root) return cb() |
| if (nodeSupportsBigInt()) { |
| fs.stat(destParent, { bigint: true }, (err, destStat) => { |
| if (err) { |
| if (err.code === 'ENOENT') return cb() |
| return cb(err) |
| } |
| if (destStat.ino && destStat.dev && destStat.ino === srcStat.ino && destStat.dev === srcStat.dev) { |
| return cb(new Error(errMsg(src, dest, funcName))) |
| } |
| return checkParentPaths(src, srcStat, destParent, funcName, cb) |
| }) |
| } else { |
| fs.stat(destParent, (err, destStat) => { |
| if (err) { |
| if (err.code === 'ENOENT') return cb() |
| return cb(err) |
| } |
| if (destStat.ino && destStat.dev && destStat.ino === srcStat.ino && destStat.dev === srcStat.dev) { |
| return cb(new Error(errMsg(src, dest, funcName))) |
| } |
| return checkParentPaths(src, srcStat, destParent, funcName, cb) |
| }) |
| } |
| } |
| |
| function checkParentPathsSync (src, srcStat, dest, funcName) { |
| const srcParent = path.resolve(path.dirname(src)) |
| const destParent = path.resolve(path.dirname(dest)) |
| if (destParent === srcParent || destParent === path.parse(destParent).root) return |
| let destStat |
| try { |
| if (nodeSupportsBigInt()) { |
| destStat = fs.statSync(destParent, { bigint: true }) |
| } else { |
| destStat = fs.statSync(destParent) |
| } |
| } catch (err) { |
| if (err.code === 'ENOENT') return |
| throw err |
| } |
| if (destStat.ino && destStat.dev && destStat.ino === srcStat.ino && destStat.dev === srcStat.dev) { |
| throw new Error(errMsg(src, dest, funcName)) |
| } |
| return checkParentPathsSync(src, srcStat, destParent, funcName) |
| } |
| |
| // return true if dest is a subdir of src, otherwise false. |
| // It only checks the path strings. |
| function isSrcSubdir (src, dest) { |
| const srcArr = path.resolve(src).split(path.sep).filter(i => i) |
| const destArr = path.resolve(dest).split(path.sep).filter(i => i) |
| return srcArr.reduce((acc, cur, i) => acc && destArr[i] === cur, true) |
| } |
| |
| function errMsg (src, dest, funcName) { |
| return `Cannot ${funcName} '${src}' to a subdirectory of itself, '${dest}'.` |
| } |
| |
| module.exports = { |
| checkPaths, |
| checkPathsSync, |
| checkParentPaths, |
| checkParentPathsSync, |
| isSrcSubdir |
| } |