pack.js 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. 'use strict'
  2. // A readable tar stream creator
  3. // Technically, this is a transform stream that you write paths into,
  4. // and tar format comes out of.
  5. // The `add()` method is like `write()` but returns this,
  6. // and end() return `this` as well, so you can
  7. // do `new Pack(opt).add('files').add('dir').end().pipe(output)
  8. // You could also do something like:
  9. // streamOfPaths().pipe(new Pack()).pipe(new fs.WriteStream('out.tar'))
  10. class PackJob {
  11. constructor (path, absolute) {
  12. this.path = path || './'
  13. this.absolute = absolute
  14. this.entry = null
  15. this.stat = null
  16. this.readdir = null
  17. this.pending = false
  18. this.ignore = false
  19. this.piped = false
  20. }
  21. }
  22. const MiniPass = require('minipass')
  23. const zlib = require('minizlib')
  24. const ReadEntry = require('./read-entry.js')
  25. const WriteEntry = require('./write-entry.js')
  26. const WriteEntrySync = WriteEntry.Sync
  27. const WriteEntryTar = WriteEntry.Tar
  28. const Yallist = require('yallist')
  29. const EOF = Buffer.alloc(1024)
  30. const ONSTAT = Symbol('onStat')
  31. const ENDED = Symbol('ended')
  32. const QUEUE = Symbol('queue')
  33. const CURRENT = Symbol('current')
  34. const PROCESS = Symbol('process')
  35. const PROCESSING = Symbol('processing')
  36. const PROCESSJOB = Symbol('processJob')
  37. const JOBS = Symbol('jobs')
  38. const JOBDONE = Symbol('jobDone')
  39. const ADDFSENTRY = Symbol('addFSEntry')
  40. const ADDTARENTRY = Symbol('addTarEntry')
  41. const STAT = Symbol('stat')
  42. const READDIR = Symbol('readdir')
  43. const ONREADDIR = Symbol('onreaddir')
  44. const PIPE = Symbol('pipe')
  45. const ENTRY = Symbol('entry')
  46. const ENTRYOPT = Symbol('entryOpt')
  47. const WRITEENTRYCLASS = Symbol('writeEntryClass')
  48. const WRITE = Symbol('write')
  49. const ONDRAIN = Symbol('ondrain')
  50. const fs = require('fs')
  51. const path = require('path')
  52. const warner = require('./warn-mixin.js')
  53. const Pack = warner(class Pack extends MiniPass {
  54. constructor (opt) {
  55. super(opt)
  56. opt = opt || Object.create(null)
  57. this.opt = opt
  58. this.file = opt.file || ''
  59. this.cwd = opt.cwd || process.cwd()
  60. this.maxReadSize = opt.maxReadSize
  61. this.preservePaths = !!opt.preservePaths
  62. this.strict = !!opt.strict
  63. this.noPax = !!opt.noPax
  64. this.prefix = (opt.prefix || '').replace(/(\\|\/)+$/, '')
  65. this.linkCache = opt.linkCache || new Map()
  66. this.statCache = opt.statCache || new Map()
  67. this.readdirCache = opt.readdirCache || new Map()
  68. this[WRITEENTRYCLASS] = WriteEntry
  69. if (typeof opt.onwarn === 'function')
  70. this.on('warn', opt.onwarn)
  71. this.portable = !!opt.portable
  72. this.zip = null
  73. if (opt.gzip) {
  74. if (typeof opt.gzip !== 'object')
  75. opt.gzip = {}
  76. if (this.portable)
  77. opt.gzip.portable = true
  78. this.zip = new zlib.Gzip(opt.gzip)
  79. this.zip.on('data', chunk => super.write(chunk))
  80. this.zip.on('end', _ => super.end())
  81. this.zip.on('drain', _ => this[ONDRAIN]())
  82. this.on('resume', _ => this.zip.resume())
  83. } else
  84. this.on('drain', this[ONDRAIN])
  85. this.noDirRecurse = !!opt.noDirRecurse
  86. this.follow = !!opt.follow
  87. this.noMtime = !!opt.noMtime
  88. this.mtime = opt.mtime || null
  89. this.filter = typeof opt.filter === 'function' ? opt.filter : _ => true
  90. this[QUEUE] = new Yallist
  91. this[JOBS] = 0
  92. this.jobs = +opt.jobs || 4
  93. this[PROCESSING] = false
  94. this[ENDED] = false
  95. }
  96. [WRITE] (chunk) {
  97. return super.write(chunk)
  98. }
  99. add (path) {
  100. this.write(path)
  101. return this
  102. }
  103. end (path) {
  104. if (path)
  105. this.write(path)
  106. this[ENDED] = true
  107. this[PROCESS]()
  108. return this
  109. }
  110. write (path) {
  111. if (this[ENDED])
  112. throw new Error('write after end')
  113. if (path instanceof ReadEntry)
  114. this[ADDTARENTRY](path)
  115. else
  116. this[ADDFSENTRY](path)
  117. return this.flowing
  118. }
  119. [ADDTARENTRY] (p) {
  120. const absolute = path.resolve(this.cwd, p.path)
  121. if (this.prefix)
  122. p.path = this.prefix + '/' + p.path.replace(/^\.(\/+|$)/, '')
  123. // in this case, we don't have to wait for the stat
  124. if (!this.filter(p.path, p))
  125. p.resume()
  126. else {
  127. const job = new PackJob(p.path, absolute, false)
  128. job.entry = new WriteEntryTar(p, this[ENTRYOPT](job))
  129. job.entry.on('end', _ => this[JOBDONE](job))
  130. this[JOBS] += 1
  131. this[QUEUE].push(job)
  132. }
  133. this[PROCESS]()
  134. }
  135. [ADDFSENTRY] (p) {
  136. const absolute = path.resolve(this.cwd, p)
  137. if (this.prefix)
  138. p = this.prefix + '/' + p.replace(/^\.(\/+|$)/, '')
  139. this[QUEUE].push(new PackJob(p, absolute))
  140. this[PROCESS]()
  141. }
  142. [STAT] (job) {
  143. job.pending = true
  144. this[JOBS] += 1
  145. const stat = this.follow ? 'stat' : 'lstat'
  146. fs[stat](job.absolute, (er, stat) => {
  147. job.pending = false
  148. this[JOBS] -= 1
  149. if (er)
  150. this.emit('error', er)
  151. else
  152. this[ONSTAT](job, stat)
  153. })
  154. }
  155. [ONSTAT] (job, stat) {
  156. this.statCache.set(job.absolute, stat)
  157. job.stat = stat
  158. // now we have the stat, we can filter it.
  159. if (!this.filter(job.path, stat))
  160. job.ignore = true
  161. this[PROCESS]()
  162. }
  163. [READDIR] (job) {
  164. job.pending = true
  165. this[JOBS] += 1
  166. fs.readdir(job.absolute, (er, entries) => {
  167. job.pending = false
  168. this[JOBS] -= 1
  169. if (er)
  170. return this.emit('error', er)
  171. this[ONREADDIR](job, entries)
  172. })
  173. }
  174. [ONREADDIR] (job, entries) {
  175. this.readdirCache.set(job.absolute, entries)
  176. job.readdir = entries
  177. this[PROCESS]()
  178. }
  179. [PROCESS] () {
  180. if (this[PROCESSING])
  181. return
  182. this[PROCESSING] = true
  183. for (let w = this[QUEUE].head;
  184. w !== null && this[JOBS] < this.jobs;
  185. w = w.next) {
  186. this[PROCESSJOB](w.value)
  187. if (w.value.ignore) {
  188. const p = w.next
  189. this[QUEUE].removeNode(w)
  190. w.next = p
  191. }
  192. }
  193. this[PROCESSING] = false
  194. if (this[ENDED] && !this[QUEUE].length && this[JOBS] === 0) {
  195. if (this.zip)
  196. this.zip.end(EOF)
  197. else {
  198. super.write(EOF)
  199. super.end()
  200. }
  201. }
  202. }
  203. get [CURRENT] () {
  204. return this[QUEUE] && this[QUEUE].head && this[QUEUE].head.value
  205. }
  206. [JOBDONE] (job) {
  207. this[QUEUE].shift()
  208. this[JOBS] -= 1
  209. this[PROCESS]()
  210. }
  211. [PROCESSJOB] (job) {
  212. if (job.pending)
  213. return
  214. if (job.entry) {
  215. if (job === this[CURRENT] && !job.piped)
  216. this[PIPE](job)
  217. return
  218. }
  219. if (!job.stat) {
  220. if (this.statCache.has(job.absolute))
  221. this[ONSTAT](job, this.statCache.get(job.absolute))
  222. else
  223. this[STAT](job)
  224. }
  225. if (!job.stat)
  226. return
  227. // filtered out!
  228. if (job.ignore)
  229. return
  230. if (!this.noDirRecurse && job.stat.isDirectory() && !job.readdir) {
  231. if (this.readdirCache.has(job.absolute))
  232. this[ONREADDIR](job, this.readdirCache.get(job.absolute))
  233. else
  234. this[READDIR](job)
  235. if (!job.readdir)
  236. return
  237. }
  238. // we know it doesn't have an entry, because that got checked above
  239. job.entry = this[ENTRY](job)
  240. if (!job.entry) {
  241. job.ignore = true
  242. return
  243. }
  244. if (job === this[CURRENT] && !job.piped)
  245. this[PIPE](job)
  246. }
  247. [ENTRYOPT] (job) {
  248. return {
  249. onwarn: (code, msg, data) => this.warn(code, msg, data),
  250. noPax: this.noPax,
  251. cwd: this.cwd,
  252. absolute: job.absolute,
  253. preservePaths: this.preservePaths,
  254. maxReadSize: this.maxReadSize,
  255. strict: this.strict,
  256. portable: this.portable,
  257. linkCache: this.linkCache,
  258. statCache: this.statCache,
  259. noMtime: this.noMtime,
  260. mtime: this.mtime
  261. }
  262. }
  263. [ENTRY] (job) {
  264. this[JOBS] += 1
  265. try {
  266. return new this[WRITEENTRYCLASS](job.path, this[ENTRYOPT](job))
  267. .on('end', () => this[JOBDONE](job))
  268. .on('error', er => this.emit('error', er))
  269. } catch (er) {
  270. this.emit('error', er)
  271. }
  272. }
  273. [ONDRAIN] () {
  274. if (this[CURRENT] && this[CURRENT].entry)
  275. this[CURRENT].entry.resume()
  276. }
  277. // like .pipe() but using super, because our write() is special
  278. [PIPE] (job) {
  279. job.piped = true
  280. if (job.readdir)
  281. job.readdir.forEach(entry => {
  282. const p = this.prefix ?
  283. job.path.slice(this.prefix.length + 1) || './'
  284. : job.path
  285. const base = p === './' ? '' : p.replace(/\/*$/, '/')
  286. this[ADDFSENTRY](base + entry)
  287. })
  288. const source = job.entry
  289. const zip = this.zip
  290. if (zip)
  291. source.on('data', chunk => {
  292. if (!zip.write(chunk))
  293. source.pause()
  294. })
  295. else
  296. source.on('data', chunk => {
  297. if (!super.write(chunk))
  298. source.pause()
  299. })
  300. }
  301. pause () {
  302. if (this.zip)
  303. this.zip.pause()
  304. return super.pause()
  305. }
  306. })
  307. class PackSync extends Pack {
  308. constructor (opt) {
  309. super(opt)
  310. this[WRITEENTRYCLASS] = WriteEntrySync
  311. }
  312. // pause/resume are no-ops in sync streams.
  313. pause () {}
  314. resume () {}
  315. [STAT] (job) {
  316. const stat = this.follow ? 'statSync' : 'lstatSync'
  317. this[ONSTAT](job, fs[stat](job.absolute))
  318. }
  319. [READDIR] (job, stat) {
  320. this[ONREADDIR](job, fs.readdirSync(job.absolute))
  321. }
  322. // gotta get it all in this tick
  323. [PIPE] (job) {
  324. const source = job.entry
  325. const zip = this.zip
  326. if (job.readdir)
  327. job.readdir.forEach(entry => {
  328. const p = this.prefix ?
  329. job.path.slice(this.prefix.length + 1) || './'
  330. : job.path
  331. const base = p === './' ? '' : p.replace(/\/*$/, '/')
  332. this[ADDFSENTRY](base + entry)
  333. })
  334. if (zip)
  335. source.on('data', chunk => {
  336. zip.write(chunk)
  337. })
  338. else
  339. source.on('data', chunk => {
  340. super[WRITE](chunk)
  341. })
  342. }
  343. }
  344. Pack.Sync = PackSync
  345. module.exports = Pack