extractors.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. var stream = require('stream');
  2. if (!stream.Readable) {
  3. var stream = require('readable-stream');
  4. }
  5. var fs = require('graceful-fs');
  6. var Q = require('q');
  7. var path = require('path');
  8. var zlib = require('zlib');
  9. var touch = Q.denodeify(require('touch'));
  10. var mkpath = Q.denodeify(require('mkpath'));
  11. var writeFile = Q.denodeify(fs.writeFile);
  12. var inflateRaw = Q.denodeify(zlib.inflateRaw);
  13. var symlink = Q.denodeify(fs.symlink);
  14. var stat = Q.denodeify(fs.stat);
  15. // Use a cache of promises for building the directory tree. This allows us to
  16. // correctly queue up file extractions for after their path has been created,
  17. // avoid trying to create the path twice and still be async.
  18. var mkdir = function (dir, cache, mode) {
  19. dir = path.normalize(path.resolve(process.cwd(), dir) + path.sep);
  20. if (mode === undefined) {
  21. mode = parseInt('777', 8) & (~process.umask());
  22. }
  23. if (!cache[dir]) {
  24. var parent;
  25. if (fs.existsSync(dir)) {
  26. parent = new Q();
  27. } else {
  28. parent = mkdir(path.dirname(dir), cache, mode);
  29. }
  30. cache[dir] = parent.then(function () {
  31. return mkpath(dir, mode);
  32. });
  33. }
  34. return cache[dir];
  35. };
  36. // Utility methods for writing output files
  37. var extractors = {
  38. folder: function (folder, destination, zip) {
  39. return mkdir(destination, zip.dirCache, folder.mode)
  40. .then(function () {
  41. return {folder: folder.path};
  42. });
  43. },
  44. store: function (file, destination, zip) {
  45. var writer;
  46. if (file.uncompressedSize === 0) {
  47. writer = touch.bind(null, destination);
  48. } else if (file.uncompressedSize <= zip.chunkSize) {
  49. writer = function () {
  50. return zip.getBuffer(file._offset, file._offset + file.uncompressedSize)
  51. .then(function (buffer) {
  52. return writeFile(destination, buffer, { mode: file.mode });
  53. });
  54. };
  55. } else {
  56. var input = new stream.Readable();
  57. input.wrap(fs.createReadStream(zip.filename, {start: file._offset, end: file._offset + file.uncompressedSize - 1}));
  58. writer = pipePromise.bind(null, input, destination, { mode: file.mode });
  59. }
  60. return mkdir(path.dirname(destination), zip.dirCache)
  61. .then(writer)
  62. .then(function () {
  63. return {stored: file.path};
  64. });
  65. },
  66. deflate: function (file, destination, zip) {
  67. // For Deflate you don't actually need to specify the end offset - and
  68. // in fact many ZIP files don't include compressed file sizes for
  69. // Deflated files so we don't even know what the end offset is.
  70. return mkdir(path.dirname(destination), zip.dirCache)
  71. .then(function () {
  72. if (file._maxSize <= zip.chunkSize) {
  73. return zip.getBuffer(file._offset, file._offset + file._maxSize)
  74. .then(inflateRaw)
  75. .then(function (buffer) {
  76. return writeFile(destination, buffer, { mode: file.mode });
  77. });
  78. } else {
  79. // For node 0.8 we need to create the Zlib stream and attach
  80. // handlers in the same tick of the event loop, which is why we do
  81. // the creation in here
  82. var input = new stream.Readable();
  83. input.wrap(fs.createReadStream(zip.filename, {start: file._offset}));
  84. var inflater = input.pipe(zlib.createInflateRaw({highWaterMark: 32 * 1024}));
  85. return pipePromise(inflater, destination, { mode: file.mode });
  86. }
  87. })
  88. .then(function () {
  89. return {deflated: file.path};
  90. });
  91. },
  92. symlink: function (file, destination, zip, basePath) {
  93. var parent = path.dirname(destination);
  94. return mkdir(parent, zip.dirCache)
  95. .then(function () {
  96. return getLinkLocation(file, destination, zip, basePath);
  97. })
  98. .then(function (linkTo) {
  99. return symlink(path.resolve(parent, linkTo), destination)
  100. .then(function () {
  101. return {symlink: file.path, linkTo: linkTo};
  102. });
  103. });
  104. },
  105. // Make a shallow copy of the file/directory this symlink points to instead
  106. // of actually creating a link
  107. copy: function (file, destination, zip, basePath) {
  108. var type;
  109. var parent = path.dirname(destination);
  110. return mkdir(parent, zip.dirCache)
  111. .then(function () {
  112. return getLinkLocation(file, destination, zip, basePath);
  113. })
  114. .then(function (linkTo) {
  115. return stat(path.resolve(parent, linkTo))
  116. .then(function (stats) {
  117. if (stats.isFile()) {
  118. type = 'File';
  119. var input = new stream.Readable();
  120. input.wrap(fs.createReadStream(path.resolve(parent, linkTo)));
  121. return pipePromise(input, destination);
  122. } else if (stats.isDirectory()) {
  123. type = 'Directory';
  124. return mkdir(destination, zip.dirCache);
  125. } else {
  126. throw new Error('Could not follow symlink to unknown file type');
  127. }
  128. })
  129. .then(function () {
  130. return {copy: file.path, original: linkTo, type: type};
  131. });
  132. });
  133. }
  134. };
  135. var getLinkLocation = function (file, destination, zip, basePath) {
  136. var parent = path.dirname(destination);
  137. return zip.getBuffer(file._offset, file._offset + file.uncompressedSize)
  138. .then(function (buffer) {
  139. var linkTo = buffer.toString();
  140. var fullLink = path.resolve(parent, linkTo);
  141. if (path.relative(basePath, fullLink).slice(0, 2) === '..') {
  142. throw new Error('Symlink links outside archive');
  143. }
  144. return linkTo;
  145. });
  146. };
  147. var pipePromise = function (input, destination, options) {
  148. var deferred = Q.defer();
  149. var output = fs.createWriteStream(destination, options);
  150. var errorHandler = function (error) {
  151. deferred.reject(error);
  152. };
  153. input.on('error', errorHandler);
  154. output.on('error', errorHandler);
  155. // For node 0.8 we can't just use the 'finish' event of the pipe
  156. input.on('end', function () {
  157. output.end(function () {
  158. deferred.resolve();
  159. });
  160. });
  161. input.pipe(output, {end: false});
  162. return deferred.promise;
  163. };
  164. module.exports = extractors;