stat.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. 'use strict'
  2. const fs = require('../fs')
  3. const path = require('path')
  4. const util = require('util')
  5. const atLeastNode = require('at-least-node')
  6. const nodeSupportsBigInt = atLeastNode('10.5.0')
  7. const stat = (file) => nodeSupportsBigInt ? fs.stat(file, { bigint: true }) : fs.stat(file)
  8. const statSync = (file) => nodeSupportsBigInt ? fs.statSync(file, { bigint: true }) : fs.statSync(file)
  9. function getStats (src, dest) {
  10. return Promise.all([
  11. stat(src),
  12. stat(dest).catch(err => {
  13. if (err.code === 'ENOENT') return null
  14. throw err
  15. })
  16. ]).then(([srcStat, destStat]) => ({ srcStat, destStat }))
  17. }
  18. function getStatsSync (src, dest) {
  19. let destStat
  20. const srcStat = statSync(src)
  21. try {
  22. destStat = statSync(dest)
  23. } catch (err) {
  24. if (err.code === 'ENOENT') return { srcStat, destStat: null }
  25. throw err
  26. }
  27. return { srcStat, destStat }
  28. }
  29. function checkPaths (src, dest, funcName, cb) {
  30. util.callbackify(getStats)(src, dest, (err, stats) => {
  31. if (err) return cb(err)
  32. const { srcStat, destStat } = stats
  33. if (destStat && areIdentical(srcStat, destStat)) {
  34. return cb(new Error('Source and destination must not be the same.'))
  35. }
  36. if (srcStat.isDirectory() && isSrcSubdir(src, dest)) {
  37. return cb(new Error(errMsg(src, dest, funcName)))
  38. }
  39. return cb(null, { srcStat, destStat })
  40. })
  41. }
  42. function checkPathsSync (src, dest, funcName) {
  43. const { srcStat, destStat } = getStatsSync(src, dest)
  44. if (destStat && areIdentical(srcStat, destStat)) {
  45. throw new Error('Source and destination must not be the same.')
  46. }
  47. if (srcStat.isDirectory() && isSrcSubdir(src, dest)) {
  48. throw new Error(errMsg(src, dest, funcName))
  49. }
  50. return { srcStat, destStat }
  51. }
  52. // recursively check if dest parent is a subdirectory of src.
  53. // It works for all file types including symlinks since it
  54. // checks the src and dest inodes. It starts from the deepest
  55. // parent and stops once it reaches the src parent or the root path.
  56. function checkParentPaths (src, srcStat, dest, funcName, cb) {
  57. const srcParent = path.resolve(path.dirname(src))
  58. const destParent = path.resolve(path.dirname(dest))
  59. if (destParent === srcParent || destParent === path.parse(destParent).root) return cb()
  60. const callback = (err, destStat) => {
  61. if (err) {
  62. if (err.code === 'ENOENT') return cb()
  63. return cb(err)
  64. }
  65. if (areIdentical(srcStat, destStat)) {
  66. return cb(new Error(errMsg(src, dest, funcName)))
  67. }
  68. return checkParentPaths(src, srcStat, destParent, funcName, cb)
  69. }
  70. if (nodeSupportsBigInt) fs.stat(destParent, { bigint: true }, callback)
  71. else fs.stat(destParent, callback)
  72. }
  73. function checkParentPathsSync (src, srcStat, dest, funcName) {
  74. const srcParent = path.resolve(path.dirname(src))
  75. const destParent = path.resolve(path.dirname(dest))
  76. if (destParent === srcParent || destParent === path.parse(destParent).root) return
  77. let destStat
  78. try {
  79. destStat = statSync(destParent)
  80. } catch (err) {
  81. if (err.code === 'ENOENT') return
  82. throw err
  83. }
  84. if (areIdentical(srcStat, destStat)) {
  85. throw new Error(errMsg(src, dest, funcName))
  86. }
  87. return checkParentPathsSync(src, srcStat, destParent, funcName)
  88. }
  89. function areIdentical (srcStat, destStat) {
  90. if (destStat.ino && destStat.dev && destStat.ino === srcStat.ino && destStat.dev === srcStat.dev) {
  91. if (nodeSupportsBigInt || destStat.ino < Number.MAX_SAFE_INTEGER) {
  92. // definitive answer
  93. return true
  94. }
  95. // Use additional heuristics if we can't use 'bigint'.
  96. // Different 'ino' could be represented the same if they are >= Number.MAX_SAFE_INTEGER
  97. // See issue 657
  98. if (destStat.size === srcStat.size &&
  99. destStat.mode === srcStat.mode &&
  100. destStat.nlink === srcStat.nlink &&
  101. destStat.atimeMs === srcStat.atimeMs &&
  102. destStat.mtimeMs === srcStat.mtimeMs &&
  103. destStat.ctimeMs === srcStat.ctimeMs &&
  104. destStat.birthtimeMs === srcStat.birthtimeMs) {
  105. // heuristic answer
  106. return true
  107. }
  108. }
  109. return false
  110. }
  111. // return true if dest is a subdir of src, otherwise false.
  112. // It only checks the path strings.
  113. function isSrcSubdir (src, dest) {
  114. const srcArr = path.resolve(src).split(path.sep).filter(i => i)
  115. const destArr = path.resolve(dest).split(path.sep).filter(i => i)
  116. return srcArr.reduce((acc, cur, i) => acc && destArr[i] === cur, true)
  117. }
  118. function errMsg (src, dest, funcName) {
  119. return `Cannot ${funcName} '${src}' to a subdirectory of itself, '${dest}'.`
  120. }
  121. module.exports = {
  122. checkPaths,
  123. checkPathsSync,
  124. checkParentPaths,
  125. checkParentPathsSync,
  126. isSrcSubdir
  127. }