eas-cli 16.14.1 → 16.15.0
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/README.md +80 -80
- package/build/commands/deploy/index.js +47 -49
- package/build/worker/assets.d.ts +19 -13
- package/build/worker/assets.js +49 -20
- package/build/worker/upload.d.ts +21 -9
- package/build/worker/upload.js +102 -80
- package/build/worker/utils/multipart.d.ts +10 -0
- package/build/worker/utils/multipart.js +63 -0
- package/oclif.manifest.json +1 -1
- package/package.json +2 -2
|
@@ -14,11 +14,11 @@ const log_1 = tslib_1.__importStar(require("../../log"));
|
|
|
14
14
|
const ora_1 = require("../../ora");
|
|
15
15
|
const projectUtils_1 = require("../../project/projectUtils");
|
|
16
16
|
const json_1 = require("../../utils/json");
|
|
17
|
-
const progress_1 = require("../../utils/progress");
|
|
18
17
|
const WorkerAssets = tslib_1.__importStar(require("../../worker/assets"));
|
|
19
18
|
const deployment_1 = require("../../worker/deployment");
|
|
20
19
|
const upload_1 = require("../../worker/upload");
|
|
21
20
|
const logs_1 = require("../../worker/utils/logs");
|
|
21
|
+
const MAX_UPLOAD_SIZE = 5e8; // 500MB
|
|
22
22
|
const isDirectory = (directoryPath) => node_fs_1.default.promises
|
|
23
23
|
.stat(directoryPath)
|
|
24
24
|
.then(stat => stat.isDirectory())
|
|
@@ -96,14 +96,11 @@ class WorkerDeploy extends EasCommand_1.default {
|
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
async function uploadTarballAsync(tarPath, uploadUrl) {
|
|
99
|
+
const payload = { filePath: tarPath };
|
|
99
100
|
const { response } = await (0, upload_1.uploadAsync)({
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
headers: {
|
|
104
|
-
accept: 'application/json',
|
|
105
|
-
},
|
|
106
|
-
});
|
|
101
|
+
baseURL: uploadUrl,
|
|
102
|
+
method: 'POST',
|
|
103
|
+
}, payload);
|
|
107
104
|
if (response.status === 413) {
|
|
108
105
|
throw new Error('Upload failed! (Payload too large)\n' +
|
|
109
106
|
`The files in "${path.relative(projectDir, projectDist.path)}" (at: ${projectDir}) exceed the maximum file size (10MB gzip).`);
|
|
@@ -116,7 +113,7 @@ class WorkerDeploy extends EasCommand_1.default {
|
|
|
116
113
|
if (!json.success || !json.result || typeof json.result !== 'object') {
|
|
117
114
|
throw new Error(json.message ? `Upload failed: ${json.message}` : 'Upload failed!');
|
|
118
115
|
}
|
|
119
|
-
const { id, fullName, token } = json.result;
|
|
116
|
+
const { id, fullName, token, upload } = json.result;
|
|
120
117
|
if (typeof token !== 'string') {
|
|
121
118
|
throw new Error('Upload failed: API failed to return a deployment token');
|
|
122
119
|
}
|
|
@@ -126,56 +123,57 @@ class WorkerDeploy extends EasCommand_1.default {
|
|
|
126
123
|
else if (typeof fullName !== 'string') {
|
|
127
124
|
throw new Error('Upload failed: API failed to return a script name');
|
|
128
125
|
}
|
|
126
|
+
else if (!Array.isArray(upload) && upload !== undefined) {
|
|
127
|
+
throw new Error('Upload failed: API returned invalid asset upload instructions');
|
|
128
|
+
}
|
|
129
129
|
const baseURL = new URL('/', uploadUrl).toString();
|
|
130
|
-
return { id, fullName, baseURL, token };
|
|
130
|
+
return { id, fullName, baseURL, token, upload };
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
|
-
async function uploadAssetsAsync(
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
133
|
+
async function uploadAssetsAsync(assetFiles, deployParams) {
|
|
134
|
+
const baseURL = new URL('/asset/', deployParams.baseURL);
|
|
135
|
+
const uploadInit = { baseURL, method: 'POST' };
|
|
136
|
+
uploadInit.baseURL.searchParams.set('token', deployParams.token);
|
|
137
|
+
const uploadPayloads = [];
|
|
138
|
+
if (deployParams.upload) {
|
|
139
|
+
const assetsBySHA512 = assetFiles.reduce((map, asset) => {
|
|
140
|
+
map.set(asset.sha512, asset);
|
|
141
|
+
return map;
|
|
142
|
+
}, new Map());
|
|
143
|
+
const payloads = deployParams.upload
|
|
144
|
+
.map(instruction => instruction.sha512.map(sha512 => {
|
|
145
|
+
const asset = assetsBySHA512.get(sha512);
|
|
146
|
+
if (!asset) {
|
|
147
|
+
// NOTE(@kitten): This should never happen
|
|
148
|
+
throw new Error(`Uploading assets failed: API instructed us to upload an asset that does not exist`);
|
|
149
|
+
}
|
|
150
|
+
return asset;
|
|
151
|
+
}))
|
|
152
|
+
.filter(assets => assets && assets.length > 0)
|
|
153
|
+
.map(assets => (assets.length > 1 ? { multipart: assets } : { asset: assets[0] }));
|
|
154
|
+
uploadPayloads.push(...payloads);
|
|
138
155
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
uploadParams.push({ url: uploadURL.toString(), filePath: asset.path });
|
|
156
|
+
else {
|
|
157
|
+
// NOTE(@kitten): Legacy format which uploads assets one-by-one
|
|
158
|
+
uploadPayloads.push(...assetFiles.map(asset => ({ asset })));
|
|
143
159
|
}
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
pending: 0,
|
|
147
|
-
percent: 0,
|
|
148
|
-
transferred: 0,
|
|
149
|
-
};
|
|
150
|
-
const updateProgress = (0, progress_1.createProgressTracker)({
|
|
151
|
-
total: progress.total,
|
|
152
|
-
message(ratio) {
|
|
153
|
-
const percent = `${Math.floor(ratio * 100)}`;
|
|
154
|
-
const details = chalk_1.default.dim(`(${progress.pending} Pending, ${progress.transferred} Completed, ${progress.total} Total)`);
|
|
155
|
-
return `Uploading assets: ${percent.padStart(3)}% ${details}`;
|
|
156
|
-
},
|
|
157
|
-
completedMessage: 'Uploaded assets',
|
|
158
|
-
});
|
|
160
|
+
const progressTotal = uploadPayloads.reduce((acc, payload) => acc + ('multipart' in payload ? payload.multipart.length : 1), 0);
|
|
161
|
+
const progressTracker = (0, upload_1.createProgressBar)(`Uploading ${progressTotal} assets`);
|
|
159
162
|
try {
|
|
160
|
-
for await (const signal of (0, upload_1.batchUploadAsync)(
|
|
161
|
-
|
|
162
|
-
progress.pending--;
|
|
163
|
-
progress.percent = ++progress.transferred / progress.total;
|
|
164
|
-
}
|
|
165
|
-
else {
|
|
166
|
-
progress.pending++;
|
|
167
|
-
}
|
|
168
|
-
updateProgress({ progress });
|
|
163
|
+
for await (const signal of (0, upload_1.batchUploadAsync)(uploadInit, uploadPayloads, progressTracker.update)) {
|
|
164
|
+
progressTracker.update(signal.progress);
|
|
169
165
|
}
|
|
170
166
|
}
|
|
171
167
|
catch (error) {
|
|
172
|
-
|
|
168
|
+
progressTracker.stop();
|
|
173
169
|
throw error;
|
|
174
170
|
}
|
|
175
|
-
|
|
171
|
+
finally {
|
|
172
|
+
progressTracker.stop();
|
|
173
|
+
}
|
|
176
174
|
}
|
|
177
|
-
let assetMap;
|
|
178
175
|
let tarPath;
|
|
176
|
+
let assetFiles;
|
|
179
177
|
let deployResult;
|
|
180
178
|
let progress = (0, ora_1.ora)('Preparing project').start();
|
|
181
179
|
try {
|
|
@@ -189,9 +187,9 @@ class WorkerDeploy extends EasCommand_1.default {
|
|
|
189
187
|
'In case of conflict, the EAS environment variable values will be used: ' +
|
|
190
188
|
manifestResult.conflictingVariableNames.join(' '));
|
|
191
189
|
}
|
|
192
|
-
|
|
190
|
+
assetFiles = await WorkerAssets.collectAssetsAsync(projectDist.type === 'server' ? projectDist.clientPath : projectDist.path, { maxFileSize: MAX_UPLOAD_SIZE });
|
|
193
191
|
tarPath = await WorkerAssets.packFilesIterableAsync(emitWorkerTarballAsync({
|
|
194
|
-
assetMap,
|
|
192
|
+
assetMap: WorkerAssets.assetsToAssetsMap(assetFiles),
|
|
195
193
|
manifest: manifestResult.manifest,
|
|
196
194
|
}));
|
|
197
195
|
if (flags.dryRun) {
|
|
@@ -232,7 +230,7 @@ class WorkerDeploy extends EasCommand_1.default {
|
|
|
232
230
|
}
|
|
233
231
|
throw error;
|
|
234
232
|
}
|
|
235
|
-
await uploadAssetsAsync(
|
|
233
|
+
await uploadAssetsAsync(assetFiles, deployResult);
|
|
236
234
|
await finalizeDeployAsync(deployResult);
|
|
237
235
|
let deploymentAlias = null;
|
|
238
236
|
if (flags.aliasName) {
|
package/build/worker/assets.d.ts
CHANGED
|
@@ -6,11 +6,24 @@ import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGr
|
|
|
6
6
|
import { EnvironmentVariableEnvironment } from '../graphql/generated';
|
|
7
7
|
interface AssetMapOptions {
|
|
8
8
|
hashOptions?: HashOptions;
|
|
9
|
+
maxFileSize: number;
|
|
9
10
|
}
|
|
11
|
+
export interface AssetFileEntry {
|
|
12
|
+
normalizedPath: string;
|
|
13
|
+
path: string;
|
|
14
|
+
size: number;
|
|
15
|
+
sha512: string;
|
|
16
|
+
type: string | null;
|
|
17
|
+
}
|
|
18
|
+
/** Collects assets from a given target path */
|
|
19
|
+
export declare function collectAssetsAsync(assetPath: string | undefined, options: AssetMapOptions): Promise<AssetFileEntry[]>;
|
|
10
20
|
/** Mapping of normalized file paths to a SHA512 hash */
|
|
11
|
-
export type AssetMap = Record<string, string
|
|
12
|
-
|
|
13
|
-
|
|
21
|
+
export type AssetMap = Record<string, string | {
|
|
22
|
+
sha512: string;
|
|
23
|
+
size: number;
|
|
24
|
+
}>;
|
|
25
|
+
/** Converts array of asset entries into AssetMap (as sent to deployment-api) */
|
|
26
|
+
export declare function assetsToAssetsMap(assets: AssetFileEntry[]): AssetMap;
|
|
14
27
|
export interface Manifest {
|
|
15
28
|
env: Record<string, string | undefined>;
|
|
16
29
|
}
|
|
@@ -31,16 +44,9 @@ interface WorkerFileEntry {
|
|
|
31
44
|
data: Buffer | string;
|
|
32
45
|
}
|
|
33
46
|
/** Reads worker files while normalizing sourcemaps and providing normalized paths */
|
|
34
|
-
declare function listWorkerFilesAsync(workerPath: string): AsyncGenerator<WorkerFileEntry>;
|
|
35
|
-
interface AssetFileEntry {
|
|
36
|
-
normalizedPath: string;
|
|
37
|
-
sha512: string;
|
|
38
|
-
path: string;
|
|
39
|
-
}
|
|
40
|
-
/** Reads files of an asset maps and enumerates normalized paths and data */
|
|
41
|
-
declare function listAssetMapFilesAsync(assetPath: string, assetMap: AssetMap): AsyncGenerator<AssetFileEntry>;
|
|
47
|
+
export declare function listWorkerFilesAsync(workerPath: string): AsyncGenerator<WorkerFileEntry>;
|
|
42
48
|
/** Entry of a normalized (gzip-safe) path and file data */
|
|
43
49
|
export type FileEntry = readonly [normalizedPath: string, data: Buffer | string];
|
|
44
50
|
/** Packs file entries into a tar.gz file (path to tgz returned) */
|
|
45
|
-
declare function packFilesIterableAsync(iterable: Iterable<FileEntry> | AsyncIterable<FileEntry>, options?: GzipOptions): Promise<string>;
|
|
46
|
-
export {
|
|
51
|
+
export declare function packFilesIterableAsync(iterable: Iterable<FileEntry> | AsyncIterable<FileEntry>, options?: GzipOptions): Promise<string>;
|
|
52
|
+
export {};
|
package/build/worker/assets.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.packFilesIterableAsync = exports.
|
|
3
|
+
exports.packFilesIterableAsync = exports.listWorkerFilesAsync = exports.createManifestAsync = exports.assetsToAssetsMap = exports.collectAssetsAsync = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const env_1 = require("@expo/env");
|
|
6
|
+
const mime_1 = tslib_1.__importDefault(require("mime"));
|
|
6
7
|
const minizlib_1 = require("minizlib");
|
|
7
8
|
const node_crypto_1 = require("node:crypto");
|
|
8
9
|
const node_fs_1 = tslib_1.__importStar(require("node:fs"));
|
|
@@ -50,9 +51,12 @@ function listFilesRecursively(basePath) {
|
|
|
50
51
|
continue;
|
|
51
52
|
}
|
|
52
53
|
else if (dirent.isFile()) {
|
|
54
|
+
const absolutePath = node_path_1.default.resolve(target, dirent.name);
|
|
55
|
+
const stats = await node_fs_1.default.promises.stat(absolutePath);
|
|
53
56
|
yield {
|
|
54
57
|
normalizedPath,
|
|
55
|
-
path:
|
|
58
|
+
path: absolutePath,
|
|
59
|
+
size: stats.size,
|
|
56
60
|
};
|
|
57
61
|
}
|
|
58
62
|
else if (dirent.isDirectory()) {
|
|
@@ -62,17 +66,54 @@ function listFilesRecursively(basePath) {
|
|
|
62
66
|
}
|
|
63
67
|
return recurseAsync();
|
|
64
68
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
69
|
+
async function determineMimeTypeAsync(filePath) {
|
|
70
|
+
let contentType = mime_1.default.getType(node_path_1.default.basename(filePath));
|
|
71
|
+
if (!contentType) {
|
|
72
|
+
const fileContent = await node_fs_1.default.promises.readFile(filePath, 'utf-8');
|
|
73
|
+
try {
|
|
74
|
+
// check if file is valid JSON without an extension, e.g. for the apple app site association file
|
|
75
|
+
const parsedData = JSON.parse(fileContent);
|
|
76
|
+
if (parsedData) {
|
|
77
|
+
contentType = 'application/json';
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch { }
|
|
81
|
+
}
|
|
82
|
+
return contentType;
|
|
83
|
+
}
|
|
84
|
+
/** Collects assets from a given target path */
|
|
85
|
+
async function collectAssetsAsync(assetPath, options) {
|
|
86
|
+
const assets = [];
|
|
68
87
|
if (assetPath) {
|
|
69
88
|
for await (const file of listFilesRecursively(assetPath)) {
|
|
70
|
-
|
|
89
|
+
if (file.size > options.maxFileSize) {
|
|
90
|
+
throw new Error(`Upload of "${file.normalizedPath}" aborted: File size is greater than the upload limit (>500MB)`);
|
|
91
|
+
}
|
|
92
|
+
const sha512$ = computeSha512HashAsync(file.path, options?.hashOptions);
|
|
93
|
+
const contentType$ = determineMimeTypeAsync(file.path);
|
|
94
|
+
assets.push({
|
|
95
|
+
normalizedPath: file.normalizedPath,
|
|
96
|
+
path: file.path,
|
|
97
|
+
size: file.size,
|
|
98
|
+
sha512: await sha512$,
|
|
99
|
+
type: await contentType$,
|
|
100
|
+
});
|
|
71
101
|
}
|
|
72
102
|
}
|
|
73
|
-
return
|
|
103
|
+
return assets;
|
|
74
104
|
}
|
|
75
|
-
exports.
|
|
105
|
+
exports.collectAssetsAsync = collectAssetsAsync;
|
|
106
|
+
/** Converts array of asset entries into AssetMap (as sent to deployment-api) */
|
|
107
|
+
function assetsToAssetsMap(assets) {
|
|
108
|
+
return assets.reduce((map, entry) => {
|
|
109
|
+
map[entry.normalizedPath] = {
|
|
110
|
+
sha512: entry.sha512,
|
|
111
|
+
size: entry.size,
|
|
112
|
+
};
|
|
113
|
+
return map;
|
|
114
|
+
}, Object.create(null));
|
|
115
|
+
}
|
|
116
|
+
exports.assetsToAssetsMap = assetsToAssetsMap;
|
|
76
117
|
/** Creates a manifest configuration sent up for deployment */
|
|
77
118
|
async function createManifestAsync(params, graphqlClient) {
|
|
78
119
|
// Resolve .env file variables
|
|
@@ -110,18 +151,6 @@ async function* listWorkerFilesAsync(workerPath) {
|
|
|
110
151
|
}
|
|
111
152
|
}
|
|
112
153
|
exports.listWorkerFilesAsync = listWorkerFilesAsync;
|
|
113
|
-
/** Reads files of an asset maps and enumerates normalized paths and data */
|
|
114
|
-
async function* listAssetMapFilesAsync(assetPath, assetMap) {
|
|
115
|
-
for (const normalizedPath in assetMap) {
|
|
116
|
-
const filePath = node_path_1.default.resolve(assetPath, normalizedPath.split('/').join(node_path_1.default.sep));
|
|
117
|
-
yield {
|
|
118
|
-
normalizedPath,
|
|
119
|
-
path: filePath,
|
|
120
|
-
sha512: assetMap[normalizedPath],
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
exports.listAssetMapFilesAsync = listAssetMapFilesAsync;
|
|
125
154
|
/** Packs file entries into a tar.gz file (path to tgz returned) */
|
|
126
155
|
async function packFilesIterableAsync(iterable, options) {
|
|
127
156
|
const writePath = `${await createTempWritePathAsync()}.tar.gz`;
|
package/build/worker/upload.d.ts
CHANGED
|
@@ -1,23 +1,35 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
/// <reference types="node" />
|
|
3
3
|
import { HeadersInit, RequestInit, Response } from 'node-fetch';
|
|
4
|
-
|
|
4
|
+
import { AssetFileEntry } from './assets';
|
|
5
|
+
export type UploadPayload = {
|
|
5
6
|
filePath: string;
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
} | {
|
|
8
|
+
asset: AssetFileEntry;
|
|
9
|
+
} | {
|
|
10
|
+
multipart: AssetFileEntry[];
|
|
11
|
+
};
|
|
12
|
+
export interface UploadRequestInit {
|
|
13
|
+
baseURL: string | URL;
|
|
8
14
|
method?: string;
|
|
9
15
|
headers?: HeadersInit;
|
|
10
|
-
body?: undefined;
|
|
11
16
|
signal?: AbortSignal;
|
|
12
17
|
}
|
|
13
18
|
export interface UploadResult {
|
|
14
|
-
|
|
19
|
+
payload: UploadPayload;
|
|
15
20
|
response: Response;
|
|
16
21
|
}
|
|
17
|
-
|
|
22
|
+
type OnProgressUpdateCallback = (progress: number) => void;
|
|
23
|
+
export declare function uploadAsync(init: UploadRequestInit, payload: UploadPayload, onProgressUpdate?: OnProgressUpdateCallback): Promise<UploadResult>;
|
|
18
24
|
export declare function callUploadApiAsync(url: string | URL, init?: RequestInit): Promise<unknown>;
|
|
19
25
|
export interface UploadPending {
|
|
20
|
-
|
|
26
|
+
payload: UploadPayload;
|
|
27
|
+
progress: number;
|
|
21
28
|
}
|
|
22
|
-
export
|
|
23
|
-
|
|
29
|
+
export declare function batchUploadAsync(init: UploadRequestInit, payloads: UploadPayload[], onProgressUpdate?: OnProgressUpdateCallback): AsyncGenerator<UploadPending>;
|
|
30
|
+
interface UploadProgressBar {
|
|
31
|
+
update(progress: number): void;
|
|
32
|
+
stop(): void;
|
|
33
|
+
}
|
|
34
|
+
export declare function createProgressBar(label?: string): UploadProgressBar;
|
|
35
|
+
export {};
|
package/build/worker/upload.js
CHANGED
|
@@ -1,53 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.batchUploadAsync = exports.callUploadApiAsync = exports.uploadAsync = void 0;
|
|
3
|
+
exports.createProgressBar = exports.batchUploadAsync = exports.callUploadApiAsync = exports.uploadAsync = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
|
+
const cli_progress_1 = tslib_1.__importDefault(require("cli-progress"));
|
|
5
6
|
const https = tslib_1.__importStar(require("https"));
|
|
6
7
|
const https_proxy_agent_1 = tslib_1.__importDefault(require("https-proxy-agent"));
|
|
7
|
-
const mime_1 = tslib_1.__importDefault(require("mime"));
|
|
8
|
-
const minizlib_1 = require("minizlib");
|
|
9
8
|
const node_fetch_1 = tslib_1.__importStar(require("node-fetch"));
|
|
10
|
-
const node_fs_1 = tslib_1.
|
|
11
|
-
const
|
|
12
|
-
const
|
|
9
|
+
const node_fs_1 = tslib_1.__importDefault(require("node:fs"));
|
|
10
|
+
const node_os_1 = tslib_1.__importDefault(require("node:os"));
|
|
11
|
+
const node_stream_1 = require("node:stream");
|
|
13
12
|
const promise_retry_1 = tslib_1.__importDefault(require("promise-retry"));
|
|
13
|
+
const multipart_1 = require("./utils/multipart");
|
|
14
|
+
const MAX_CONCURRENCY = Math.min(10, Math.max(node_os_1.default.availableParallelism() * 2, 20));
|
|
14
15
|
const MAX_RETRIES = 4;
|
|
15
|
-
const MAX_CONCURRENCY = 10;
|
|
16
|
-
const MIN_RETRY_TIMEOUT = 100;
|
|
17
|
-
const MAX_UPLOAD_SIZE = 5e8; // 5MB
|
|
18
|
-
const MIN_COMPRESSION_SIZE = 5e4; // 50kB
|
|
19
|
-
const isCompressible = (contentType, size) => {
|
|
20
|
-
if (size < MIN_COMPRESSION_SIZE) {
|
|
21
|
-
// Don't compress small files
|
|
22
|
-
return false;
|
|
23
|
-
}
|
|
24
|
-
else if (contentType && /^(?:audio|video|image)\//i.test(contentType)) {
|
|
25
|
-
// Never compress images, audio, or videos as they're presumably precompressed
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
28
|
-
else if (contentType && /^application\//i.test(contentType)) {
|
|
29
|
-
// Only compress `application/` files if they're marked as XML/JSON/JS
|
|
30
|
-
return /(?:xml|json5?|javascript)$/i.test(contentType);
|
|
31
|
-
}
|
|
32
|
-
else {
|
|
33
|
-
return true;
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
const getContentTypeAsync = async (filePath) => {
|
|
37
|
-
let contentType = mime_1.default.getType(node_path_1.default.basename(filePath));
|
|
38
|
-
if (!contentType) {
|
|
39
|
-
const fileContent = await (0, promises_1.readFile)(filePath, 'utf-8');
|
|
40
|
-
try {
|
|
41
|
-
// check if file is valid JSON without an extension, e.g. for the apple app site association file
|
|
42
|
-
const parsedData = JSON.parse(fileContent);
|
|
43
|
-
if (parsedData) {
|
|
44
|
-
contentType = 'application/json';
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
catch { }
|
|
48
|
-
}
|
|
49
|
-
return contentType;
|
|
50
|
-
};
|
|
51
16
|
let sharedAgent;
|
|
52
17
|
const getAgent = () => {
|
|
53
18
|
if (sharedAgent) {
|
|
@@ -66,36 +31,56 @@ const getAgent = () => {
|
|
|
66
31
|
}));
|
|
67
32
|
}
|
|
68
33
|
};
|
|
69
|
-
async function uploadAsync(
|
|
70
|
-
const { filePath, signal, compress, method = 'POST', url, headers: headersInit, ...requestInit } = params;
|
|
71
|
-
const stat = await node_fs_1.default.promises.stat(filePath);
|
|
72
|
-
if (stat.size > MAX_UPLOAD_SIZE) {
|
|
73
|
-
throw new Error(`Upload of "${filePath}" aborted: File size is greater than the upload limit (>500MB)`);
|
|
74
|
-
}
|
|
75
|
-
const contentType = await getContentTypeAsync(filePath);
|
|
34
|
+
async function uploadAsync(init, payload, onProgressUpdate) {
|
|
76
35
|
return await (0, promise_retry_1.default)(async (retry) => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
36
|
+
if (onProgressUpdate) {
|
|
37
|
+
onProgressUpdate(0);
|
|
38
|
+
}
|
|
39
|
+
const headers = new node_fetch_1.Headers(init.headers);
|
|
40
|
+
const url = new URL(`${init.baseURL}`);
|
|
41
|
+
let errorPrefix;
|
|
42
|
+
let body;
|
|
43
|
+
let method = init.method || 'POST';
|
|
44
|
+
if ('asset' in payload) {
|
|
45
|
+
const { asset } = payload;
|
|
46
|
+
errorPrefix = `Upload of "${asset.normalizedPath}" failed`;
|
|
47
|
+
if (asset.type) {
|
|
48
|
+
headers.set('content-type', asset.type);
|
|
49
|
+
}
|
|
50
|
+
if (asset.size) {
|
|
51
|
+
headers.set('content-length', `${asset.size}`);
|
|
52
|
+
}
|
|
53
|
+
method = 'POST';
|
|
54
|
+
url.pathname = `/asset/${asset.sha512}`;
|
|
55
|
+
body = node_fs_1.default.createReadStream(asset.path);
|
|
56
|
+
}
|
|
57
|
+
else if ('filePath' in payload) {
|
|
58
|
+
const { filePath } = payload;
|
|
59
|
+
errorPrefix = 'Worker deployment failed';
|
|
60
|
+
body = node_fs_1.default.createReadStream(filePath);
|
|
61
|
+
}
|
|
62
|
+
else if ('multipart' in payload) {
|
|
63
|
+
const { multipart } = payload;
|
|
64
|
+
errorPrefix = `Upload of ${multipart.length} assets failed`;
|
|
65
|
+
headers.set('content-type', multipart_1.multipartContentType);
|
|
66
|
+
method = 'PATCH';
|
|
67
|
+
url.pathname = '/asset/batch';
|
|
68
|
+
const iterator = (0, multipart_1.createMultipartBodyFromFilesAsync)(multipart.map(asset => ({
|
|
69
|
+
name: asset.sha512,
|
|
70
|
+
filePath: asset.path,
|
|
71
|
+
contentType: asset.type,
|
|
72
|
+
contentLength: asset.size,
|
|
73
|
+
})), onProgressUpdate);
|
|
74
|
+
body = node_stream_1.Readable.from(iterator);
|
|
88
75
|
}
|
|
89
76
|
let response;
|
|
90
77
|
try {
|
|
91
|
-
response = await (0, node_fetch_1.default)(
|
|
92
|
-
...requestInit,
|
|
78
|
+
response = await (0, node_fetch_1.default)(url, {
|
|
93
79
|
method,
|
|
94
|
-
body
|
|
80
|
+
body,
|
|
95
81
|
headers,
|
|
96
82
|
agent: getAgent(),
|
|
97
|
-
|
|
98
|
-
signal,
|
|
83
|
+
signal: init.signal,
|
|
99
84
|
});
|
|
100
85
|
}
|
|
101
86
|
catch (error) {
|
|
@@ -113,11 +98,11 @@ async function uploadAsync(params) {
|
|
|
113
98
|
if (rayId) {
|
|
114
99
|
message += `\nReport this error quoting Request ID ${rayId}`;
|
|
115
100
|
}
|
|
116
|
-
return
|
|
101
|
+
return `${errorPrefix}: ${message}`;
|
|
117
102
|
}
|
|
118
103
|
else {
|
|
119
104
|
const json = await response.json().catch(() => null);
|
|
120
|
-
return json?.error ??
|
|
105
|
+
return json?.error ?? `${errorPrefix}: ${response.statusText}`;
|
|
121
106
|
}
|
|
122
107
|
};
|
|
123
108
|
if (response.status === 408 ||
|
|
@@ -127,21 +112,23 @@ async function uploadAsync(params) {
|
|
|
127
112
|
return retry(new Error(await getErrorMessageAsync()));
|
|
128
113
|
}
|
|
129
114
|
else if (response.status === 413) {
|
|
130
|
-
const message =
|
|
115
|
+
const message = `${errorPrefix}: File size exceeded the upload limit`;
|
|
131
116
|
throw new Error(message);
|
|
132
117
|
}
|
|
133
118
|
else if (!response.ok) {
|
|
134
119
|
throw new Error(await getErrorMessageAsync());
|
|
135
120
|
}
|
|
121
|
+
else if (onProgressUpdate) {
|
|
122
|
+
onProgressUpdate(1);
|
|
123
|
+
}
|
|
136
124
|
return {
|
|
137
|
-
|
|
125
|
+
payload,
|
|
138
126
|
response,
|
|
139
127
|
};
|
|
140
128
|
}, {
|
|
141
129
|
retries: MAX_RETRIES,
|
|
142
|
-
minTimeout:
|
|
143
|
-
randomize:
|
|
144
|
-
factor: 2,
|
|
130
|
+
minTimeout: 50,
|
|
131
|
+
randomize: false,
|
|
145
132
|
});
|
|
146
133
|
}
|
|
147
134
|
exports.uploadAsync = uploadAsync;
|
|
@@ -169,19 +156,41 @@ async function callUploadApiAsync(url, init) {
|
|
|
169
156
|
});
|
|
170
157
|
}
|
|
171
158
|
exports.callUploadApiAsync = callUploadApiAsync;
|
|
172
|
-
async function* batchUploadAsync(
|
|
159
|
+
async function* batchUploadAsync(init, payloads, onProgressUpdate) {
|
|
160
|
+
const progressTracker = new Array(payloads.length).fill(0);
|
|
173
161
|
const controller = new AbortController();
|
|
174
162
|
const queue = new Set();
|
|
163
|
+
const initWithSignal = { ...init, signal: controller.signal };
|
|
164
|
+
const getProgressValue = () => {
|
|
165
|
+
const progress = progressTracker.reduce((acc, value) => acc + value, 0);
|
|
166
|
+
return progress / payloads.length;
|
|
167
|
+
};
|
|
168
|
+
const sendProgressUpdate = onProgressUpdate &&
|
|
169
|
+
(() => {
|
|
170
|
+
onProgressUpdate(getProgressValue());
|
|
171
|
+
});
|
|
175
172
|
try {
|
|
176
173
|
let index = 0;
|
|
177
|
-
while (index <
|
|
178
|
-
while (queue.size < MAX_CONCURRENCY && index <
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
174
|
+
while (index < payloads.length || queue.size > 0) {
|
|
175
|
+
while (queue.size < MAX_CONCURRENCY && index < payloads.length) {
|
|
176
|
+
const currentIndex = index++;
|
|
177
|
+
const payload = payloads[currentIndex];
|
|
178
|
+
const onChildProgressUpdate = sendProgressUpdate &&
|
|
179
|
+
((progress) => {
|
|
180
|
+
progressTracker[currentIndex] = progress;
|
|
181
|
+
sendProgressUpdate();
|
|
182
|
+
});
|
|
183
|
+
const uploadPromise = uploadAsync(initWithSignal, payload, onChildProgressUpdate).finally(() => {
|
|
184
|
+
queue.delete(uploadPromise);
|
|
185
|
+
progressTracker[currentIndex] = 1;
|
|
186
|
+
});
|
|
187
|
+
queue.add(uploadPromise);
|
|
188
|
+
yield { payload, progress: getProgressValue() };
|
|
183
189
|
}
|
|
184
|
-
yield
|
|
190
|
+
yield {
|
|
191
|
+
...(await Promise.race(queue)),
|
|
192
|
+
progress: getProgressValue(),
|
|
193
|
+
};
|
|
185
194
|
}
|
|
186
195
|
if (queue.size > 0) {
|
|
187
196
|
controller.abort();
|
|
@@ -194,3 +203,16 @@ async function* batchUploadAsync(uploads) {
|
|
|
194
203
|
}
|
|
195
204
|
}
|
|
196
205
|
exports.batchUploadAsync = batchUploadAsync;
|
|
206
|
+
function createProgressBar(label = 'Uploading assets') {
|
|
207
|
+
const queueProgressBar = new cli_progress_1.default.SingleBar({ format: `|{bar}| {percentage}% ${label}` }, cli_progress_1.default.Presets.rect);
|
|
208
|
+
queueProgressBar.start(1, 0);
|
|
209
|
+
return {
|
|
210
|
+
update(progress) {
|
|
211
|
+
queueProgressBar.update(progress);
|
|
212
|
+
},
|
|
213
|
+
stop() {
|
|
214
|
+
queueProgressBar.stop();
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
exports.createProgressBar = createProgressBar;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface MultipartFileEntry {
|
|
2
|
+
name: string;
|
|
3
|
+
filePath: string;
|
|
4
|
+
contentType: string | null;
|
|
5
|
+
contentLength: number | null;
|
|
6
|
+
}
|
|
7
|
+
export declare const multipartContentType = "multipart/form-data; boundary=----formdata-eas-cli";
|
|
8
|
+
type OnProgressUpdateCallback = (progress: number) => void;
|
|
9
|
+
export declare function createMultipartBodyFromFilesAsync(entries: MultipartFileEntry[], onProgressUpdate?: OnProgressUpdateCallback): AsyncGenerator<Uint8Array>;
|
|
10
|
+
export {};
|