appcenter-cli
Version:
Command line tool for Visual Studio App Center
160 lines (131 loc) • 5.03 kB
text/typescript
/**
* 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);
});
}