UNPKG

appcenter-cli

Version:

Command line tool for Visual Studio App Center

160 lines (131 loc) 5.03 kB
/** * 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. */ import * as crypto from "crypto"; import * as fs from "fs"; import * as pfs from "../../../util/misc/promisfied-fs"; import * as path from "path"; import * as stream from "stream"; import * as _ from "lodash"; import { isDirectory } from "./file-utils"; // Do not throw an exception if either of these modules are missing, as they may not be needed by the // consumer of this file. const HASH_ALGORITHM = "sha256"; export class PackageManifest { private _map: Map<string, string>; public constructor(map?: Map<string, string>) { if (!map) { map = new Map<string, string>(); } this._map = map; } public toMap(): Map<string, string> { return this._map; } public computePackageHash(): string { let entries: string[] = []; this._map.forEach((hash: string, name: string): void => { 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 crypto.createHash(HASH_ALGORITHM) .update(JSON.stringify(entries)) .digest("hex"); } public serialize(): string { const obj: any = {}; this._map.forEach(function (value, key) { obj[key] = value; }); return JSON.stringify(obj); } public static deserialize(serializedContents: string): PackageManifest { try { const obj: any = JSON.parse(serializedContents); const map = new Map<string, string>(); for (const key of Object.keys(obj)) { map.set(key, obj[key]); } return new PackageManifest(map); } catch (e) { // Eat it return; } } public static normalizePath(filePath: string): string { //replace all backslashes coming from cli running on windows machines by slashes return filePath.replace(/\\/g, "/"); } public static isIgnored(relativeFilePath: string): boolean { const __MACOSX = "__MACOSX/"; const DS_STORE = ".DS_Store"; const CODEPUSH_METADATA = ".codepushrelease"; return _.startsWith(relativeFilePath, __MACOSX) || relativeFilePath === DS_STORE || _.endsWith(relativeFilePath, "/" + DS_STORE) || relativeFilePath === CODEPUSH_METADATA || _.endsWith(relativeFilePath, "/" + CODEPUSH_METADATA); } } export async function generatePackageHashFromDirectory(directoryPath: string, basePath: string): Promise<string> { try { if (!isDirectory(directoryPath)) { throw new Error("Not a directory. Please either create a directory, or use hashFile()."); } } catch (error) { throw new Error("Directory does not exist. Please either create a directory, or use hashFile()."); } const manifest: PackageManifest = await generatePackageManifestFromDirectory(directoryPath, basePath); return manifest.computePackageHash(); } export function generatePackageManifestFromDirectory(directoryPath: string, basePath: string): Promise<PackageManifest> { return new Promise<PackageManifest>(async (resolve, reject) => { const fileHashesMap = new Map<string, string>(); const files: string[] = await pfs.walk(directoryPath); if (!files || files.length === 0) { 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 const generateManifestPromise: Promise<void> = files.reduce((soFar: Promise<void>, filePath: string) => { return soFar .then(() => { const relativePath: string = PackageManifest.normalizePath(path.relative(basePath, filePath)); if (!PackageManifest.isIgnored(relativePath)) { return hashFile(filePath) .then((hash: string) => { fileHashesMap.set(relativePath, hash); }); } }); }, Promise.resolve(null as void)); generateManifestPromise .then(() => { resolve(new PackageManifest(fileHashesMap)); }, reject); }); } export function hashFile(filePath: string): Promise<string> { const readStream: fs.ReadStream = fs.createReadStream(filePath); return hashStream(readStream); } export function hashStream(readStream: stream.Readable): Promise<string> { return new Promise<string>((resolve, reject) => { const hashStream = crypto.createHash(HASH_ALGORITHM) as any as stream.Transform; readStream .on("error", (error: any): void => { hashStream.end(); reject(error); }) .on("end", (): void => { hashStream.end(); const buffer = hashStream.read() as Buffer; const hash: string = buffer.toString("hex"); resolve(hash); }); readStream.pipe(hashStream); }); }