pack.js 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  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 normPath = require('./normalize-windows-path.js')
  54. const Pack = warner(class Pack extends Minipass {
  55. constructor (opt) {
  56. super(opt)
  57. opt = opt || Object.create(null)
  58. this.opt = opt
  59. this.file = opt.file || ''
  60. this.cwd = opt.cwd || process.cwd()
  61. this.maxReadSize = opt.maxReadSize
  62. this.preservePaths = !!opt.preservePaths
  63. this.strict = !!opt.strict
  64. this.noPax = !!opt.noPax
  65. this.prefix = normPath(opt.prefix || '')
  66. this.linkCache = opt.linkCache || new Map()
  67. this.statCache = opt.statCache || new Map()
  68. this.readdirCache = opt.readdirCache || new Map()
  69. this[WRITEENTRYCLASS] = WriteEntry
  70. if (typeof opt.onwarn === 'function') {
  71. this.on('warn', opt.onwarn)
  72. }
  73. this.portable = !!opt.portable
  74. this.zip = null
  75. if (opt.gzip) {
  76. if (typeof opt.gzip !== 'object') {
  77. opt.gzip = {}
  78. }
  79. if (this.portable) {
  80. opt.gzip.portable = true
  81. }
  82. this.zip = new zlib.Gzip(opt.gzip)
  83. this.zip.on('data', chunk => super.write(chunk))
  84. this.zip.on('end', _ => super.end())
  85. this.zip.on('drain', _ => this[ONDRAIN]())
  86. this.on('resume', _ => this.zip.resume())
  87. } else {
  88. this.on('drain', this[ONDRAIN])
  89. }
  90. this.noDirRecurse = !!opt.noDirRecurse
  91. this.follow = !!opt.follow
  92. this.noMtime = !!opt.noMtime
  93. this.mtime = opt.mtime || null
  94. this.filter = typeof opt.filter === 'function' ? opt.filter : _ => true
  95. this[QUEUE] = new Yallist()
  96. this[JOBS] = 0
  97. this.jobs = +opt.jobs || 4
  98. this[PROCESSING] = false
  99. this[ENDED] = false
  100. }
  101. [WRITE] (chunk) {
  102. return super.write(chunk)
  103. }
  104. add (path) {
  105. this.write(path)
  106. return this
  107. }
  108. end (path) {
  109. if (path) {
  110. this.write(path)
  111. }
  112. this[ENDED] = true
  113. this[PROCESS]()
  114. return this
  115. }
  116. write (path) {
  117. if (this[ENDED]) {
  118. throw new Error('write after end')
  119. }
  120. if (path instanceof ReadEntry) {
  121. this[ADDTARENTRY](path)
  122. } else {
  123. this[ADDFSENTRY](path)
  124. }
  125. return this.flowing
  126. }
  127. [ADDTARENTRY] (p) {
  128. const absolute = normPath(path.resolve(this.cwd, p.path))
  129. // in this case, we don't have to wait for the stat
  130. if (!this.filter(p.path, p)) {
  131. p.resume()
  132. } else {
  133. const job = new PackJob(p.path, absolute, false)
  134. job.entry = new WriteEntryTar(p, this[ENTRYOPT](job))
  135. job.entry.on('end', _ => this[JOBDONE](job))
  136. this[JOBS] += 1
  137. this[QUEUE].push(job)
  138. }
  139. this[PROCESS]()
  140. }
  141. [ADDFSENTRY] (p) {
  142. const absolute = normPath(path.resolve(this.cwd, p))
  143. this[QUEUE].push(new PackJob(p, absolute))
  144. this[PROCESS]()
  145. }
  146. [STAT] (job) {
  147. job.pending = true
  148. this[JOBS] += 1
  149. const stat = this.follow ? 'stat' : 'lstat'
  150. fs[stat](job.absolute, (er, stat) => {
  151. job.pending = false
  152. this[JOBS] -= 1
  153. if (er) {
  154. this.emit('error', er)
  155. } else {
  156. this[ONSTAT](job, stat)
  157. }
  158. })
  159. }
  160. [ONSTAT] (job, stat) {
  161. this.statCache.set(job.absolute, stat)
  162. job.stat = stat
  163. // now we have the stat, we can filter it.
  164. if (!this.filter(job.path, stat)) {
  165. job.ignore = true
  166. }
  167. this[PROCESS]()
  168. }
  169. [READDIR] (job) {
  170. job.pending = true
  171. this[JOBS] += 1
  172. fs.readdir(job.absolute, (er, entries) => {
  173. job.pending = false
  174. this[JOBS] -= 1
  175. if (er) {
  176. return this.emit('error', er)
  177. }
  178. this[ONREADDIR](job, entries)
  179. })
  180. }
  181. [ONREADDIR] (job, entries) {
  182. this.readdirCache.set(job.absolute, entries)
  183. job.readdir = entries
  184. this[PROCESS]()
  185. }
  186. [PROCESS] () {
  187. if (this[PROCESSING]) {
  188. return
  189. }
  190. this[PROCESSING] = true
  191. for (let w = this[QUEUE].head;
  192. w !== null && this[JOBS] < this.jobs;
  193. w = w.next) {
  194. this[PROCESSJOB](w.value)
  195. if (w.value.ignore) {
  196. const p = w.next
  197. this[QUEUE].removeNode(w)
  198. w.next = p
  199. }
  200. }
  201. this[PROCESSING] = false
  202. if (this[ENDED] && !this[QUEUE].length && this[JOBS] === 0) {
  203. if (this.zip) {
  204. this.zip.end(EOF)
  205. } else {
  206. super.write(EOF)
  207. super.end()
  208. }
  209. }
  210. }
  211. get [CURRENT] () {
  212. return this[QUEUE] && this[QUEUE].head && this[QUEUE].head.value
  213. }
  214. [JOBDONE] (job) {
  215. this[QUEUE].shift()
  216. this[JOBS] -= 1
  217. this[PROCESS]()
  218. }
  219. [PROCESSJOB] (job) {
  220. if (job.pending) {
  221. return
  222. }
  223. if (job.entry) {
  224. if (job === this[CURRENT] && !job.piped) {
  225. this[PIPE](job)
  226. }
  227. return
  228. }
  229. if (!job.stat) {
  230. if (this.statCache.has(job.absolute)) {
  231. this[ONSTAT](job, this.statCache.get(job.absolute))
  232. } else {
  233. this[STAT](job)
  234. }
  235. }
  236. if (!job.stat) {
  237. return
  238. }
  239. // filtered out!
  240. if (job.ignore) {
  241. return
  242. }
  243. if (!this.noDirRecurse && job.stat.isDirectory() && !job.readdir) {
  244. if (this.readdirCache.has(job.absolute)) {
  245. this[ONREADDIR](job, this.readdirCache.get(job.absolute))
  246. } else {
  247. this[READDIR](job)
  248. }
  249. if (!job.readdir) {
  250. return
  251. }
  252. }
  253. // we know it doesn't have an entry, because that got checked above
  254. job.entry = this[ENTRY](job)
  255. if (!job.entry) {
  256. job.ignore = true
  257. return
  258. }
  259. if (job === this[CURRENT] && !job.piped) {
  260. this[PIPE](job)
  261. }
  262. }
  263. [ENTRYOPT] (job) {
  264. return {
  265. onwarn: (code, msg, data) => this.warn(code, msg, data),
  266. noPax: this.noPax,
  267. cwd: this.cwd,
  268. absolute: job.absolute,
  269. preservePaths: this.preservePaths,
  270. maxReadSize: this.maxReadSize,
  271. strict: this.strict,
  272. portable: this.portable,
  273. linkCache: this.linkCache,
  274. statCache: this.statCache,
  275. noMtime: this.noMtime,
  276. mtime: this.mtime,
  277. prefix: this.prefix,
  278. }
  279. }
  280. [ENTRY] (job) {
  281. this[JOBS] += 1
  282. try {
  283. return new this[WRITEENTRYCLASS](job.path, this[ENTRYOPT](job))
  284. .on('end', () => this[JOBDONE](job))
  285. .on('error', er => this.emit('error', er))
  286. } catch (er) {
  287. this.emit('error', er)
  288. }
  289. }
  290. [ONDRAIN] () {
  291. if (this[CURRENT] && this[CURRENT].entry) {
  292. this[CURRENT].entry.resume()
  293. }
  294. }
  295. // like .pipe() but using super, because our write() is special
  296. [PIPE] (job) {
  297. job.piped = true
  298. if (job.readdir) {
  299. job.readdir.forEach(entry => {
  300. const p = job.path
  301. const base = p === './' ? '' : p.replace(/\/*$/, '/')
  302. this[ADDFSENTRY](base + entry)
  303. })
  304. }
  305. const source = job.entry
  306. const zip = this.zip
  307. if (zip) {
  308. source.on('data', chunk => {
  309. if (!zip.write(chunk)) {
  310. source.pause()
  311. }
  312. })
  313. } else {
  314. source.on('data', chunk => {
  315. if (!super.write(chunk)) {
  316. source.pause()
  317. }
  318. })
  319. }
  320. }
  321. pause () {
  322. if (this.zip) {
  323. this.zip.pause()
  324. }
  325. return super.pause()
  326. }
  327. })
  328. class PackSync extends Pack {
  329. constructor (opt) {
  330. super(opt)
  331. this[WRITEENTRYCLASS] = WriteEntrySync
  332. }
  333. // pause/resume are no-ops in sync streams.
  334. pause () {}
  335. resume () {}
  336. [STAT] (job) {
  337. const stat = this.follow ? 'statSync' : 'lstatSync'
  338. this[ONSTAT](job, fs[stat](job.absolute))
  339. }
  340. [READDIR] (job, stat) {
  341. this[ONREADDIR](job, fs.readdirSync(job.absolute))
  342. }
  343. // gotta get it all in this tick
  344. [PIPE] (job) {
  345. const source = job.entry
  346. const zip = this.zip
  347. if (job.readdir) {
  348. job.readdir.forEach(entry => {
  349. const p = job.path
  350. const base = p === './' ? '' : p.replace(/\/*$/, '/')
  351. this[ADDFSENTRY](base + entry)
  352. })
  353. }
  354. if (zip) {
  355. source.on('data', chunk => {
  356. zip.write(chunk)
  357. })
  358. } else {
  359. source.on('data', chunk => {
  360. super[WRITE](chunk)
  361. })
  362. }
  363. }
  364. }
  365. Pack.Sync = PackSync
  366. module.exports = Pack