You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

pack.js 9.8KB

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