code-push-cli
Version:
Management CLI for the CodePush service
215 lines (213 loc) • 7.84 kB
JavaScript
/**
* NOTE!!! This utility file is duplicated for use by the CodePush service (for server-driven hashing/
* integrity checks) and Management SDK (for end-to-end code signing), please keep them in sync.
*/
var crypto = require("crypto");
var fs = require("fs");
var path = require("path");
var q = require("q");
// Do not throw an exception if either of these modules are missing, as they may not be needed by the
// consumer of this file.
// - recursiveFs: Only required for hashing of directories
// - yauzl: Only required for in-memory hashing of zip files
try {
var recursiveFs = require("recursive-fs");
}
catch (e) { }
try {
var yauzl = require("yauzl");
}
catch (e) { }
var HASH_ALGORITHM = "sha256";
function generatePackageHashFromDirectory(directoryPath, basePath) {
if (!fs.lstatSync(directoryPath).isDirectory()) {
throw new Error("Not a directory. Please either create a directory, or use hashFile().");
}
return generatePackageManifestFromDirectory(directoryPath, basePath)
.then(function (manifest) {
return manifest.computePackageHash();
});
}
exports.generatePackageHashFromDirectory = generatePackageHashFromDirectory;
function generatePackageManifestFromZip(filePath) {
var deferred = q.defer();
var reject = function (error) {
if (deferred.promise.isPending()) {
deferred.reject(error);
}
};
var resolve = function (manifest) {
if (deferred.promise.isPending()) {
deferred.resolve(manifest);
}
};
var zipFile;
yauzl.open(filePath, { lazyEntries: true }, function (error, openedZipFile) {
if (error) {
// This is the first time we try to read the package as a .zip file;
// however, it may not be a .zip file. Handle this gracefully.
resolve(null);
return;
}
zipFile = openedZipFile;
var fileHashesMap = new Map();
var hashFilePromises = [];
// Read each entry in the archive sequentially and generate a hash for it.
zipFile.readEntry();
zipFile
.on("error", function (error) {
reject(error);
})
.on("entry", function (entry) {
var fileName = PackageManifest.normalizePath(entry.fileName);
if (PackageManifest.isIgnored(fileName)) {
zipFile.readEntry();
return;
}
zipFile.openReadStream(entry, function (error, readStream) {
if (error) {
reject(error);
return;
}
hashFilePromises.push(hashStream(readStream)
.then(function (hash) {
fileHashesMap.set(fileName, hash);
zipFile.readEntry();
}, reject));
});
})
.on("end", function () {
q.all(hashFilePromises).then(function () { return resolve(new PackageManifest(fileHashesMap)); }, reject);
});
});
return deferred.promise
.finally(function () { return zipFile && zipFile.close(); });
}
exports.generatePackageManifestFromZip = generatePackageManifestFromZip;
function generatePackageManifestFromDirectory(directoryPath, basePath) {
var deferred = q.defer();
var fileHashesMap = new Map();
recursiveFs.readdirr(directoryPath, function (error, directories, files) {
if (error) {
deferred.reject(error);
return;
}
if (!files || files.length === 0) {
deferred.reject("Error: Can't sign the release because no files were found.");
return;
}
// Hash the files sequentially, because streaming them in parallel is not necessarily faster
var generateManifestPromise = files.reduce(function (soFar, filePath) {
return soFar
.then(function () {
var relativePath = PackageManifest.normalizePath(path.relative(basePath, filePath));
if (!PackageManifest.isIgnored(relativePath)) {
return hashFile(filePath)
.then(function (hash) {
fileHashesMap.set(relativePath, hash);
});
}
});
}, q(null));
generateManifestPromise
.then(function () {
deferred.resolve(new PackageManifest(fileHashesMap));
}, deferred.reject)
.done();
});
return deferred.promise;
}
exports.generatePackageManifestFromDirectory = generatePackageManifestFromDirectory;
function hashFile(filePath) {
var readStream = fs.createReadStream(filePath);
return hashStream(readStream);
}
exports.hashFile = hashFile;
function hashStream(readStream) {
var hashStream = crypto.createHash(HASH_ALGORITHM);
var deferred = q.defer();
readStream
.on("error", function (error) {
if (deferred.promise.isPending()) {
hashStream.end();
deferred.reject(error);
}
})
.on("end", function () {
if (deferred.promise.isPending()) {
hashStream.end();
var buffer = hashStream.read();
var hash = buffer.toString("hex");
deferred.resolve(hash);
}
});
readStream.pipe(hashStream);
return deferred.promise;
}
exports.hashStream = hashStream;
var PackageManifest = (function () {
function PackageManifest(map) {
if (!map) {
map = new Map();
}
this._map = map;
}
PackageManifest.prototype.toMap = function () {
return this._map;
};
PackageManifest.prototype.computePackageHash = function () {
var entries = [];
this._map.forEach(function (hash, name) {
entries.push(name + ":" + hash);
});
// Make sure this list is alphabetically ordered so that other clients
// can also compute this hash easily given the update contents.
entries = entries.sort();
return q(crypto.createHash(HASH_ALGORITHM)
.update(JSON.stringify(entries))
.digest("hex"));
};
PackageManifest.prototype.serialize = function () {
var obj = {};
this._map.forEach(function (value, key) {
obj[key] = value;
});
return JSON.stringify(obj);
};
PackageManifest.deserialize = function (serializedContents) {
try {
var obj = JSON.parse(serializedContents);
var map = new Map();
for (var _i = 0, _a = Object.keys(obj); _i < _a.length; _i++) {
var key = _a[_i];
map.set(key, obj[key]);
}
return new PackageManifest(map);
}
catch (e) {
}
};
PackageManifest.normalizePath = function (filePath) {
//replace all backslashes coming from cli running on windows machines by slashes
return filePath.replace(/\\/g, "/");
};
PackageManifest.isIgnored = function (relativeFilePath) {
var __MACOSX = "__MACOSX/";
var DS_STORE = ".DS_Store";
var CODEPUSH_METADATA = ".codepushrelease";
return startsWith(relativeFilePath, __MACOSX)
|| relativeFilePath === DS_STORE
|| endsWith(relativeFilePath, "/" + DS_STORE)
|| relativeFilePath === CODEPUSH_METADATA
|| endsWith(relativeFilePath, "/" + CODEPUSH_METADATA);
};
return PackageManifest;
})();
exports.PackageManifest = PackageManifest;
function startsWith(str, prefix) {
return str && str.substring(0, prefix.length) === prefix;
}
function endsWith(str, suffix) {
return str && str.indexOf(suffix, str.length - suffix.length) !== -1;
}
//# sourceMappingURL=hash-utils.js.map