stat.js 5.0 KB

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