module.exports = Generator Generator.create = create var tar = require("./tar") , Stream = require("stream").Stream , Parser = require("./parser") , fs = require("fs") function create (opts) { return new Generator(opts) } function Generator (opts) { this.readable = true this.currentFile = null this._paused = false this._ended = false this._queue = [] this.options = { cwd: process.cwd() } Object.keys(opts).forEach(function (o) { this.options[o] = opts[o] }, this) if (this.options.cwd.slice(-1) !== "/") { this.options.cwd += "/" } Stream.apply(this) } Generator.prototype = Object.create(Stream.prototype) Generator.prototype.pause = function () { if (this.currentFile) this.currentFile.pause() this.paused = true this.emit("pause") } Generator.prototype.resume = function () { this.paused = false if (this.currentFile) this.currentFile.resume() this.emit("resume") this._processQueue() } Generator.prototype.end = function () { this._ended = true this._processQueue() } Generator.prototype.append = function (f, st) { if (this._ended) return this.emit("error", new Error( "Cannot append after ending")) // if it's a string, then treat it as a filename. // if it's a number, then treat it as a fd // if it's a Stats, then treat it as a stat object // if it's a Stream, then stream it in. var s = toFileStream(f, st) if (!s) return this.emit("error", new TypeError( "Invalid argument: "+f)) // make sure it's in the folder being added. if (s.name.indexOf(this.options.cwd) !== 0) { this.emit("error", new Error( "Invalid argument: "+s.name+"\nOutside of "+this.options.cwd)) } s.name = s.name.substr(this.options.cwd.length) s.pause() this._queue.push(s) if (!s._needStat) return this._processQueue() var self = this fs.lstat(s.name, function (er, st) { if (er) return self.emit("error", new Error( "invalid file "+s.name+"\n"+er.message)) s.mode = st.mode & 0777 s.uid = st.uid s.gid = st.gid s.size = st.size s.mtime = +st.mtime / 1000 s.type = st.isFile() ? "0" : st.isSymbolicLink() ? "2" : st.isCharacterDevice() ? "3" : st.isBlockDevice() ? "4" : st.isDirectory() ? "5" : st.isFIFO() ? "6" : null // TODO: handle all the types in // http://cdrecord.berlios.de/private/man/star/star.4.html // for now, skip over unknown ones. if (s.type === null) { console.error("Unknown file type: " + s.name) // kick out of the queue var i = self._queue.indexOf(s) if (i !== -1) self._queue.splice(i, 1) self._processQueue() return } if (s.type === "2") return fs.readlink(s.name, function (er, n) { if (er) return self.emit("error", new Error( "error reading link value "+s.name+"\n"+er.message)) s.linkname = n s._needStat = false self._processQueue() }) s._needStat = false self._processQueue() }) return false } function toFileStream (thing) { if (typeof thing === "string") { return toFileStream(fs.createReadStream(thing)) } if (thing && typeof thing === "object") { if (thing instanceof (Parser.File)) return thing if (thing instanceof Stream) { if (thing.hasOwnProperty("name") && thing.hasOwnProperty("mode") && thing.hasOwnProperty("uid") && thing.hasOwnProperty("gid") && thing.hasOwnProperty("size") && thing.hasOwnProperty("mtime") && thing.hasOwnProperty("type")) return thing if (thing instanceof (fs.ReadStream)) { thing.name = thing.path } if (thing.name) { thing._needStat = true return thing } } } return null } Generator.prototype._processQueue = function processQueue () { console.error("processQueue", this._queue[0]) if (this._paused) return false if (this.currentFile || this._queue.length && this._queue[0]._needStat) { // either already processing one, or waiting on something. return } var f = this.currentFile = this._queue.shift() if (!f) { if (this._ended) { // close it off with 2 blocks of nulls. this.emit("data", new Buffer(new Array(512 * 2))) this.emit("end") this.emit("close") } return true } if (f.type === Parser.File.types.Directory && f.name.slice(-1) !== "/") { f.name += "/" } // write out a Pax header if the file isn't kosher. if (this._needPax(f)) this._emitPax(f) // write out the header f.ustar = true this._emitHeader(f) var fpos = 0 , self = this console.error("about to read body data", f) f.on("data", function (c) { self.emit("data", c) self.fpos += c.length }) f.on("error", function (er) { self.emit("error", er) }) f.on("end", function $END () { // pad with \0 out to an even multiple of 512 bytes. // this ensures that every file starts on a block. var b = new Buffer(fpos % 512 || 512) for (var i = 0, l = b.length; i < l; i ++) b[i] = 0 //console.log(b.length, b) self.emit("data", b) self.currentFile = null self._processQueue() }) f.resume() } Generator.prototype._needPax = function (f) { // meh. why not? return true return oddTextField(f.name, "NAME") || oddTextField(f.link, "LINK") || oddTextField(f.gname, "GNAME") || oddTextField(f.uname, "UNAME") || oddTextField(f.prefix, "PREFIX") } // check if a text field is too long or non-ascii function oddTextField (val, field) { var nl = Buffer.byteLength(val) , len = tar.fieldSize[field] if (nl > len || nl !== val.length) return true } // emit a Pax header of "key = val" for any file with // odd or too-long field values. Generator.prototype._emitPax = function (f) { // since these tend to be relatively small, just go ahead // and emit it all in-band. That saves having to keep // track of the pax state in the generator, and we can // go right back to emitting the file in the same tick. var dir = f.name.replace(/[^\/]+\/?$/, "") , base = f.name.substr(dir.length) var pax = { name: dir + "PaxHeader/" +base , mode: 0644 , uid: f.uid , gid: f.gid , mtime: +f.mtime // don't know size yet. , size: -1 , type: "x" // extended header , ustar: true , ustarVersion: "00" , user: f.user || f.uname || "" , group: f.group || f.gname || "" , dev: { major: f.dev && f.dev.major || 0 , minor: f.dev && f.dev.minor || 0 } , prefix: f.prefix , linkname: "" } // generate the Pax body var kv = { path: (f.prefix ? f.prefix + "/" : "") + f.name , atime: f.atime , mtime: f.mtime , ctime: f.ctime , charset: "UTF-8" , gid: f.gid , uid: f.uid , uname: f.user || f.uname || "" , gname: f.group || f.gname || "" , linkpath: f.linkpath || "" , size: f.size } // "%d %s=%s\n", , , // length includes the length of the length number, // the key=val, and the \n. var body = new Buffer(Object.keys(kv).map(function (key) { if (!kv[key]) return ["", ""] var s = new Buffer(" " + key + "=" + kv[key]+"\n") , digits = Math.floor(Math.log(s.length) / Math.log(10)) + 1 // if adding that many digits will make it go over that length, // then add one to it if (s.length > Math.pow(10, digits) - digits) digits ++ return [s.length + digits, s] }).reduce(function (l, r) { return l + r[0] + r[1] }, "")) pax.size = body.length this._emitHeader(pax) this.emit("data", body) // now the trailing buffer to make it an even number of 512 blocks var b = new Buffer(512 + (body.length % 512 || 512)) for (var i = 0, l = b.length; i < l; i ++) b[i] = 0 this.emit("data", b) } Generator.prototype._emitHeader = function (f) { var header = new Buffer(new Array(512)) , fields = tar.fields , offs = tar.fieldOffs , sz = tar.fieldSize addField(header, "NAME", f.name) addField(header, "MODE", f.mode) addField(header, "UID", f.uid) addField(header, "GID", f.gid) addField(header, "SIZE", f.size) addField(header, "MTIME", +f.mtime) // checksum is generated based on it being spaces // then it's written as: "######\0 " // where ### is a zero-lead 6-digit octal number addField(header, "CKSUM", " ") addField(header, "TYPE", f.type) addField(header, "LINKNAME", f.linkname || "") if (f.ustar) { console.error(">>> ustar!!") addField(header, "USTAR", tar.ustar) addField(header, "USTARVER", 0) addField(header, "UNAME", f.user || "") addField(header, "GNAME", f.group || "") if (f.dev) { addField(header, "DEVMAJ", f.dev.major || 0) addField(header, "DEVMIN", f.dev.minor || 0) } addField(header, "PREFIX", f.prefix) } else { console.error(">>> no ustar!") } // now the header is written except for checksum. var ck = 0 for (var i = 0; i < 512; i ++) ck += header[i] addField(header, "CKSUM", nF(ck, 7)) header[ offs[fields.CKSUM] + 7 ] = 0 this.emit("data", header) } function addField (buf, field, val) { var f = tar.fields[field] console.error("Adding field", field, val) val = typeof val === "number" ? nF(val, tar.fieldSize[f]) : new Buffer(val || "") val.copy(buf, tar.fieldOffs[f]) } function toBase256 (num, len) { console.error("toBase256", num, len) var positive = num > 0 , buf = new Buffer(len) if (!positive) { // rare and slow var b = num.toString(2).substr(1) , padTo = (len - 1) * 8 b = new Array(padTo - b.length + 1).join("0") + b // take the 2's complement var ht = b.match(/^([01]*)(10*)?$/) , head = ht[1] , tail = ht[2] head = head.split("1").join("2") .split("0").join("1") .split("2").join("0") b = head + tail buf[0] = 0xFF for (var i = 1; i < len; i ++) { buf[i] = parseInt(buf.substr(i * 8, 8), 2) } return buf } buf[0] = 0x80 for (var i = 1, l = len, p = l - 1; i < l; i ++, p --) { buf[p] = num % 256 num = Math.floor(num / 256) } return buf } function nF (num, size) { var ns = num.toString(8) if (num < 0 || ns.length >= size) { // make a base 256 buffer // then return it return toBase256(num, size) } var buf = new Buffer(size) ns = new Array(size - ns.length - 1).join("0") + ns + " " buf[size - 1] = 0 buf.asciiWrite(ns) return buf }