3 const Buffer = require('./buffer.js')
5 // A readable tar stream creator
6 // Technically, this is a transform stream that you write paths into,
7 // and tar format comes out of.
8 // The `add()` method is like `write()` but returns this,
9 // and end() return `this` as well, so you can
10 // do `new Pack(opt).add('files').add('dir').end().pipe(output)
11 // You could also do something like:
12 // streamOfPaths().pipe(new Pack()).pipe(new fs.WriteStream('out.tar'))
15 constructor (path, absolute) {
16 this.path = path || './'
17 this.absolute = absolute
27 const MiniPass = require('minipass')
28 const zlib = require('minizlib')
29 const ReadEntry = require('./read-entry.js')
30 const WriteEntry = require('./write-entry.js')
31 const WriteEntrySync = WriteEntry.Sync
32 const WriteEntryTar = WriteEntry.Tar
33 const Yallist = require('yallist')
34 const EOF = Buffer.alloc(1024)
35 const ONSTAT = Symbol('onStat')
36 const ENDED = Symbol('ended')
37 const QUEUE = Symbol('queue')
38 const CURRENT = Symbol('current')
39 const PROCESS = Symbol('process')
40 const PROCESSING = Symbol('processing')
41 const PROCESSJOB = Symbol('processJob')
42 const JOBS = Symbol('jobs')
43 const JOBDONE = Symbol('jobDone')
44 const ADDFSENTRY = Symbol('addFSEntry')
45 const ADDTARENTRY = Symbol('addTarEntry')
46 const STAT = Symbol('stat')
47 const READDIR = Symbol('readdir')
48 const ONREADDIR = Symbol('onreaddir')
49 const PIPE = Symbol('pipe')
50 const ENTRY = Symbol('entry')
51 const ENTRYOPT = Symbol('entryOpt')
52 const WRITEENTRYCLASS = Symbol('writeEntryClass')
53 const WRITE = Symbol('write')
54 const ONDRAIN = Symbol('ondrain')
56 const fs = require('fs')
57 const path = require('path')
58 const warner = require('./warn-mixin.js')
60 const Pack = warner(class Pack extends MiniPass {
63 opt = opt || Object.create(null)
65 this.cwd = opt.cwd || process.cwd()
66 this.maxReadSize = opt.maxReadSize
67 this.preservePaths = !!opt.preservePaths
68 this.strict = !!opt.strict
69 this.noPax = !!opt.noPax
70 this.prefix = (opt.prefix || '').replace(/(\\|\/)+$/, '')
71 this.linkCache = opt.linkCache || new Map()
72 this.statCache = opt.statCache || new Map()
73 this.readdirCache = opt.readdirCache || new Map()
75 this[WRITEENTRYCLASS] = WriteEntry
76 if (typeof opt.onwarn === 'function')
77 this.on('warn', opt.onwarn)
81 if (typeof opt.gzip !== 'object')
83 this.zip = new zlib.Gzip(opt.gzip)
84 this.zip.on('data', chunk => super.write(chunk))
85 this.zip.on('end', _ => super.end())
86 this.zip.on('drain', _ => this[ONDRAIN]())
87 this.on('resume', _ => this.zip.resume())
89 this.on('drain', this[ONDRAIN])
91 this.portable = !!opt.portable
92 this.noDirRecurse = !!opt.noDirRecurse
93 this.follow = !!opt.follow
94 this.noMtime = !!opt.noMtime
95 this.mtime = opt.mtime || null
97 this.filter = typeof opt.filter === 'function' ? opt.filter : _ => true
99 this[QUEUE] = new Yallist
101 this.jobs = +opt.jobs || 4
102 this[PROCESSING] = false
107 return super.write(chunk)
125 throw new Error('write after end')
127 if (path instanceof ReadEntry)
128 this[ADDTARENTRY](path)
130 this[ADDFSENTRY](path)
135 const absolute = path.resolve(this.cwd, p.path)
137 p.path = this.prefix + '/' + p.path.replace(/^\.(\/+|$)/, '')
139 // in this case, we don't have to wait for the stat
140 if (!this.filter(p.path, p))
143 const job = new PackJob(p.path, absolute, false)
144 job.entry = new WriteEntryTar(p, this[ENTRYOPT](job))
145 job.entry.on('end', _ => this[JOBDONE](job))
147 this[QUEUE].push(job)
154 const absolute = path.resolve(this.cwd, p)
156 p = this.prefix + '/' + p.replace(/^\.(\/+|$)/, '')
158 this[QUEUE].push(new PackJob(p, absolute))
165 const stat = this.follow ? 'stat' : 'lstat'
166 fs[stat](job.absolute, (er, stat) => {
170 this.emit('error', er)
172 this[ONSTAT](job, stat)
176 [ONSTAT] (job, stat) {
177 this.statCache.set(job.absolute, stat)
180 // now we have the stat, we can filter it.
181 if (!this.filter(job.path, stat))
190 fs.readdir(job.absolute, (er, entries) => {
194 return this.emit('error', er)
195 this[ONREADDIR](job, entries)
199 [ONREADDIR] (job, entries) {
200 this.readdirCache.set(job.absolute, entries)
201 job.readdir = entries
206 if (this[PROCESSING])
209 this[PROCESSING] = true
210 for (let w = this[QUEUE].head;
211 w !== null && this[JOBS] < this.jobs;
213 this[PROCESSJOB](w.value)
214 if (w.value.ignore) {
216 this[QUEUE].removeNode(w)
221 this[PROCESSING] = false
223 if (this[ENDED] && !this[QUEUE].length && this[JOBS] === 0) {
234 return this[QUEUE] && this[QUEUE].head && this[QUEUE].head.value
248 if (job === this[CURRENT] && !job.piped)
254 if (this.statCache.has(job.absolute))
255 this[ONSTAT](job, this.statCache.get(job.absolute))
266 if (!this.noDirRecurse && job.stat.isDirectory() && !job.readdir) {
267 if (this.readdirCache.has(job.absolute))
268 this[ONREADDIR](job, this.readdirCache.get(job.absolute))
275 // we know it doesn't have an entry, because that got checked above
276 job.entry = this[ENTRY](job)
282 if (job === this[CURRENT] && !job.piped)
288 onwarn: (msg, data) => {
293 absolute: job.absolute,
294 preservePaths: this.preservePaths,
295 maxReadSize: this.maxReadSize,
297 portable: this.portable,
298 linkCache: this.linkCache,
299 statCache: this.statCache,
300 noMtime: this.noMtime,
308 return new this[WRITEENTRYCLASS](job.path, this[ENTRYOPT](job))
309 .on('end', () => this[JOBDONE](job))
310 .on('error', er => this.emit('error', er))
312 this.emit('error', er)
317 if (this[CURRENT] && this[CURRENT].entry)
318 this[CURRENT].entry.resume()
321 // like .pipe() but using super, because our write() is special
326 job.readdir.forEach(entry => {
327 const p = this.prefix ?
328 job.path.slice(this.prefix.length + 1) || './'
331 const base = p === './' ? '' : p.replace(/\/*$/, '/')
332 this[ADDFSENTRY](base + entry)
335 const source = job.entry
339 source.on('data', chunk => {
340 if (!zip.write(chunk))
344 source.on('data', chunk => {
345 if (!super.write(chunk))
357 class PackSync extends Pack {
360 this[WRITEENTRYCLASS] = WriteEntrySync
363 // pause/resume are no-ops in sync streams.
368 const stat = this.follow ? 'statSync' : 'lstatSync'
369 this[ONSTAT](job, fs[stat](job.absolute))
372 [READDIR] (job, stat) {
373 this[ONREADDIR](job, fs.readdirSync(job.absolute))
376 // gotta get it all in this tick
378 const source = job.entry
382 job.readdir.forEach(entry => {
383 const p = this.prefix ?
384 job.path.slice(this.prefix.length + 1) || './'
387 const base = p === './' ? '' : p.replace(/\/*$/, '/')
388 this[ADDFSENTRY](base + entry)
392 source.on('data', chunk => {
396 source.on('data', chunk => {
404 module.exports = Pack