| /** |
| * node-archiver |
| * |
| * Copyright (c) 2012-2014 Chris Talkington, contributors. |
| * Licensed under the MIT license. |
| * https://github.com/archiverjs/node-archiver/blob/master/LICENSE-MIT |
| */ |
| var fs = require('fs'); |
| var inherits = require('util').inherits; |
| var Transform = require('readable-stream').Transform; |
| |
| var async = require('async'); |
| |
| var util = require('./util'); |
| |
| var Archiver = module.exports = function(options) { |
| if (!(this instanceof Archiver)) { |
| return new Archiver(options); |
| } |
| |
| options = this.options = util.defaults(options, { |
| highWaterMark: 1024 * 1024, |
| statConcurrency: 4 |
| }); |
| |
| Transform.call(this, options); |
| |
| this._entries = []; |
| this._format = false; |
| this._module = false; |
| this._pending = 0; |
| this._pointer = 0; |
| |
| this._queue = async.queue(this._onQueueTask.bind(this), 1); |
| this._queue.drain = this._onQueueDrain.bind(this); |
| |
| this._statQueue = async.queue(this._onStatQueueTask.bind(this), options.statConcurrency); |
| |
| this._state = { |
| aborted: false, |
| finalize: false, |
| finalizing: false, |
| finalized: false, |
| modulePiped: false |
| }; |
| }; |
| |
| inherits(Archiver, Transform); |
| |
| Archiver.prototype._abort = function() { |
| this._state.aborted = true; |
| this._queue.kill(); |
| this._statQueue.kill(); |
| |
| if (this._queue.idle()) { |
| this._shutdown(); |
| } |
| }; |
| |
| Archiver.prototype._append = function(filepath, data) { |
| data = data || {}; |
| |
| var task = { |
| source: null, |
| filepath: filepath |
| }; |
| |
| if (!data.name) { |
| data.name = filepath; |
| } |
| |
| data.sourcePath = filepath; |
| task.data = data; |
| |
| if (data.stats && data.stats instanceof fs.Stats) { |
| task = this._updateQueueTaskWithStats(task, data.stats); |
| this._queue.push(task); |
| } else { |
| this._statQueue.push(task); |
| } |
| }; |
| |
| Archiver.prototype._finalize = function() { |
| if (this._state.finalizing || this._state.finalized || this._state.aborted) { |
| return; |
| } |
| |
| this._state.finalizing = true; |
| |
| this._moduleFinalize(); |
| |
| this._state.finalizing = false; |
| this._state.finalized = true; |
| }; |
| |
| Archiver.prototype._maybeFinalize = function() { |
| if (this._state.finalizing || this._state.finalized || this._state.aborted) { |
| return false; |
| } |
| |
| if (this._state.finalize && this._pending === 0 && this._queue.idle() && this._statQueue.idle()) { |
| this._finalize(); |
| return true; |
| } |
| |
| return false; |
| }; |
| |
| Archiver.prototype._moduleAppend = function(source, data, callback) { |
| if (this._state.aborted) { |
| callback(); |
| return; |
| } |
| |
| this._module.append(source, data, function(err) { |
| this._task = null; |
| |
| if (this._state.aborted) { |
| this._shutdown(); |
| return; |
| } |
| |
| if (err) { |
| this.emit('error', err); |
| setImmediate(callback); |
| return; |
| } |
| |
| this.emit('entry', data); |
| this._entries.push(data); |
| |
| setImmediate(callback); |
| }.bind(this)); |
| }; |
| |
| Archiver.prototype._moduleFinalize = function() { |
| if (typeof this._module.finalize === 'function') { |
| this._module.finalize(); |
| } else if (typeof this._module.end === 'function') { |
| this._module.end(); |
| } else { |
| this.emit('error', new Error('module: no suitable finalize/end method found')); |
| return; |
| } |
| }; |
| |
| Archiver.prototype._modulePipe = function() { |
| this._module.on('error', this._onModuleError.bind(this)); |
| this._module.pipe(this); |
| this._state.modulePiped = true; |
| }; |
| |
| Archiver.prototype._moduleSupports = function(key) { |
| if (!this._module.supports || !this._module.supports[key]) { |
| return false; |
| } |
| |
| return this._module.supports[key]; |
| }; |
| |
| Archiver.prototype._moduleUnpipe = function() { |
| this._module.unpipe(this); |
| this._state.modulePiped = false; |
| }; |
| |
| Archiver.prototype._normalizeEntryData = function(data, stats) { |
| data = util.defaults(data, { |
| type: 'file', |
| name: null, |
| date: null, |
| mode: null, |
| sourcePath: null, |
| stats: false |
| }); |
| |
| if (stats && data.stats === false) { |
| data.stats = stats; |
| } |
| |
| var isDir = data.type === 'directory'; |
| |
| if (data.name) { |
| data.name = util.sanitizePath(data.name); |
| |
| if (data.name.slice(-1) === '/') { |
| isDir = true; |
| data.type = 'directory'; |
| } else if (isDir) { |
| data.name += '/'; |
| } |
| } |
| |
| if (typeof data.mode === 'number') { |
| data.mode &= 0777; |
| } else if (data.stats && data.mode === null) { |
| data.mode = data.stats.mode & 0777; |
| } else if (data.mode === null) { |
| data.mode = isDir ? 0755 : 0644; |
| } |
| |
| if (data.stats && data.date === null) { |
| data.date = data.stats.mtime; |
| } else { |
| data.date = util.dateify(data.date); |
| } |
| |
| return data; |
| }; |
| |
| Archiver.prototype._onModuleError = function(err) { |
| this.emit('error', err); |
| }; |
| |
| Archiver.prototype._onQueueDrain = function() { |
| if (this._state.finalizing || this._state.finalized || this._state.aborted) { |
| return; |
| } |
| |
| if (this._state.finalize && this._pending === 0 && this._queue.idle() && this._statQueue.idle()) { |
| this._finalize(); |
| return; |
| } |
| }; |
| |
| Archiver.prototype._onQueueTask = function(task, callback) { |
| if (this._state.finalizing || this._state.finalized || this._state.aborted) { |
| callback(); |
| return; |
| } |
| |
| this._task = task; |
| this._moduleAppend(task.source, task.data, callback); |
| }; |
| |
| Archiver.prototype._onStatQueueTask = function(task, callback) { |
| if (this._state.finalizing || this._state.finalized || this._state.aborted) { |
| callback(); |
| return; |
| } |
| |
| fs.stat(task.filepath, function(err, stats) { |
| if (this._state.aborted) { |
| setImmediate(callback); |
| return; |
| } |
| |
| if (err) { |
| this.emit('error', err); |
| setImmediate(callback); |
| return; |
| } |
| |
| task = this._updateQueueTaskWithStats(task, stats); |
| |
| if (task.source !== null) { |
| this._queue.push(task); |
| setImmediate(callback); |
| } else { |
| this.emit('error', new Error('unsupported entry: ' + task.filepath)); |
| setImmediate(callback); |
| return; |
| } |
| }.bind(this)); |
| }; |
| |
| Archiver.prototype._shutdown = function() { |
| this._moduleUnpipe(); |
| this.end(); |
| }; |
| |
| Archiver.prototype._transform = function(chunk, encoding, callback) { |
| if (chunk) { |
| this._pointer += chunk.length; |
| } |
| |
| callback(null, chunk); |
| }; |
| |
| Archiver.prototype._updateQueueTaskWithStats = function(task, stats) { |
| if (stats.isFile()) { |
| task.data.type = 'file'; |
| task.data.sourceType = 'stream'; |
| task.source = util.lazyReadStream(task.filepath); |
| } else if (stats.isDirectory() && this._moduleSupports('directory')) { |
| task.data.name = util.trailingSlashIt(task.data.name); |
| task.data.type = 'directory'; |
| task.data.sourcePath = util.trailingSlashIt(task.filepath); |
| task.data.sourceType = 'buffer'; |
| task.source = new Buffer(0); |
| } else { |
| return task; |
| } |
| |
| task.data = this._normalizeEntryData(task.data, stats); |
| return task; |
| }; |
| |
| Archiver.prototype.abort = function() { |
| if (this._state.aborted || this._state.finalized) { |
| return this; |
| } |
| |
| this._abort(); |
| |
| return this; |
| }; |
| |
| Archiver.prototype.append = function(source, data) { |
| if (this._state.finalize || this._state.aborted) { |
| this.emit('error', new Error('append: queue closed')); |
| return this; |
| } |
| |
| data = this._normalizeEntryData(data); |
| |
| if (typeof data.name !== 'string' || data.name.length === 0) { |
| this.emit('error', new Error('append: entry name must be a non-empty string value')); |
| return this; |
| } |
| |
| if (data.type === 'directory' && !this._moduleSupports('directory')) { |
| this.emit('error', new Error('append: entries of "directory" type not currently supported by this module')); |
| return this; |
| } |
| |
| source = util.normalizeInputSource(source); |
| |
| if (Buffer.isBuffer(source)) { |
| data.sourceType = 'buffer'; |
| } else if (util.isStream(source)) { |
| data.sourceType = 'stream'; |
| } else { |
| this.emit('error', new Error('append: input source must be valid Stream or Buffer instance')); |
| return this; |
| } |
| |
| this._queue.push({ |
| data: data, |
| source: source |
| }); |
| |
| return this; |
| }; |
| |
| Archiver.prototype.bulk = function(mappings) { |
| if (this._state.finalize || this._state.aborted) { |
| this.emit('error', new Error('bulk: queue closed')); |
| return this; |
| } |
| |
| if (!Array.isArray(mappings)) { |
| mappings = [mappings]; |
| } |
| |
| var self = this; |
| var files = util.file.normalizeFilesArray(mappings); |
| |
| files.forEach(function(file){ |
| var isExpandedPair = file.orig.expand || false; |
| var fileData = file.data || {}; |
| |
| file.src.forEach(function(filepath) { |
| var data = util._.extend({}, fileData); |
| data.name = isExpandedPair ? util.unixifyPath(file.dest) : util.unixifyPath(file.dest || '', filepath); |
| |
| if (data.name === '.') { |
| return; |
| } |
| |
| self._append(filepath, data); |
| }); |
| }); |
| |
| return this; |
| }; |
| |
| Archiver.prototype.directory = function(dirpath, destpath, data) { |
| if (this._state.finalize || this._state.aborted) { |
| this.emit('error', new Error('directory: queue closed')); |
| return this; |
| } |
| |
| if (typeof dirpath !== 'string' || dirpath.length === 0) { |
| this.emit('error', new Error('directory: dirpath must be a non-empty string value')); |
| return this; |
| } |
| |
| this._pending++; |
| |
| if (destpath === false) { |
| destpath = ''; |
| } else if (typeof destpath !== 'string'){ |
| destpath = dirpath; |
| } |
| |
| if (typeof data !== 'object') { |
| data = {}; |
| } |
| |
| var self = this; |
| |
| util.walkdir(dirpath, function(err, results) { |
| if (err) { |
| self.emit('error', err); |
| } else { |
| results.forEach(function(file) { |
| var entryData = util._.extend({}, data); |
| entryData.name = util.sanitizePath(destpath, file.relative); |
| entryData.stats = file.stats; |
| |
| self._append(file.path, entryData); |
| }); |
| } |
| |
| self._pending--; |
| self._maybeFinalize(); |
| }); |
| |
| return this; |
| }; |
| |
| Archiver.prototype.file = function(filepath, data) { |
| if (this._state.finalize || this._state.aborted) { |
| this.emit('error', new Error('file: queue closed')); |
| return this; |
| } |
| |
| if (typeof filepath !== 'string' || filepath.length === 0) { |
| this.emit('error', new Error('file: filepath must be a non-empty string value')); |
| return this; |
| } |
| |
| this._append(filepath, data); |
| |
| return this; |
| }; |
| |
| Archiver.prototype.finalize = function() { |
| if (this._state.aborted) { |
| this.emit('error', new Error('finalize: archive was aborted')); |
| return this; |
| } |
| |
| if (this._state.finalize) { |
| this.emit('error', new Error('finalize: archive already finalizing')); |
| return this; |
| } |
| |
| this._state.finalize = true; |
| |
| if (this._pending === 0 && this._queue.idle() && this._statQueue.idle()) { |
| this._finalize(); |
| } |
| |
| return this; |
| }; |
| |
| Archiver.prototype.setFormat = function(format) { |
| if (this._format) { |
| this.emit('error', new Error('format: archive format already set')); |
| return this; |
| } |
| |
| this._format = format; |
| |
| return this; |
| }; |
| |
| Archiver.prototype.setModule = function(module) { |
| if (this._state.aborted) { |
| this.emit('error', new Error('module: archive was aborted')); |
| return this; |
| } |
| |
| if (this._state.module) { |
| this.emit('error', new Error('module: module already set')); |
| return this; |
| } |
| |
| this._module = module; |
| this._modulePipe(); |
| |
| return this; |
| }; |
| |
| Archiver.prototype.pointer = function() { |
| return this._pointer; |
| }; |