You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
154 lines
5.1 KiB
154 lines
5.1 KiB
'use strict' |
|
|
|
const fs = require('../fs') |
|
const path = require('path') |
|
const util = require('util') |
|
|
|
function getStats (src, dest, opts) { |
|
const statFunc = opts.dereference |
|
? (file) => fs.stat(file, { bigint: true }) |
|
: (file) => fs.lstat(file, { bigint: true }) |
|
return Promise.all([ |
|
statFunc(src), |
|
statFunc(dest).catch(err => { |
|
if (err.code === 'ENOENT') return null |
|
throw err |
|
}) |
|
]).then(([srcStat, destStat]) => ({ srcStat, destStat })) |
|
} |
|
|
|
function getStatsSync (src, dest, opts) { |
|
let destStat |
|
const statFunc = opts.dereference |
|
? (file) => fs.statSync(file, { bigint: true }) |
|
: (file) => fs.lstatSync(file, { bigint: true }) |
|
const srcStat = statFunc(src) |
|
try { |
|
destStat = statFunc(dest) |
|
} catch (err) { |
|
if (err.code === 'ENOENT') return { srcStat, destStat: null } |
|
throw err |
|
} |
|
return { srcStat, destStat } |
|
} |
|
|
|
function checkPaths (src, dest, funcName, opts, cb) { |
|
util.callbackify(getStats)(src, dest, opts, (err, stats) => { |
|
if (err) return cb(err) |
|
const { srcStat, destStat } = stats |
|
|
|
if (destStat) { |
|
if (areIdentical(srcStat, destStat)) { |
|
const srcBaseName = path.basename(src) |
|
const destBaseName = path.basename(dest) |
|
if (funcName === 'move' && |
|
srcBaseName !== destBaseName && |
|
srcBaseName.toLowerCase() === destBaseName.toLowerCase()) { |
|
return cb(null, { srcStat, destStat, isChangingCase: true }) |
|
} |
|
return cb(new Error('Source and destination must not be the same.')) |
|
} |
|
if (srcStat.isDirectory() && !destStat.isDirectory()) { |
|
return cb(new Error(`Cannot overwrite non-directory '${dest}' with directory '${src}'.`)) |
|
} |
|
if (!srcStat.isDirectory() && destStat.isDirectory()) { |
|
return cb(new Error(`Cannot overwrite directory '${dest}' with non-directory '${src}'.`)) |
|
} |
|
} |
|
|
|
if (srcStat.isDirectory() && isSrcSubdir(src, dest)) { |
|
return cb(new Error(errMsg(src, dest, funcName))) |
|
} |
|
return cb(null, { srcStat, destStat }) |
|
}) |
|
} |
|
|
|
function checkPathsSync (src, dest, funcName, opts) { |
|
const { srcStat, destStat } = getStatsSync(src, dest, opts) |
|
|
|
if (destStat) { |
|
if (areIdentical(srcStat, destStat)) { |
|
const srcBaseName = path.basename(src) |
|
const destBaseName = path.basename(dest) |
|
if (funcName === 'move' && |
|
srcBaseName !== destBaseName && |
|
srcBaseName.toLowerCase() === destBaseName.toLowerCase()) { |
|
return { srcStat, destStat, isChangingCase: true } |
|
} |
|
throw new Error('Source and destination must not be the same.') |
|
} |
|
if (srcStat.isDirectory() && !destStat.isDirectory()) { |
|
throw new Error(`Cannot overwrite non-directory '${dest}' with directory '${src}'.`) |
|
} |
|
if (!srcStat.isDirectory() && destStat.isDirectory()) { |
|
throw new Error(`Cannot overwrite directory '${dest}' with non-directory '${src}'.`) |
|
} |
|
} |
|
|
|
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() |
|
fs.stat(destParent, { bigint: true }, (err, destStat) => { |
|
if (err) { |
|
if (err.code === 'ENOENT') return cb() |
|
return cb(err) |
|
} |
|
if (areIdentical(srcStat, destStat)) { |
|
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 { |
|
destStat = fs.statSync(destParent, { bigint: true }) |
|
} catch (err) { |
|
if (err.code === 'ENOENT') return |
|
throw err |
|
} |
|
if (areIdentical(srcStat, destStat)) { |
|
throw new Error(errMsg(src, dest, funcName)) |
|
} |
|
return checkParentPathsSync(src, srcStat, destParent, funcName) |
|
} |
|
|
|
function areIdentical (srcStat, destStat) { |
|
return destStat.ino && destStat.dev && destStat.ino === srcStat.ino && destStat.dev === srcStat.dev |
|
} |
|
|
|
// 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, |
|
areIdentical |
|
}
|
|
|