stat.js 5.1 KB

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