extractors.js 6.41 KB
Newer Older
Takter.10's avatar
Takter.10 committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184
var stream = require('stream');
if (!stream.Readable) {
    var stream = require('readable-stream');
}
var fs = require('graceful-fs');
var Q = require('q');
var path = require('path');
var zlib = require('zlib');
var touch = Q.denodeify(require('touch'));
var mkpath = Q.denodeify(require('mkpath'));
var writeFile = Q.denodeify(fs.writeFile);
var inflateRaw = Q.denodeify(zlib.inflateRaw);
var symlink = Q.denodeify(fs.symlink);
var stat = Q.denodeify(fs.stat);

// Use a cache of promises for building the directory tree. This allows us to
// correctly queue up file extractions for after their path has been created,
// avoid trying to create the path twice and still be async.
var mkdir = function (dir, cache, mode) {
    dir = path.normalize(path.resolve(process.cwd(), dir) + path.sep);
    if (mode === undefined) {
        mode = parseInt('777', 8) & (~process.umask());
    }

    if (!cache[dir]) {
        var parent;

        if (fs.existsSync(dir)) {
            parent = new Q();
        } else {
            parent = mkdir(path.dirname(dir), cache, mode);
        }

        cache[dir] = parent.then(function () {
            return mkpath(dir, mode);
        });
    }

    return cache[dir];
};

// Utility methods for writing output files
var extractors = {
    folder: function (folder, destination, zip) {
        return mkdir(destination, zip.dirCache, folder.mode)
        .then(function () {
            return {folder: folder.path};
        });
    },
    store: function (file, destination, zip) {
        var writer;

        if (file.uncompressedSize === 0) {
            writer = touch.bind(null, destination);
        } else if (file.uncompressedSize <= zip.chunkSize) {
            writer = function () {
                return zip.getBuffer(file._offset, file._offset + file.uncompressedSize)
                .then(function (buffer) {
                    return writeFile(destination, buffer, { mode: file.mode });
                });
            };
        } else {
            var input = new stream.Readable();
            input.wrap(fs.createReadStream(zip.filename, {start: file._offset, end: file._offset + file.uncompressedSize - 1}));
            writer = pipePromise.bind(null, input, destination, { mode: file.mode });
        }

        return mkdir(path.dirname(destination), zip.dirCache)
        .then(writer)
        .then(function () {
            return {stored: file.path};
        });
    },
    deflate: function (file, destination, zip) {
        // For Deflate you don't actually need to specify the end offset - and
        // in fact many ZIP files don't include compressed file sizes for
        // Deflated files so we don't even know what the end offset is.

        return mkdir(path.dirname(destination), zip.dirCache)
        .then(function () {
            if (file._maxSize <= zip.chunkSize) {
                return zip.getBuffer(file._offset, file._offset + file._maxSize)
                .then(inflateRaw)
                .then(function (buffer) {
                    return writeFile(destination, buffer, { mode: file.mode });
                });
            } else {
                // For node 0.8 we need to create the Zlib stream and attach
                // handlers in the same tick of the event loop, which is why we do
                // the creation in here
                var input = new stream.Readable();
                input.wrap(fs.createReadStream(zip.filename, {start: file._offset}));
                var inflater = input.pipe(zlib.createInflateRaw({highWaterMark: 32 * 1024}));

                return pipePromise(inflater, destination, { mode: file.mode });
            }
        })
        .then(function () {
            return {deflated: file.path};
        });
    },
    symlink: function (file, destination, zip, basePath) {
        var parent = path.dirname(destination);
        return mkdir(parent, zip.dirCache)
        .then(function () {
            return getLinkLocation(file, destination, zip, basePath);
        })
        .then(function (linkTo) {
            return symlink(path.resolve(parent, linkTo), destination)
            .then(function () {
                return {symlink: file.path, linkTo: linkTo};
            });
        });
    },
    // Make a shallow copy of the file/directory this symlink points to instead
    // of actually creating a link
    copy: function (file, destination, zip, basePath) {
        var type;
        var parent = path.dirname(destination);

        return mkdir(parent, zip.dirCache)
        .then(function () {
            return getLinkLocation(file, destination, zip, basePath);
        })
        .then(function (linkTo) {
            return stat(path.resolve(parent, linkTo))
            .then(function (stats) {
                if (stats.isFile()) {
                    type = 'File';
                    var input = new stream.Readable();
                    input.wrap(fs.createReadStream(path.resolve(parent, linkTo)));
                    return pipePromise(input, destination);
                } else if (stats.isDirectory()) {
                    type = 'Directory';
                    return mkdir(destination, zip.dirCache);
                } else {
                    throw new Error('Could not follow symlink to unknown file type');
                }
            })
            .then(function () {
                return {copy: file.path, original: linkTo, type: type};
            });
        });
    }
};

var getLinkLocation = function (file, destination, zip, basePath) {
    var parent = path.dirname(destination);
    return zip.getBuffer(file._offset, file._offset + file.uncompressedSize)
    .then(function (buffer) {
        var linkTo = buffer.toString();
        var fullLink = path.resolve(parent, linkTo);

        if (path.relative(basePath, fullLink).slice(0, 2) === '..') {
            throw new Error('Symlink links outside archive');
        }

        return linkTo;
    });
};

var pipePromise = function (input, destination, options) {
    var deferred = Q.defer();
    var output = fs.createWriteStream(destination, options);
    var errorHandler = function (error) {
        deferred.reject(error);
    };

    input.on('error', errorHandler);
    output.on('error', errorHandler);

    // For node 0.8 we can't just use the 'finish' event of the pipe
    input.on('end', function () {
        output.end(function () {
            deferred.resolve();
        });
    });

    input.pipe(output, {end: false});

    return deferred.promise;
};

module.exports = extractors;