UNPKG

5.23 kBJavaScriptView Raw
1'use strict'
2
3const fs = require('../fs')
4const path = require('path')
5const util = require('util')
6
7function getStats (src, dest, opts) {
8 const statFunc = opts.dereference
9 ? (file) => fs.stat(file, { bigint: true })
10 : (file) => fs.lstat(file, { bigint: true })
11 return Promise.all([
12 statFunc(src),
13 statFunc(dest).catch(err => {
14 if (err.code === 'ENOENT') return null
15 throw err
16 })
17 ]).then(([srcStat, destStat]) => ({ srcStat, destStat }))
18}
19
20function getStatsSync (src, dest, opts) {
21 let destStat
22 const statFunc = opts.dereference
23 ? (file) => fs.statSync(file, { bigint: true })
24 : (file) => fs.lstatSync(file, { bigint: true })
25 const srcStat = statFunc(src)
26 try {
27 destStat = statFunc(dest)
28 } catch (err) {
29 if (err.code === 'ENOENT') return { srcStat, destStat: null }
30 throw err
31 }
32 return { srcStat, destStat }
33}
34
35function checkPaths (src, dest, funcName, opts, cb) {
36 util.callbackify(getStats)(src, dest, opts, (err, stats) => {
37 if (err) return cb(err)
38 const { srcStat, destStat } = stats
39
40 if (destStat) {
41 if (areIdentical(srcStat, destStat)) {
42 const srcBaseName = path.basename(src)
43 const destBaseName = path.basename(dest)
44 if (funcName === 'move' &&
45 srcBaseName !== destBaseName &&
46 srcBaseName.toLowerCase() === destBaseName.toLowerCase()) {
47 return cb(null, { srcStat, destStat, isChangingCase: true })
48 }
49 return cb(new Error('Source and destination must not be the same.'))
50 }
51 if (srcStat.isDirectory() && !destStat.isDirectory()) {
52 return cb(new Error(`Cannot overwrite non-directory '${dest}' with directory '${src}'.`))
53 }
54 if (!srcStat.isDirectory() && destStat.isDirectory()) {
55 return cb(new Error(`Cannot overwrite directory '${dest}' with non-directory '${src}'.`))
56 }
57 }
58
59 if (srcStat.isDirectory() && isSrcSubdir(src, dest)) {
60 return cb(new Error(errMsg(src, dest, funcName)))
61 }
62 return cb(null, { srcStat, destStat })
63 })
64}
65
66function checkPathsSync (src, dest, funcName, opts) {
67 const { srcStat, destStat } = getStatsSync(src, dest, opts)
68
69 if (destStat) {
70 if (areIdentical(srcStat, destStat)) {
71 const srcBaseName = path.basename(src)
72 const destBaseName = path.basename(dest)
73 if (funcName === 'move' &&
74 srcBaseName !== destBaseName &&
75 srcBaseName.toLowerCase() === destBaseName.toLowerCase()) {
76 return { srcStat, destStat, isChangingCase: true }
77 }
78 throw new Error('Source and destination must not be the same.')
79 }
80 if (srcStat.isDirectory() && !destStat.isDirectory()) {
81 throw new Error(`Cannot overwrite non-directory '${dest}' with directory '${src}'.`)
82 }
83 if (!srcStat.isDirectory() && destStat.isDirectory()) {
84 throw new Error(`Cannot overwrite directory '${dest}' with non-directory '${src}'.`)
85 }
86 }
87
88 if (srcStat.isDirectory() && isSrcSubdir(src, dest)) {
89 throw new Error(errMsg(src, dest, funcName))
90 }
91 return { srcStat, destStat }
92}
93
94// recursively check if dest parent is a subdirectory of src.
95// It works for all file types including symlinks since it
96// checks the src and dest inodes. It starts from the deepest
97// parent and stops once it reaches the src parent or the root path.
98function checkParentPaths (src, srcStat, dest, funcName, cb) {
99 const srcParent = path.resolve(path.dirname(src))
100 const destParent = path.resolve(path.dirname(dest))
101 if (destParent === srcParent || destParent === path.parse(destParent).root) return cb()
102 fs.stat(destParent, { bigint: true }, (err, destStat) => {
103 if (err) {
104 if (err.code === 'ENOENT') return cb()
105 return cb(err)
106 }
107 if (areIdentical(srcStat, destStat)) {
108 return cb(new Error(errMsg(src, dest, funcName)))
109 }
110 return checkParentPaths(src, srcStat, destParent, funcName, cb)
111 })
112}
113
114function checkParentPathsSync (src, srcStat, dest, funcName) {
115 const srcParent = path.resolve(path.dirname(src))
116 const destParent = path.resolve(path.dirname(dest))
117 if (destParent === srcParent || destParent === path.parse(destParent).root) return
118 let destStat
119 try {
120 destStat = fs.statSync(destParent, { bigint: true })
121 } catch (err) {
122 if (err.code === 'ENOENT') return
123 throw err
124 }
125 if (areIdentical(srcStat, destStat)) {
126 throw new Error(errMsg(src, dest, funcName))
127 }
128 return checkParentPathsSync(src, srcStat, destParent, funcName)
129}
130
131function areIdentical (srcStat, destStat) {
132 return destStat.ino && destStat.dev && destStat.ino === srcStat.ino && destStat.dev === srcStat.dev
133}
134
135// return true if dest is a subdir of src, otherwise false.
136// It only checks the path strings.
137function isSrcSubdir (src, dest) {
138 const srcArr = path.resolve(src).split(path.sep).filter(i => i)
139 const destArr = path.resolve(dest).split(path.sep).filter(i => i)
140 return srcArr.reduce((acc, cur, i) => acc && destArr[i] === cur, true)
141}
142
143function errMsg (src, dest, funcName) {
144 return `Cannot ${funcName} '${src}' to a subdirectory of itself, '${dest}'.`
145}
146
147module.exports = {
148 checkPaths,
149 checkPathsSync,
150 checkParentPaths,
151 checkParentPathsSync,
152 isSrcSubdir,
153 areIdentical
154}