UNPKG

11.7 kBJavaScriptView Raw
1/**
2 * node-archiver
3 *
4 * Copyright (c) 2012-2014 Chris Talkington, contributors.
5 * Licensed under the MIT license.
6 * https://github.com/archiverjs/node-archiver/blob/master/LICENSE-MIT
7 */
8var fs = require('fs');
9var inherits = require('util').inherits;
10var Transform = require('readable-stream').Transform;
11
12var async = require('async');
13
14var util = require('./util');
15
16var Archiver = module.exports = function(options) {
17 if (!(this instanceof Archiver)) {
18 return new Archiver(options);
19 }
20
21 options = this.options = util.defaults(options, {
22 highWaterMark: 1024 * 1024,
23 statConcurrency: 4
24 });
25
26 Transform.call(this, options);
27
28 this._entries = [];
29 this._format = false;
30 this._module = false;
31 this._pending = 0;
32 this._pointer = 0;
33
34 this._queue = async.queue(this._onQueueTask.bind(this), 1);
35 this._queue.drain = this._onQueueDrain.bind(this);
36
37 this._statQueue = async.queue(this._onStatQueueTask.bind(this), options.statConcurrency);
38
39 this._state = {
40 aborted: false,
41 finalize: false,
42 finalizing: false,
43 finalized: false,
44 modulePiped: false
45 };
46};
47
48inherits(Archiver, Transform);
49
50Archiver.prototype._abort = function() {
51 this._state.aborted = true;
52 this._queue.kill();
53 this._statQueue.kill();
54
55 if (this._queue.idle()) {
56 this._shutdown();
57 }
58};
59
60Archiver.prototype._append = function(filepath, data) {
61 data = data || {};
62
63 var task = {
64 source: null,
65 filepath: filepath
66 };
67
68 if (!data.name) {
69 data.name = filepath;
70 }
71
72 data.sourcePath = filepath;
73 task.data = data;
74
75 if (data.stats && data.stats instanceof fs.Stats) {
76 task = this._updateQueueTaskWithStats(task, data.stats);
77 this._queue.push(task);
78 } else {
79 this._statQueue.push(task);
80 }
81};
82
83Archiver.prototype._finalize = function() {
84 if (this._state.finalizing || this._state.finalized || this._state.aborted) {
85 return;
86 }
87
88 this._state.finalizing = true;
89
90 this._moduleFinalize();
91
92 this._state.finalizing = false;
93 this._state.finalized = true;
94};
95
96Archiver.prototype._maybeFinalize = function() {
97 if (this._state.finalizing || this._state.finalized || this._state.aborted) {
98 return false;
99 }
100
101 if (this._state.finalize && this._pending === 0 && this._queue.idle() && this._statQueue.idle()) {
102 this._finalize();
103 return true;
104 }
105
106 return false;
107};
108
109Archiver.prototype._moduleAppend = function(source, data, callback) {
110 if (this._state.aborted) {
111 callback();
112 return;
113 }
114
115 this._module.append(source, data, function(err) {
116 this._task = null;
117
118 if (this._state.aborted) {
119 this._shutdown();
120 return;
121 }
122
123 if (err) {
124 this.emit('error', err);
125 setImmediate(callback);
126 return;
127 }
128
129 this.emit('entry', data);
130 this._entries.push(data);
131
132 setImmediate(callback);
133 }.bind(this));
134};
135
136Archiver.prototype._moduleFinalize = function() {
137 if (typeof this._module.finalize === 'function') {
138 this._module.finalize();
139 } else if (typeof this._module.end === 'function') {
140 this._module.end();
141 } else {
142 this.emit('error', new Error('module: no suitable finalize/end method found'));
143 return;
144 }
145};
146
147Archiver.prototype._modulePipe = function() {
148 this._module.on('error', this._onModuleError.bind(this));
149 this._module.pipe(this);
150 this._state.modulePiped = true;
151};
152
153Archiver.prototype._moduleSupports = function(key) {
154 if (!this._module.supports || !this._module.supports[key]) {
155 return false;
156 }
157
158 return this._module.supports[key];
159};
160
161Archiver.prototype._moduleUnpipe = function() {
162 this._module.unpipe(this);
163 this._state.modulePiped = false;
164};
165
166Archiver.prototype._normalizeEntryData = function(data, stats) {
167 data = util.defaults(data, {
168 type: 'file',
169 name: null,
170 date: null,
171 mode: null,
172 sourcePath: null,
173 stats: false
174 });
175
176 if (stats && data.stats === false) {
177 data.stats = stats;
178 }
179
180 var isDir = data.type === 'directory';
181
182 if (data.name) {
183 data.name = util.sanitizePath(data.name);
184
185 if (data.name.slice(-1) === '/') {
186 isDir = true;
187 data.type = 'directory';
188 } else if (isDir) {
189 data.name += '/';
190 }
191 }
192
193 if (typeof data.mode === 'number') {
194 data.mode &= 0777;
195 } else if (data.stats && data.mode === null) {
196 data.mode = data.stats.mode & 0777;
197 } else if (data.mode === null) {
198 data.mode = isDir ? 0755 : 0644;
199 }
200
201 if (data.stats && data.date === null) {
202 data.date = data.stats.mtime;
203 } else {
204 data.date = util.dateify(data.date);
205 }
206
207 return data;
208};
209
210Archiver.prototype._onModuleError = function(err) {
211 this.emit('error', err);
212};
213
214Archiver.prototype._onQueueDrain = function() {
215 if (this._state.finalizing || this._state.finalized || this._state.aborted) {
216 return;
217 }
218
219 if (this._state.finalize && this._pending === 0 && this._queue.idle() && this._statQueue.idle()) {
220 this._finalize();
221 return;
222 }
223};
224
225Archiver.prototype._onQueueTask = function(task, callback) {
226 if (this._state.finalizing || this._state.finalized || this._state.aborted) {
227 callback();
228 return;
229 }
230
231 this._task = task;
232 this._moduleAppend(task.source, task.data, callback);
233};
234
235Archiver.prototype._onStatQueueTask = function(task, callback) {
236 if (this._state.finalizing || this._state.finalized || this._state.aborted) {
237 callback();
238 return;
239 }
240
241 fs.stat(task.filepath, function(err, stats) {
242 if (this._state.aborted) {
243 setImmediate(callback);
244 return;
245 }
246
247 if (err) {
248 this.emit('error', err);
249 setImmediate(callback);
250 return;
251 }
252
253 task = this._updateQueueTaskWithStats(task, stats);
254
255 if (task.source !== null) {
256 this._queue.push(task);
257 setImmediate(callback);
258 } else {
259 this.emit('error', new Error('unsupported entry: ' + task.filepath));
260 setImmediate(callback);
261 return;
262 }
263 }.bind(this));
264};
265
266Archiver.prototype._shutdown = function() {
267 this._moduleUnpipe();
268 this.end();
269};
270
271Archiver.prototype._transform = function(chunk, encoding, callback) {
272 if (chunk) {
273 this._pointer += chunk.length;
274 }
275
276 callback(null, chunk);
277};
278
279Archiver.prototype._updateQueueTaskWithStats = function(task, stats) {
280 if (stats.isFile()) {
281 task.data.type = 'file';
282 task.data.sourceType = 'stream';
283 task.source = util.lazyReadStream(task.filepath);
284 } else if (stats.isDirectory() && this._moduleSupports('directory')) {
285 task.data.name = util.trailingSlashIt(task.data.name);
286 task.data.type = 'directory';
287 task.data.sourcePath = util.trailingSlashIt(task.filepath);
288 task.data.sourceType = 'buffer';
289 task.source = new Buffer(0);
290 } else {
291 return task;
292 }
293
294 task.data = this._normalizeEntryData(task.data, stats);
295 return task;
296};
297
298Archiver.prototype.abort = function() {
299 if (this._state.aborted || this._state.finalized) {
300 return this;
301 }
302
303 this._abort();
304
305 return this;
306};
307
308Archiver.prototype.append = function(source, data) {
309 if (this._state.finalize || this._state.aborted) {
310 this.emit('error', new Error('append: queue closed'));
311 return this;
312 }
313
314 data = this._normalizeEntryData(data);
315
316 if (typeof data.name !== 'string' || data.name.length === 0) {
317 this.emit('error', new Error('append: entry name must be a non-empty string value'));
318 return this;
319 }
320
321 if (data.type === 'directory' && !this._moduleSupports('directory')) {
322 this.emit('error', new Error('append: entries of "directory" type not currently supported by this module'));
323 return this;
324 }
325
326 source = util.normalizeInputSource(source);
327
328 if (Buffer.isBuffer(source)) {
329 data.sourceType = 'buffer';
330 } else if (util.isStream(source)) {
331 data.sourceType = 'stream';
332 } else {
333 this.emit('error', new Error('append: input source must be valid Stream or Buffer instance'));
334 return this;
335 }
336
337 this._queue.push({
338 data: data,
339 source: source
340 });
341
342 return this;
343};
344
345Archiver.prototype.bulk = function(mappings) {
346 if (this._state.finalize || this._state.aborted) {
347 this.emit('error', new Error('bulk: queue closed'));
348 return this;
349 }
350
351 if (!Array.isArray(mappings)) {
352 mappings = [mappings];
353 }
354
355 var self = this;
356 var files = util.file.normalizeFilesArray(mappings);
357
358 files.forEach(function(file){
359 var isExpandedPair = file.orig.expand || false;
360 var fileData = file.data || {};
361
362 file.src.forEach(function(filepath) {
363 var data = util._.extend({}, fileData);
364 data.name = isExpandedPair ? util.unixifyPath(file.dest) : util.unixifyPath(file.dest || '', filepath);
365
366 if (data.name === '.') {
367 return;
368 }
369
370 self._append(filepath, data);
371 });
372 });
373
374 return this;
375};
376
377Archiver.prototype.directory = function(dirpath, destpath, data) {
378 if (this._state.finalize || this._state.aborted) {
379 this.emit('error', new Error('directory: queue closed'));
380 return this;
381 }
382
383 if (typeof dirpath !== 'string' || dirpath.length === 0) {
384 this.emit('error', new Error('directory: dirpath must be a non-empty string value'));
385 return this;
386 }
387
388 this._pending++;
389
390 if (destpath === false) {
391 destpath = '';
392 } else if (typeof destpath !== 'string'){
393 destpath = dirpath;
394 }
395
396 if (typeof data !== 'object') {
397 data = {};
398 }
399
400 var self = this;
401
402 util.walkdir(dirpath, function(err, results) {
403 if (err) {
404 self.emit('error', err);
405 } else {
406 results.forEach(function(file) {
407 var entryData = util._.extend({}, data);
408 entryData.name = util.sanitizePath(destpath, file.relative);
409 entryData.stats = file.stats;
410
411 self._append(file.path, entryData);
412 });
413 }
414
415 self._pending--;
416 self._maybeFinalize();
417 });
418
419 return this;
420};
421
422Archiver.prototype.file = function(filepath, data) {
423 if (this._state.finalize || this._state.aborted) {
424 this.emit('error', new Error('file: queue closed'));
425 return this;
426 }
427
428 if (typeof filepath !== 'string' || filepath.length === 0) {
429 this.emit('error', new Error('file: filepath must be a non-empty string value'));
430 return this;
431 }
432
433 this._append(filepath, data);
434
435 return this;
436};
437
438Archiver.prototype.finalize = function() {
439 if (this._state.aborted) {
440 this.emit('error', new Error('finalize: archive was aborted'));
441 return this;
442 }
443
444 if (this._state.finalize) {
445 this.emit('error', new Error('finalize: archive already finalizing'));
446 return this;
447 }
448
449 this._state.finalize = true;
450
451 if (this._pending === 0 && this._queue.idle() && this._statQueue.idle()) {
452 this._finalize();
453 }
454
455 return this;
456};
457
458Archiver.prototype.setFormat = function(format) {
459 if (this._format) {
460 this.emit('error', new Error('format: archive format already set'));
461 return this;
462 }
463
464 this._format = format;
465
466 return this;
467};
468
469Archiver.prototype.setModule = function(module) {
470 if (this._state.aborted) {
471 this.emit('error', new Error('module: archive was aborted'));
472 return this;
473 }
474
475 if (this._state.module) {
476 this.emit('error', new Error('module: module already set'));
477 return this;
478 }
479
480 this._module = module;
481 this._modulePipe();
482
483 return this;
484};
485
486Archiver.prototype.pointer = function() {
487 return this._pointer;
488};
\No newline at end of file