realpath.js 2.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. // look up the realpath, but cache stats to minimize overhead
  2. // If the parent folder is in the realpath cache, then we just
  3. // lstat the child, since there's no need to do a full realpath
  4. // This is not a separate module, and is much simpler than Node's
  5. // built-in fs.realpath, because we only care about symbolic links,
  6. // so we can handle many fewer edge cases.
  7. const fs = require('fs')
  8. /* istanbul ignore next */
  9. const promisify = require('util').promisify || require('util-promisify')
  10. const readlink = promisify(fs.readlink)
  11. const lstat = promisify(fs.lstat)
  12. const { resolve, basename, dirname } = require('path')
  13. const realpathCached = (path, rpcache, stcache, depth) => {
  14. // just a safety against extremely deep eloops
  15. /* istanbul ignore next */
  16. if (depth > 2000)
  17. throw eloop(path)
  18. path = resolve(path)
  19. if (rpcache.has(path))
  20. return Promise.resolve(rpcache.get(path))
  21. const dir = dirname(path)
  22. const base = basename(path)
  23. if (base && rpcache.has(dir))
  24. return realpathChild(dir, base, rpcache, stcache, depth)
  25. // if it's the root, then we know it's real
  26. if (!base) {
  27. rpcache.set(dir, dir)
  28. return Promise.resolve(dir)
  29. }
  30. // the parent, what is that?
  31. // find out, and then come back.
  32. return realpathCached(dir, rpcache, stcache, depth + 1).then(() =>
  33. realpathCached(path, rpcache, stcache, depth + 1))
  34. }
  35. const lstatCached = (path, stcache) => {
  36. if (stcache.has(path))
  37. return Promise.resolve(stcache.get(path))
  38. const p = lstat(path).then(st => {
  39. stcache.set(path, st)
  40. return st
  41. })
  42. stcache.set(path, p)
  43. return p
  44. }
  45. // This is a slight fib, as it doesn't actually occur during a stat syscall.
  46. // But file systems are giant piles of lies, so whatever.
  47. const eloop = path =>
  48. Object.assign(new Error(
  49. `ELOOP: too many symbolic links encountered, stat '${path}'`), {
  50. errno: -62,
  51. syscall: 'stat',
  52. code: 'ELOOP',
  53. path: path,
  54. })
  55. const realpathChild = (dir, base, rpcache, stcache, depth) => {
  56. const realdir = rpcache.get(dir)
  57. // that unpossible
  58. /* istanbul ignore next */
  59. if (typeof realdir === 'undefined')
  60. throw new Error('in realpathChild without parent being in realpath cache')
  61. const realish = resolve(realdir, base)
  62. return lstatCached(realish, stcache).then(st => {
  63. if (!st.isSymbolicLink()) {
  64. rpcache.set(resolve(dir, base), realish)
  65. return realish
  66. }
  67. let res
  68. return readlink(realish).then(target => {
  69. const resolved = res = resolve(realdir, target)
  70. if (realish === resolved)
  71. throw eloop(realish)
  72. return realpathCached(resolved, rpcache, stcache, depth + 1)
  73. }).then(real => {
  74. rpcache.set(resolve(dir, base), real)
  75. return real
  76. })
  77. })
  78. }
  79. module.exports = realpathCached