forked from Fediversity/fediversity.eu
159 lines
5 KiB
JavaScript
159 lines
5 KiB
JavaScript
|
'use strict'
|
||
|
|
||
|
const fs = require('../fs')
|
||
|
const path = require('path')
|
||
|
const u = require('universalify').fromPromise
|
||
|
|
||
|
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 }
|
||
|
}
|
||
|
|
||
|
async function checkPaths (src, dest, funcName, opts) {
|
||
|
const { srcStat, destStat } = await getStats(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 }
|
||
|
}
|
||
|
|
||
|
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.
|
||
|
async function checkParentPaths (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 = await fs.stat(destParent, { bigint: true })
|
||
|
} catch (err) {
|
||
|
if (err.code === 'ENOENT') return
|
||
|
throw err
|
||
|
}
|
||
|
|
||
|
if (areIdentical(srcStat, destStat)) {
|
||
|
throw new Error(errMsg(src, dest, funcName))
|
||
|
}
|
||
|
|
||
|
return checkParentPaths(src, srcStat, destParent, funcName)
|
||
|
}
|
||
|
|
||
|
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.every((cur, i) => destArr[i] === cur)
|
||
|
}
|
||
|
|
||
|
function errMsg (src, dest, funcName) {
|
||
|
return `Cannot ${funcName} '${src}' to a subdirectory of itself, '${dest}'.`
|
||
|
}
|
||
|
|
||
|
module.exports = {
|
||
|
// checkPaths
|
||
|
checkPaths: u(checkPaths),
|
||
|
checkPathsSync,
|
||
|
// checkParent
|
||
|
checkParentPaths: u(checkParentPaths),
|
||
|
checkParentPathsSync,
|
||
|
// Misc
|
||
|
isSrcSubdir,
|
||
|
areIdentical
|
||
|
}
|