eoas 3.1.3 → 3.2.0-beta2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/publish.js +52 -21
- package/dist/lib/assets.d.ts +21 -2
- package/dist/lib/assets.js +99 -22
- package/dist/lib/crypto.d.ts +8 -0
- package/dist/lib/crypto.js +27 -0
- package/package.json +1 -1
package/dist/commands/publish.js
CHANGED
|
@@ -72,8 +72,9 @@ class Publish extends core_1.Command {
|
|
|
72
72
|
required: false,
|
|
73
73
|
}),
|
|
74
74
|
dumpSourcemap: core_1.Flags.boolean({
|
|
75
|
-
description: 'Emit Hermes source maps alongside the bundle so the
|
|
76
|
-
default:
|
|
75
|
+
description: 'Emit Hermes source maps alongside the bundle (default: true). Without a source map Hermes bakes a random temp path into the bytecode, so two exports of identical code never hash the same and server-side change detection cannot work. The maps also let tools like Sentry or PostHog symbolicate the published artifact; they stay in the output directory and are never uploaded. Disable with --no-dumpSourcemap.',
|
|
76
|
+
default: true,
|
|
77
|
+
allowNo: true,
|
|
77
78
|
}),
|
|
78
79
|
'rollout-percentage': core_1.Flags.integer({
|
|
79
80
|
min: 1,
|
|
@@ -259,7 +260,7 @@ class Publish extends core_1.Command {
|
|
|
259
260
|
});
|
|
260
261
|
log_1.default.withInfo(`expoConfig.json file created in ${outputDir} directory`);
|
|
261
262
|
const uploadFilesSpinner = (0, ora_1.ora)('📤 Uploading files...').start();
|
|
262
|
-
const files = (0, assets_1.computeFilesRequests)(projectDir, outputDir, platform || expoConfig_1.RequestedPlatform.All);
|
|
263
|
+
const files = await (0, assets_1.computeFilesRequests)(projectDir, outputDir, platform || expoConfig_1.RequestedPlatform.All);
|
|
263
264
|
if (!files.length) {
|
|
264
265
|
uploadFilesSpinner.fail('No files to upload');
|
|
265
266
|
process.exit(1);
|
|
@@ -268,30 +269,47 @@ class Publish extends core_1.Command {
|
|
|
268
269
|
// One group id for the whole run: every platform update of this publish
|
|
269
270
|
// shares it, so control plane servers list them as a single publish.
|
|
270
271
|
const publishGroupId = (0, crypto_1.randomUUID)();
|
|
272
|
+
// Collected rather than logged inline: writing to the terminal under a
|
|
273
|
+
// running spinner interleaves with its frames.
|
|
274
|
+
const unchangedPlatforms = [];
|
|
271
275
|
try {
|
|
272
|
-
|
|
276
|
+
const outcomes = await Promise.all(runtimeVersions.map(async ({ runtimeVersion, platform }) => {
|
|
273
277
|
if (!runtimeVersion) {
|
|
274
278
|
throw new Error('Runtime version is not resolved');
|
|
275
279
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
280
|
+
try {
|
|
281
|
+
return {
|
|
282
|
+
...(await (0, assets_1.requestUploadUrls)({
|
|
283
|
+
body: { files: (0, assets_1.buildUploadFiles)(files, platform) },
|
|
284
|
+
requestUploadUrl: `${serverUrl}/${appId}/requestUploadUrl/${branch}`,
|
|
285
|
+
auth: credentials,
|
|
286
|
+
runtimeVersion,
|
|
287
|
+
platform,
|
|
288
|
+
commitHash,
|
|
289
|
+
message: resolvedMessage,
|
|
290
|
+
rolloutPercentage,
|
|
291
|
+
publishGroup: publishGroupId,
|
|
292
|
+
branch,
|
|
293
|
+
})),
|
|
283
294
|
runtimeVersion,
|
|
284
295
|
platform,
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
}
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
if (error instanceof assets_1.NoChangesDetectedError) {
|
|
300
|
+
unchangedPlatforms.push(platform);
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
throw error;
|
|
304
|
+
}
|
|
294
305
|
}));
|
|
306
|
+
uploadUrls = outcomes.filter((entry) => {
|
|
307
|
+
return entry !== null;
|
|
308
|
+
});
|
|
309
|
+
if (!uploadUrls.length) {
|
|
310
|
+
uploadFilesSpinner.warn('⚠️ No changes found in the update, nothing to deploy');
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
295
313
|
// Every path and URL the server handed back is checked here, before a
|
|
296
314
|
// single file is opened. A server that forges a filePath would otherwise
|
|
297
315
|
// make the CLI read arbitrary files off this machine and PUT them wherever
|
|
@@ -358,6 +376,14 @@ class Publish extends core_1.Command {
|
|
|
358
376
|
}
|
|
359
377
|
}));
|
|
360
378
|
uploadFilesSpinner.succeed('✅ Files uploaded successfully');
|
|
379
|
+
for (const { platform: uploadedPlatform, uploadRequests } of uploadUrls) {
|
|
380
|
+
const totalFiles = (0, assets_1.buildUploadFiles)(files, uploadedPlatform).length;
|
|
381
|
+
const deduplicated = totalFiles - uploadRequests.length;
|
|
382
|
+
log_1.default.withInfo(`📊 ${uploadedPlatform}: ${uploadRequests.length}/${totalFiles} files uploaded, ${deduplicated} deduplicated (already on the server)`);
|
|
383
|
+
}
|
|
384
|
+
for (const skipped of unchangedPlatforms) {
|
|
385
|
+
log_1.default.withInfo(`⚠️ There is no change in the update for ${skipped}, ignored...`);
|
|
386
|
+
}
|
|
361
387
|
}
|
|
362
388
|
catch (e) {
|
|
363
389
|
uploadFilesSpinner.fail('❌ Failed to upload static files');
|
|
@@ -423,9 +449,14 @@ class Publish extends core_1.Command {
|
|
|
423
449
|
if (hasSuccess) {
|
|
424
450
|
log_1.default.withInfo(`🌿 Branch: \`${branch}\``);
|
|
425
451
|
log_1.default.withInfo(`⏳ Deployed at: \`${new Date().toUTCString()}\`\n`);
|
|
452
|
+
for (const { platform: publishedPlatform, updateId } of uploadUrls) {
|
|
453
|
+
log_1.default.withInfo(`🆔 ${publishedPlatform} update id: \`${updateId}\``);
|
|
454
|
+
}
|
|
426
455
|
const groupAcknowledged = uploadUrls.every(u => u.publishGroup === publishGroupId);
|
|
427
456
|
if (groupAcknowledged) {
|
|
428
|
-
log_1.default.withInfo(`📦 Publish group: \`${publishGroupId}
|
|
457
|
+
log_1.default.withInfo(`📦 Publish group: \`${publishGroupId}\` (groups the ${uploadUrls
|
|
458
|
+
.map(u => u.platform)
|
|
459
|
+
.join(' + ')} updates of this run)`);
|
|
429
460
|
}
|
|
430
461
|
else if (uploadUrls.length > 1) {
|
|
431
462
|
// Only worth a note when several platforms were published: a single
|
package/dist/lib/assets.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Platform } from '@expo/config';
|
|
1
2
|
import Joi from 'joi';
|
|
2
3
|
import { Credentials } from './auth';
|
|
3
4
|
import { RequestedPlatform } from './expoConfig';
|
|
@@ -6,12 +7,26 @@ export interface AssetToUpload {
|
|
|
6
7
|
path: string;
|
|
7
8
|
name: string;
|
|
8
9
|
ext: string;
|
|
10
|
+
hash: string;
|
|
11
|
+
key: string;
|
|
12
|
+
platform: Platform | null;
|
|
13
|
+
isLaunchAsset: boolean;
|
|
9
14
|
}
|
|
10
|
-
export
|
|
15
|
+
export type FileRole = 'launch' | 'asset' | 'config';
|
|
16
|
+
export interface FileUploadItem {
|
|
17
|
+
path: string;
|
|
18
|
+
hash: string;
|
|
19
|
+
key?: string;
|
|
20
|
+
ext?: string;
|
|
21
|
+
role: FileRole;
|
|
22
|
+
}
|
|
23
|
+
export declare function buildUploadFiles(files: AssetToUpload[], platform: string): FileUploadItem[];
|
|
24
|
+
export declare function computeFilesRequests(projectDir: string, outputDir: string, requestedPlatform: RequestedPlatform): Promise<AssetToUpload[]>;
|
|
11
25
|
export interface RequestUploadUrlItem {
|
|
12
26
|
requestUploadUrl: string;
|
|
13
27
|
fileName: string;
|
|
14
28
|
filePath: string;
|
|
29
|
+
originalFileName: string;
|
|
15
30
|
headers?: Record<string, string>;
|
|
16
31
|
}
|
|
17
32
|
export interface RequestUploadUrlsResponse {
|
|
@@ -41,9 +56,13 @@ export declare function resolveUploadRequests({ uploadRequests, exportDir, manif
|
|
|
41
56
|
manifest: AssetToUpload[];
|
|
42
57
|
}): Promise<ResolvedUploadRequest[]>;
|
|
43
58
|
export declare function activeRolloutConflictMessage(branch: string): string;
|
|
59
|
+
export declare class NoChangesDetectedError extends Error {
|
|
60
|
+
readonly platform: string;
|
|
61
|
+
constructor(platform: string);
|
|
62
|
+
}
|
|
44
63
|
export declare function requestUploadUrls({ body, requestUploadUrl, auth, runtimeVersion, platform, commitHash, message, rolloutPercentage, publishGroup, branch, }: {
|
|
45
64
|
body: {
|
|
46
|
-
|
|
65
|
+
files: FileUploadItem[];
|
|
47
66
|
};
|
|
48
67
|
requestUploadUrl: string;
|
|
49
68
|
auth: Credentials;
|
package/dist/lib/assets.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.requestUploadUrls = exports.activeRolloutConflictMessage = exports.resolveUploadRequests = exports.RequestUploadUrlsResponseJoi = exports.computeFilesRequests = exports.MetadataJoi = void 0;
|
|
3
|
+
exports.requestUploadUrls = exports.NoChangesDetectedError = exports.activeRolloutConflictMessage = exports.resolveUploadRequests = exports.RequestUploadUrlsResponseJoi = exports.computeFilesRequests = exports.buildUploadFiles = exports.MetadataJoi = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const fs_extra_1 = tslib_1.__importDefault(require("fs-extra"));
|
|
6
6
|
const joi_1 = tslib_1.__importDefault(require("joi"));
|
|
7
7
|
const path_1 = tslib_1.__importDefault(require("path"));
|
|
8
8
|
const auth_1 = require("./auth");
|
|
9
|
+
const crypto_1 = require("./crypto");
|
|
9
10
|
const expoConfig_1 = require("./expoConfig");
|
|
10
11
|
const fetch_1 = require("./fetch");
|
|
11
12
|
const log_1 = tslib_1.__importDefault(require("./log"));
|
|
@@ -24,6 +25,26 @@ exports.MetadataJoi = joi_1.default.object({
|
|
|
24
25
|
web: fileMetadataJoi,
|
|
25
26
|
}).required(),
|
|
26
27
|
}).required();
|
|
28
|
+
// buildUploadFiles is one platform's publish: its launch asset, its assets and
|
|
29
|
+
// the config files, each stamped with its role. This is what tells the server
|
|
30
|
+
// which bundle is which — only the CLI reads metadata.json.
|
|
31
|
+
function buildUploadFiles(files, platform) {
|
|
32
|
+
return files
|
|
33
|
+
.filter(file => file.platform === null || file.platform === platform)
|
|
34
|
+
.map(file => {
|
|
35
|
+
if (file.platform === null) {
|
|
36
|
+
return { path: file.path, hash: file.hash, role: 'config' };
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
path: file.path,
|
|
40
|
+
hash: file.hash,
|
|
41
|
+
key: file.key,
|
|
42
|
+
ext: file.ext,
|
|
43
|
+
role: file.isLaunchAsset ? 'launch' : 'asset',
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
exports.buildUploadFiles = buildUploadFiles;
|
|
27
48
|
function loadMetadata(distRoot) {
|
|
28
49
|
// eslint-disable-next-line
|
|
29
50
|
const fileContent = fs_extra_1.default.readFileSync(path_1.default.join(distRoot, 'metadata.json'), 'utf8');
|
|
@@ -54,23 +75,65 @@ function loadMetadata(distRoot) {
|
|
|
54
75
|
log_1.default.debug(`Loaded ${platforms.length} platform(s): ${platforms.join(', ')}`);
|
|
55
76
|
return metadata;
|
|
56
77
|
}
|
|
57
|
-
function
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
78
|
+
async function digestExportFile(exportRoot, relativePath) {
|
|
79
|
+
const absolutePath = path_1.default.resolve(exportRoot, relativePath);
|
|
80
|
+
if (absolutePath !== exportRoot && !absolutePath.startsWith(exportRoot + path_1.default.sep)) {
|
|
81
|
+
throw new Error(`Refusing to hash "${relativePath}": it resolves outside the export directory.`);
|
|
82
|
+
}
|
|
83
|
+
return await (0, crypto_1.digestFile)(absolutePath);
|
|
84
|
+
}
|
|
85
|
+
async function computeFilesRequests(projectDir, outputDir, requestedPlatform) {
|
|
86
|
+
const exportDir = path_1.default.join(projectDir, outputDir);
|
|
87
|
+
let exportRoot;
|
|
88
|
+
try {
|
|
89
|
+
exportRoot = await fs_extra_1.default.realpath(exportDir);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
throw new Error(`Export directory ${exportDir} could not be resolved.`);
|
|
93
|
+
}
|
|
94
|
+
const metadata = loadMetadata(exportRoot);
|
|
95
|
+
const pending = [
|
|
96
|
+
{
|
|
97
|
+
path: 'metadata.json',
|
|
98
|
+
name: 'metadata.json',
|
|
99
|
+
ext: 'json',
|
|
100
|
+
platform: null,
|
|
101
|
+
isLaunchAsset: false,
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
path: 'expoConfig.json',
|
|
105
|
+
name: 'expoConfig.json',
|
|
106
|
+
ext: 'json',
|
|
107
|
+
platform: null,
|
|
108
|
+
isLaunchAsset: false,
|
|
109
|
+
},
|
|
62
110
|
];
|
|
63
111
|
for (const platform of Object.keys(metadata.fileMetadata)) {
|
|
64
112
|
if (requestedPlatform !== expoConfig_1.RequestedPlatform.All && requestedPlatform !== platform) {
|
|
65
113
|
continue;
|
|
66
114
|
}
|
|
67
115
|
const bundle = metadata.fileMetadata[platform].bundle;
|
|
68
|
-
|
|
116
|
+
pending.push({
|
|
117
|
+
path: bundle,
|
|
118
|
+
name: path_1.default.basename(bundle),
|
|
119
|
+
ext: 'hbc',
|
|
120
|
+
platform,
|
|
121
|
+
isLaunchAsset: true,
|
|
122
|
+
});
|
|
69
123
|
for (const asset of metadata.fileMetadata[platform].assets) {
|
|
70
|
-
|
|
124
|
+
pending.push({
|
|
125
|
+
path: asset.path,
|
|
126
|
+
name: path_1.default.basename(asset.path),
|
|
127
|
+
ext: asset.ext,
|
|
128
|
+
platform,
|
|
129
|
+
isLaunchAsset: false,
|
|
130
|
+
});
|
|
71
131
|
}
|
|
72
132
|
}
|
|
73
|
-
return
|
|
133
|
+
return await Promise.all(pending.map(async (entry) => ({
|
|
134
|
+
...entry,
|
|
135
|
+
...(await digestExportFile(exportRoot, entry.path)),
|
|
136
|
+
})));
|
|
74
137
|
}
|
|
75
138
|
exports.computeFilesRequests = computeFilesRequests;
|
|
76
139
|
// The server dictates which local files the CLI opens and where their bytes are
|
|
@@ -91,6 +154,7 @@ const uploadRequestJoi = joi_1.default.object({
|
|
|
91
154
|
.required(),
|
|
92
155
|
fileName: joi_1.default.string().required(),
|
|
93
156
|
filePath: joi_1.default.string().required(),
|
|
157
|
+
originalFileName: joi_1.default.string().required(),
|
|
94
158
|
headers: uploadRequestHeadersJoi,
|
|
95
159
|
// Unknown keys are tolerated so a newer server can add fields without
|
|
96
160
|
// breaking older CLIs; nothing reads them.
|
|
@@ -177,40 +241,41 @@ async function resolveUploadRequests({ uploadRequests, exportDir, manifest, }) {
|
|
|
177
241
|
const resolved = [];
|
|
178
242
|
for (const item of uploadRequests) {
|
|
179
243
|
assertSafeUploadUrl(item.requestUploadUrl);
|
|
180
|
-
|
|
181
|
-
|
|
244
|
+
const exportPath = item.originalFileName;
|
|
245
|
+
assertRelativePathShape(exportPath);
|
|
246
|
+
const manifestEntry = manifestByPath.get(exportPath);
|
|
182
247
|
if (!manifestEntry) {
|
|
183
|
-
throw new Error(`Refusing to upload "${
|
|
248
|
+
throw new Error(`Refusing to upload "${exportPath}": the server asked for a file that is not part of this export.`);
|
|
184
249
|
}
|
|
185
|
-
if (item.fileName !== path_1.default.basename(
|
|
186
|
-
throw new Error(`Refusing to upload "${
|
|
250
|
+
if (item.fileName !== path_1.default.basename(exportPath)) {
|
|
251
|
+
throw new Error(`Refusing to upload "${exportPath}": the server returned the mismatched name "${item.fileName}".`);
|
|
187
252
|
}
|
|
188
|
-
if (seen.has(
|
|
189
|
-
throw new Error(`The server requested "${
|
|
253
|
+
if (seen.has(exportPath)) {
|
|
254
|
+
throw new Error(`The server requested "${exportPath}" more than once.`);
|
|
190
255
|
}
|
|
191
|
-
seen.add(
|
|
192
|
-
const absolutePath = path_1.default.resolve(exportRoot,
|
|
256
|
+
seen.add(exportPath);
|
|
257
|
+
const absolutePath = path_1.default.resolve(exportRoot, exportPath);
|
|
193
258
|
// Unreachable on POSIX: a path with no '..' segment and no leading separator
|
|
194
259
|
// cannot resolve out of the root. Kept for the Windows drive-relative case
|
|
195
260
|
// ("C:file" when the export root sits on another drive) and as a backstop if
|
|
196
261
|
// the checks above are ever relaxed.
|
|
197
262
|
if (absolutePath !== exportRoot && !absolutePath.startsWith(exportRoot + path_1.default.sep)) {
|
|
198
|
-
throw new Error(`Refusing to upload "${
|
|
263
|
+
throw new Error(`Refusing to upload "${exportPath}": it resolves outside the export directory.`);
|
|
199
264
|
}
|
|
200
265
|
let realPath;
|
|
201
266
|
try {
|
|
202
267
|
realPath = await fs_extra_1.default.realpath(absolutePath);
|
|
203
268
|
}
|
|
204
269
|
catch {
|
|
205
|
-
throw new Error(`File ${
|
|
270
|
+
throw new Error(`File ${exportPath} not found in the export directory.`);
|
|
206
271
|
}
|
|
207
272
|
// The root is already canonical, so any difference here means a symlink was
|
|
208
273
|
// traversed, either as the file itself or as one of its parent directories.
|
|
209
274
|
if (realPath !== absolutePath) {
|
|
210
|
-
throw new Error(`Refusing to upload "${
|
|
275
|
+
throw new Error(`Refusing to upload "${exportPath}": it is or goes through a symlink.`);
|
|
211
276
|
}
|
|
212
277
|
if (!(await fs_extra_1.default.lstat(absolutePath)).isFile()) {
|
|
213
|
-
throw new Error(`Refusing to upload "${
|
|
278
|
+
throw new Error(`Refusing to upload "${exportPath}": it is not a regular file.`);
|
|
214
279
|
}
|
|
215
280
|
resolved.push({ item, absolutePath, manifestEntry });
|
|
216
281
|
}
|
|
@@ -221,6 +286,15 @@ function activeRolloutConflictMessage(branch) {
|
|
|
221
286
|
return `A progressive rollout is already active for branch "${branch}" on this runtime version. End or revert it from the dashboard before publishing a new update.`;
|
|
222
287
|
}
|
|
223
288
|
exports.activeRolloutConflictMessage = activeRolloutConflictMessage;
|
|
289
|
+
class NoChangesDetectedError extends Error {
|
|
290
|
+
platform;
|
|
291
|
+
constructor(platform) {
|
|
292
|
+
super(`There is no change in the update for ${platform}`);
|
|
293
|
+
this.platform = platform;
|
|
294
|
+
this.name = 'NoChangesDetectedError';
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
exports.NoChangesDetectedError = NoChangesDetectedError;
|
|
224
298
|
async function requestUploadUrls({ body, requestUploadUrl, auth, runtimeVersion, platform, commitHash, message, rolloutPercentage, publishGroup, branch, }) {
|
|
225
299
|
const uploadUrl = new URL(requestUploadUrl);
|
|
226
300
|
uploadUrl.searchParams.set('runtimeVersion', runtimeVersion);
|
|
@@ -247,6 +321,9 @@ async function requestUploadUrls({ body, requestUploadUrl, auth, runtimeVersion,
|
|
|
247
321
|
if (response.status === 409) {
|
|
248
322
|
throw new Error(activeRolloutConflictMessage(branch));
|
|
249
323
|
}
|
|
324
|
+
if (response.status === 406) {
|
|
325
|
+
throw new NoChangesDetectedError(platform);
|
|
326
|
+
}
|
|
250
327
|
if (!response.ok) {
|
|
251
328
|
const text = await response.text();
|
|
252
329
|
throw new Error(`Failed to request upload URL: ${text}${(0, auth_1.missingEooTokenHint)(response.status)}`);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
/// <reference types="node" />
|
|
3
|
+
export declare function toBase64Url(buffer: Buffer): string;
|
|
4
|
+
export interface FileDigest {
|
|
5
|
+
hash: string;
|
|
6
|
+
key: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function digestFile(filePath: string): Promise<FileDigest>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.digestFile = exports.toBase64Url = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const crypto_1 = require("crypto");
|
|
6
|
+
const fs_1 = tslib_1.__importDefault(require("fs"));
|
|
7
|
+
// Same encoding as internal/crypto.GetBase64URLEncoding: the string
|
|
8
|
+
// shapeManifestAsset puts on ManifestAsset.hash.
|
|
9
|
+
function toBase64Url(buffer) {
|
|
10
|
+
return buffer.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
11
|
+
}
|
|
12
|
+
exports.toBase64Url = toBase64Url;
|
|
13
|
+
async function digestFile(filePath) {
|
|
14
|
+
const sha256 = (0, crypto_1.createHash)('sha256');
|
|
15
|
+
const md5 = (0, crypto_1.createHash)('md5');
|
|
16
|
+
await new Promise((resolve, reject) => {
|
|
17
|
+
fs_1.default.createReadStream(filePath)
|
|
18
|
+
.on('error', reject)
|
|
19
|
+
.on('data', chunk => {
|
|
20
|
+
sha256.update(chunk);
|
|
21
|
+
md5.update(chunk);
|
|
22
|
+
})
|
|
23
|
+
.on('end', resolve);
|
|
24
|
+
});
|
|
25
|
+
return { hash: toBase64Url(sha256.digest()), key: md5.digest('hex') };
|
|
26
|
+
}
|
|
27
|
+
exports.digestFile = digestFile;
|