blob: a314ff61346da2b8a7182200bf620cdd0bddf7e5 [file] [log] [blame]
/**
* node-archiver
*
* Copyright (c) 2012-2014 Chris Talkington, contributors.
* Licensed under the MIT license.
* https://github.com/ctalkington/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) {
options = this.options = util.defaults(options, {
highWaterMark: 1024 * 1024
});
Transform.call(this, options);
this._entries = [];
this._module = false;
this._pointer = 0;
this._queue = async.queue(this._onQueueTask.bind(this), 1);
this._queue.drain = this._onQueueDrain.bind(this);
this._state = {
finalize: false,
finalized: false,
modulePiped: false
};
};
inherits(Archiver, Transform);
Archiver.prototype._append = function(filepath, data) {
data = data || {};
if (!data.name) {
data.name = filepath;
}
data.sourcePath = filepath;
this._queue.push({
data: data,
source: null,
deferredStat: true,
filepath: filepath
});
};
Archiver.prototype._moduleAppend = function(source, data, callback) {
this._module.append(source, data, callback);
};
Archiver.prototype._moduleFinalize = function() {
this._state.finalized = true;
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('format module missing finalize and end method'));
}
};
Archiver.prototype._moduleSupports = function(key) {
this._module.supports = util.defaults(this._module.supports, {
directory: false
});
return this._module.supports[key];
};
Archiver.prototype._normalizeEntryData = function(data, stats) {
stats = stats || false;
data = util.defaults(data, {
type: 'file',
name: null,
date: null,
mode: null,
sourcePath: null
});
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 (stats) {
data.mode = stats.mode & 0777;
} else {
data.mode = isDir ? 0755 : 0644;
}
if (stats && data.date === null) {
data.date = stats.mtime;
}
data.date = util.dateify(data.date);
data._stats = stats;
return data;
};
Archiver.prototype._onModuleError = function(err) {
this.emit('error', err);
};
Archiver.prototype._onQueueDrain = function() {
if (this._state.finalize && !this._state.finalized && this._queue.idle()) {
this._moduleFinalize();
}
};
Archiver.prototype._onQueueTask = function(task, callback) {
var afterAppend = function(err, entry) {
if (err) {
this.emit('error', err);
callback();
return;
}
entry = entry || task.data;
this.emit('entry', entry);
this._entries.push(entry);
callback();
}.bind(this);
var afterStat = function(err, stats) {
if (err) {
this.emit('error', err);
callback();
return;
}
task = this._updateQueueTaskWithStats(task, stats);
if (task.source !== null) {
this._moduleAppend(task.source, task.data, afterAppend);
} else {
this.emit('error', new Error('unsupported entry: ' + task.filepath));
callback();
return;
}
}.bind(this);
if (task.deferredStat) {
fs.stat(task.filepath, afterStat);
} else {
this._moduleAppend(task.source, task.data, afterAppend);
}
};
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._pipeModuleOutput = function() {
this._module.on('error', this._onModuleError.bind(this));
this._module.pipe(this);
this._state.modulePiped = true;
};
Archiver.prototype._processFile = function(source, data, callback) {
this.emit('error', new Error('method not implemented'));
};
Archiver.prototype._transform = function(chunk, encoding, callback) {
if (chunk) {
this._pointer += chunk.length;
}
callback(null, chunk);
};
Archiver.prototype.append = function(source, data) {
if (this._state.finalize) {
this.emit('error', new Error('unable to append after calling finalize.'));
return this;
}
data = this._normalizeEntryData(data);
if (typeof data.name !== 'string' || data.name.length === 0) {
this.emit('error', new Error('entry name must be a non-empty string value'));
return this;
}
if (data.type === 'directory' && !this._moduleSupports('directory')) {
this.emit('error', new Error('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('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.emit('error', new Error('unable to append after calling finalize.'));
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);
var name = isExpandedPair ? file.dest : util.unixifyPath(file.dest || '', filepath);
if (name === '.') {
return;
}
data.name = name;
self._append(filepath, data);
});
});
return this;
};
Archiver.prototype.file = function(filepath, data) {
if (this._state.finalize) {
this.emit('error', new Error('unable to append after calling finalize.'));
return this;
}
if (typeof filepath !== 'string' || filepath.length === 0) {
this.emit('error', new Error('filepath must be a non-empty string value'));
return this;
}
this._append(filepath, data);
return this;
};
Archiver.prototype.finalize = function() {
this._state.finalize = true;
if (this._queue.idle()) {
this._moduleFinalize();
}
return this;
};
Archiver.prototype.setModule = function(module) {
if (this._state.modulePiped) {
this.emit('error', new Error('format module already set'));
return;
}
this._module = module;
this._pipeModuleOutput();
};
Archiver.prototype.pointer = function() {
return this._pointer;
};