walk.js 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. /*
  2. * This file is part of the storage node for the Joystream project.
  3. * Copyright (C) 2019 Joystream Contributors
  4. *
  5. * This program is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation, either version 3 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License
  16. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  17. */
  18. 'use strict'
  19. const fs = require('fs')
  20. const path = require('path')
  21. const debug = require('debug')('joystream:util:fs:walk')
  22. class Walker {
  23. constructor(archive, base, cb) {
  24. this.archive = archive
  25. this.base = base
  26. this.slice_offset = this.base.length
  27. if (this.base[this.slice_offset - 1] !== '/') {
  28. this.slice_offset += 1
  29. }
  30. this.cb = cb
  31. this.pending = 0
  32. }
  33. /*
  34. * Check pending
  35. */
  36. checkPending(name) {
  37. // Decrease pending count again.
  38. this.pending -= 1
  39. debug('Finishing', name, 'decreases pending to', this.pending)
  40. if (!this.pending) {
  41. debug('No more pending.')
  42. this.cb(null)
  43. }
  44. }
  45. /*
  46. * Helper function for walk; split out because it's used in two places.
  47. */
  48. reportAndRecurse(relname, fname, lstat, linktarget) {
  49. // First report the value
  50. this.cb(null, relname, lstat, linktarget)
  51. // Recurse
  52. if (lstat.isDirectory()) {
  53. this.walk(fname)
  54. }
  55. this.checkPending(fname)
  56. }
  57. walk(dir) {
  58. // This is a little hacky - since readdir() may take a while, and we don't
  59. // want the pending count to drop to zero before it's finished, we bump
  60. // it up and down while readdir() does it's job.
  61. // What this achieves is that when processing a parent directory finishes
  62. // before walk() on a subdirectory could finish its readdir() call, the
  63. // pending count still has a value.
  64. // Note that in order not to hang on empty directories, we need to
  65. // explicitly check the pending count in cases when there are no files.
  66. this.pending += 1
  67. this.archive.readdir(dir, (err, files) => {
  68. if (err) {
  69. this.cb(err)
  70. return
  71. }
  72. // More pending data.
  73. this.pending += files.length
  74. debug('Reading', dir, 'bumps pending to', this.pending)
  75. files.forEach((name) => {
  76. const fname = path.resolve(dir, name)
  77. this.archive.lstat(fname, (err2, lstat) => {
  78. if (err2) {
  79. this.cb(err2)
  80. return
  81. }
  82. // The base is always prefixed, so a simple string slice should do.
  83. const relname = fname.slice(this.slice_offset)
  84. // We have a symbolic link? Resolve it.
  85. if (lstat.isSymbolicLink()) {
  86. this.archive.readlink(fname, (err3, linktarget) => {
  87. if (err3) {
  88. this.cb(err3)
  89. return
  90. }
  91. this.reportAndRecurse(relname, fname, lstat, linktarget)
  92. })
  93. } else {
  94. this.reportAndRecurse(relname, fname, lstat)
  95. }
  96. })
  97. })
  98. this.checkPending(dir)
  99. })
  100. }
  101. }
  102. /*
  103. * Recursively walk a file system hierarchy (in undefined order), returning all
  104. * entries via the callback(err, relname, lstat, [linktarget]). The name relative
  105. * to the base is returned.
  106. *
  107. * You can optionally pass an 'archive', i.e. a class or module that responds to
  108. * file system like functions. If you don't, then the 'fs' module is assumed as
  109. * default.
  110. *
  111. * The callback is invoked one last time without data to signal the end of data.
  112. */
  113. module.exports = function (base, archive, cb) {
  114. // Archive is optional and defaults to fs, but cb is not.
  115. if (!cb) {
  116. cb = archive
  117. archive = fs
  118. }
  119. const resolved = path.resolve(base)
  120. const w = new Walker(archive, resolved, cb)
  121. w.walk(resolved)
  122. }