forked from microsoft/code-push
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathhash-utils.ts
More file actions
252 lines (208 loc) · 8.26 KB
/
Copy pathhash-utils.ts
File metadata and controls
252 lines (208 loc) · 8.26 KB
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
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
/**
* NOTE!!! This utility file is duplicated for use by the CodePush service (for server-driven hashing/
* integrity checks) and CLI (for end-to-end code signing), please keep them in sync.
*
* Also, the same hashing behavior must be preserved in the native SDK's.
*/
import * as crypto from "crypto";
import * as fs from "fs";
import * as path from "path";
import * as q from "q";
import * as stream from "stream";
// 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) {}
import Promise = q.Promise;
const HASH_ALGORITHM = "sha256";
export function generatePackageHashFromDirectory(directoryPath: string, basePath: string): Promise<string> {
if (!fs.lstatSync(directoryPath).isDirectory()) {
throw new Error("Not a directory. Please either create a directory, or use hashFile().");
}
return generatePackageManifestFromDirectory(directoryPath, basePath)
.then((manifest: PackageManifest) => {
return manifest.computePackageHash()
});
}
export function generatePackageManifestFromZip(filePath: string): Promise<PackageManifest> {
var deferred: q.Deferred<PackageManifest> = q.defer<PackageManifest>();
var reject = (error: Error) => {
if (deferred.promise.isPending()) {
deferred.reject(error);
}
}
var resolve = (manifest: PackageManifest) => {
if (deferred.promise.isPending()) {
deferred.resolve(manifest);
}
}
var zipFile: any;
yauzl.open(filePath, { lazyEntries: true }, (error?: any, openedZipFile?: any): void => {
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<string, string>();
var hashFilePromises: q.Promise<void>[] = [];
// Read each entry in the archive sequentially and generate a hash for it.
zipFile.readEntry();
zipFile
.on("error", (error: any): void => {
reject(error);
})
.on("entry", (entry: any): void => {
var fileName: string = PackageManifest.normalizePath(entry.fileName);
if (PackageManifest.isIgnored(fileName)) {
zipFile.readEntry();
return;
}
zipFile.openReadStream(entry, (error?: any, readStream?: stream.Readable): void => {
if (error) {
reject(error);
return;
}
hashFilePromises.push(
hashStream(readStream)
.then((hash: string) => {
fileHashesMap.set(fileName, hash);
zipFile.readEntry();
}, reject)
);
});
})
.on("end", (): void => {
q.all(hashFilePromises).then(
() => resolve(new PackageManifest(fileHashesMap)),
reject
);
});
});
return deferred.promise
.finally(() => zipFile && zipFile.close());
}
export function generatePackageManifestFromDirectory(directoryPath: string, basePath: string): Promise<PackageManifest> {
var deferred: q.Deferred<PackageManifest> = q.defer<PackageManifest>();
var fileHashesMap = new Map<string, string>();
recursiveFs.readdirr(directoryPath, (error?: any, directories?: string[], files?: string[]): void => {
if (error) {
deferred.reject(error);
return;
}
if (!files || files.length === 0) {
deferred.reject("Can't hash the directory because no files were found.");
return;
}
// Hash the files sequentially, because streaming them in parallel is not necessarily faster
var generateManifestPromise: Promise<void> = files.reduce((soFar: Promise<void>, filePath: string) => {
return soFar
.then(() => {
var relativePath: string = PackageManifest.normalizePath(path.relative(basePath, filePath));
if (!PackageManifest.isIgnored(relativePath)) {
return hashFile(filePath)
.then((hash: string) => {
fileHashesMap.set(relativePath, hash);
});
}
});
}, q(<void>null));
generateManifestPromise
.then(() => {
deferred.resolve(new PackageManifest(fileHashesMap));
}, deferred.reject)
.done();
});
return deferred.promise;
}
export function hashFile(filePath: string): Promise<string> {
var readStream: fs.ReadStream = fs.createReadStream(filePath);
return hashStream(readStream);
}
export function hashStream(readStream: stream.Readable): Promise<string> {
var hashStream = <stream.Transform><any>crypto.createHash(HASH_ALGORITHM);
var deferred: q.Deferred<string> = q.defer<string>();
readStream
.on("error", (error: any): void => {
if (deferred.promise.isPending()) {
hashStream.end();
deferred.reject(error);
}
})
.on("end", (): void => {
if (deferred.promise.isPending()) {
hashStream.end();
var buffer = <Buffer>hashStream.read();
var hash: string = buffer.toString("hex");
deferred.resolve(hash);
}
});
readStream.pipe(hashStream);
return deferred.promise;
}
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(): Promise<string> {
var 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 q(
crypto.createHash(HASH_ALGORITHM)
.update(JSON.stringify(entries))
.digest("hex")
);
}
public serialize(): string {
var obj: any = {};
this._map.forEach(function(value, key) {
obj[key] = value;
});
return JSON.stringify(obj);
}
public static deserialize(serializedContents: string): PackageManifest {
try {
var obj: any = JSON.parse(serializedContents);
var map = new Map<string, string>();
for (var key of Object.keys(obj)) {
map.set(key, obj[key]);
}
return new PackageManifest(map);
} catch (e) {
}
}
public static normalizePath(filePath: string): string {
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);
}
}
function startsWith(str: string, prefix: string): boolean {
return str && str.substring(0, prefix.length) === prefix;
}
function endsWith(str: string, suffix: string): boolean {
return str && str.indexOf(suffix, str.length - suffix.length) !== -1;
}