@devicecloud.dev/dcd 4.1.5 → 4.1.7
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/cloud.js +48 -44
- package/dist/commands/status.d.ts +0 -1
- package/dist/commands/status.js +10 -33
- package/dist/commands/upload.d.ts +1 -0
- package/dist/commands/upload.js +11 -9
- package/dist/config/flags/execution.flags.js +1 -1
- package/dist/gateways/api-gateway.d.ts +18 -2
- package/dist/gateways/api-gateway.js +18 -4
- package/dist/gateways/supabase-gateway.d.ts +1 -1
- package/dist/gateways/supabase-gateway.js +68 -9
- package/dist/methods.d.ts +1 -1
- package/dist/methods.js +467 -27
- package/dist/services/results-polling.service.d.ts +30 -0
- package/dist/services/results-polling.service.js +167 -71
- package/dist/services/test-submission.service.js +11 -0
- package/dist/types/generated/schema.types.d.ts +15 -0
- package/dist/types/schema.types.d.ts +1523 -0
- package/dist/types/schema.types.js +3 -0
- package/dist/utils/styling.d.ts +106 -0
- package/dist/utils/styling.js +166 -0
- package/oclif.manifest.json +8 -2
- package/package.json +16 -15
package/dist/methods.js
CHANGED
|
@@ -12,6 +12,7 @@ const StreamZip = require("node-stream-zip");
|
|
|
12
12
|
const api_gateway_1 = require("./gateways/api-gateway");
|
|
13
13
|
const supabase_gateway_1 = require("./gateways/supabase-gateway");
|
|
14
14
|
const metadata_extractor_service_1 = require("./services/metadata-extractor.service");
|
|
15
|
+
const styling_1 = require("./utils/styling");
|
|
15
16
|
const mimeTypeLookupByExtension = {
|
|
16
17
|
apk: 'application/vnd.android.package-archive',
|
|
17
18
|
yaml: 'application/x-yaml',
|
|
@@ -76,66 +77,505 @@ const verifyAppZip = async (zipPath) => {
|
|
|
76
77
|
zip.close();
|
|
77
78
|
};
|
|
78
79
|
exports.verifyAppZip = verifyAppZip;
|
|
79
|
-
const uploadBinary = async (filePath, apiUrl, apiKey, ignoreShaCheck = false, log = true) => {
|
|
80
|
+
const uploadBinary = async (filePath, apiUrl, apiKey, ignoreShaCheck = false, log = true, debug = false) => {
|
|
80
81
|
if (log) {
|
|
81
|
-
core_1.ux.action.start('Checking and uploading binary', 'Initializing', {
|
|
82
|
+
core_1.ux.action.start(styling_1.colors.bold('Checking and uploading binary'), styling_1.colors.dim('Initializing'), {
|
|
82
83
|
stdout: true,
|
|
83
84
|
});
|
|
84
85
|
}
|
|
86
|
+
if (debug) {
|
|
87
|
+
console.log('[DEBUG] Binary upload started');
|
|
88
|
+
console.log(`[DEBUG] File path: ${filePath}`);
|
|
89
|
+
console.log(`[DEBUG] API URL: ${apiUrl}`);
|
|
90
|
+
console.log(`[DEBUG] Ignore SHA check: ${ignoreShaCheck}`);
|
|
91
|
+
}
|
|
92
|
+
const startTime = Date.now();
|
|
93
|
+
try {
|
|
94
|
+
// Prepare file for upload
|
|
95
|
+
const file = await prepareFileForUpload(filePath, debug, startTime);
|
|
96
|
+
// Calculate SHA hash
|
|
97
|
+
const sha = await calculateFileHash(file, debug, log);
|
|
98
|
+
// Check for existing upload with same SHA
|
|
99
|
+
if (!ignoreShaCheck && sha) {
|
|
100
|
+
const { exists, binaryId } = await checkExistingUpload(apiUrl, apiKey, sha, debug);
|
|
101
|
+
if (exists && binaryId) {
|
|
102
|
+
if (log) {
|
|
103
|
+
core_1.ux.info(styling_1.colors.dim('SHA hash matches existing binary with ID: ') + (0, styling_1.formatId)(binaryId) + styling_1.colors.dim(', skipping upload. Force upload with --ignore-sha-check'));
|
|
104
|
+
core_1.ux.action.stop(styling_1.colors.info('Skipping upload'));
|
|
105
|
+
}
|
|
106
|
+
return binaryId;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Perform the upload
|
|
110
|
+
const uploadId = await performUpload(filePath, apiUrl, apiKey, file, sha, debug, startTime);
|
|
111
|
+
if (log) {
|
|
112
|
+
core_1.ux.action.stop(styling_1.colors.success('\n✓ Binary uploaded with ID: ') + (0, styling_1.formatId)(uploadId));
|
|
113
|
+
}
|
|
114
|
+
return uploadId;
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
if (debug) {
|
|
118
|
+
console.error('[DEBUG] Binary upload failed:', error);
|
|
119
|
+
console.error(`[DEBUG] Error type: ${error instanceof Error ? error.name : typeof error}`);
|
|
120
|
+
console.error(`[DEBUG] Error message: ${error instanceof Error ? error.message : String(error)}`);
|
|
121
|
+
if (error instanceof Error && error.stack) {
|
|
122
|
+
console.error(`[DEBUG] Stack trace: ${error.stack}`);
|
|
123
|
+
}
|
|
124
|
+
console.error(`[DEBUG] Failed after ${Date.now() - startTime}ms`);
|
|
125
|
+
}
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
exports.uploadBinary = uploadBinary;
|
|
130
|
+
/**
|
|
131
|
+
* Prepares a file for upload by reading or compressing it
|
|
132
|
+
* @param filePath Path to the file to upload
|
|
133
|
+
* @param debug Whether debug logging is enabled
|
|
134
|
+
* @param startTime Timestamp when upload started
|
|
135
|
+
* @returns Promise resolving to prepared File object
|
|
136
|
+
*/
|
|
137
|
+
async function prepareFileForUpload(filePath, debug, startTime) {
|
|
138
|
+
if (debug) {
|
|
139
|
+
console.log('[DEBUG] Preparing file for upload...');
|
|
140
|
+
}
|
|
85
141
|
let file;
|
|
86
142
|
if (filePath?.endsWith('.app')) {
|
|
143
|
+
if (debug) {
|
|
144
|
+
console.log('[DEBUG] Compressing .app folder to zip...');
|
|
145
|
+
}
|
|
87
146
|
const zippedAppBlob = await (0, exports.compressFolderToBlob)(filePath);
|
|
88
147
|
file = new File([zippedAppBlob], filePath + '.zip');
|
|
148
|
+
if (debug) {
|
|
149
|
+
console.log(`[DEBUG] Compressed file size: ${(zippedAppBlob.size / 1024 / 1024).toFixed(2)} MB`);
|
|
150
|
+
}
|
|
89
151
|
}
|
|
90
152
|
else {
|
|
153
|
+
if (debug) {
|
|
154
|
+
console.log('[DEBUG] Reading binary file...');
|
|
155
|
+
}
|
|
91
156
|
const fileBuffer = await (0, promises_1.readFile)(filePath);
|
|
157
|
+
if (debug) {
|
|
158
|
+
console.log(`[DEBUG] File size: ${(fileBuffer.length / 1024 / 1024).toFixed(2)} MB`);
|
|
159
|
+
}
|
|
92
160
|
const binaryBlob = new Blob([new Uint8Array(fileBuffer)], {
|
|
93
161
|
type: mimeTypeLookupByExtension[filePath.split('.').pop()],
|
|
94
162
|
});
|
|
95
163
|
file = new File([binaryBlob], filePath);
|
|
96
164
|
}
|
|
97
|
-
|
|
165
|
+
if (debug) {
|
|
166
|
+
console.log(`[DEBUG] File preparation completed in ${Date.now() - startTime}ms`);
|
|
167
|
+
}
|
|
168
|
+
return file;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Calculates SHA-256 hash for a file
|
|
172
|
+
* @param file File to calculate hash for
|
|
173
|
+
* @param debug Whether debug logging is enabled
|
|
174
|
+
* @param log Whether to log warnings
|
|
175
|
+
* @returns Promise resolving to SHA-256 hash or undefined if failed
|
|
176
|
+
*/
|
|
177
|
+
async function calculateFileHash(file, debug, log) {
|
|
98
178
|
try {
|
|
99
|
-
|
|
179
|
+
if (debug) {
|
|
180
|
+
console.log('[DEBUG] Calculating SHA-256 hash...');
|
|
181
|
+
}
|
|
182
|
+
const hashStartTime = Date.now();
|
|
183
|
+
const sha = await getFileHashFromFile(file);
|
|
184
|
+
if (debug) {
|
|
185
|
+
console.log(`[DEBUG] SHA-256 hash: ${sha}`);
|
|
186
|
+
console.log(`[DEBUG] Hash calculation completed in ${Date.now() - hashStartTime}ms`);
|
|
187
|
+
}
|
|
188
|
+
return sha;
|
|
100
189
|
}
|
|
101
190
|
catch (error) {
|
|
102
191
|
if (log) {
|
|
103
192
|
console.warn('Warning: Failed to get file hash', error);
|
|
104
193
|
}
|
|
194
|
+
if (debug) {
|
|
195
|
+
console.error('[DEBUG] Hash calculation failed:', error);
|
|
196
|
+
}
|
|
197
|
+
return undefined;
|
|
105
198
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Checks if an upload with the same SHA already exists
|
|
202
|
+
* @param apiUrl API base URL
|
|
203
|
+
* @param apiKey API authentication key
|
|
204
|
+
* @param sha SHA-256 hash to check
|
|
205
|
+
* @param debug Whether debug logging is enabled
|
|
206
|
+
* @returns Promise resolving to object with exists flag and optional binaryId
|
|
207
|
+
*/
|
|
208
|
+
async function checkExistingUpload(apiUrl, apiKey, sha, debug) {
|
|
209
|
+
try {
|
|
210
|
+
if (debug) {
|
|
211
|
+
console.log('[DEBUG] Checking for existing upload with matching SHA...');
|
|
212
|
+
console.log(`[DEBUG] Target endpoint: ${apiUrl}/uploads/checkForExistingUpload`);
|
|
213
|
+
}
|
|
214
|
+
const shaCheckStartTime = Date.now();
|
|
215
|
+
const { appBinaryId, exists } = await api_gateway_1.ApiGateway.checkForExistingUpload(apiUrl, apiKey, sha);
|
|
216
|
+
if (debug) {
|
|
217
|
+
console.log(`[DEBUG] SHA check completed in ${Date.now() - shaCheckStartTime}ms`);
|
|
218
|
+
console.log(`[DEBUG] Existing binary found: ${exists}`);
|
|
109
219
|
if (exists) {
|
|
110
|
-
|
|
111
|
-
core_1.ux.info(`sha hash matches existing binary with id: ${appBinaryId}, skipping upload. Force upload with --ignore-sha-check`);
|
|
112
|
-
core_1.ux.action.stop(`Skipping upload.`);
|
|
113
|
-
}
|
|
114
|
-
return appBinaryId;
|
|
220
|
+
console.log(`[DEBUG] Existing binary ID: ${appBinaryId}`);
|
|
115
221
|
}
|
|
116
222
|
}
|
|
117
|
-
|
|
118
|
-
|
|
223
|
+
return { binaryId: appBinaryId, exists };
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
if (debug) {
|
|
227
|
+
console.error('[DEBUG] SHA check failed (continuing with upload):', error);
|
|
228
|
+
console.error(`[DEBUG] Error type: ${error instanceof Error ? error.name : typeof error}`);
|
|
229
|
+
console.error(`[DEBUG] Error message: ${error instanceof Error ? error.message : String(error)}`);
|
|
230
|
+
}
|
|
231
|
+
return { exists: false };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Performs the actual file upload
|
|
236
|
+
* @param filePath Path to the file being uploaded
|
|
237
|
+
* @param apiUrl API base URL
|
|
238
|
+
* @param apiKey API authentication key
|
|
239
|
+
* @param file Prepared file to upload
|
|
240
|
+
* @param sha SHA-256 hash of the file
|
|
241
|
+
* @param debug Whether debug logging is enabled
|
|
242
|
+
* @param startTime Timestamp when upload started
|
|
243
|
+
* @returns Promise resolving to upload ID
|
|
244
|
+
*/
|
|
245
|
+
async function performUpload(filePath, apiUrl, apiKey, file, sha, debug, startTime) {
|
|
246
|
+
if (debug) {
|
|
247
|
+
console.log('[DEBUG] Requesting upload URL...');
|
|
248
|
+
console.log(`[DEBUG] Target endpoint: ${apiUrl}/uploads/getBinaryUploadUrl`);
|
|
249
|
+
console.log(`[DEBUG] Platform: ${filePath?.endsWith('.apk') ? 'android' : 'ios'}`);
|
|
250
|
+
}
|
|
251
|
+
const urlRequestStartTime = Date.now();
|
|
252
|
+
const { id, message, path, token, b2 } = await api_gateway_1.ApiGateway.getBinaryUploadUrl(apiUrl, apiKey, filePath?.endsWith('.apk') ? 'android' : 'ios', file.size);
|
|
253
|
+
if (debug) {
|
|
254
|
+
console.log(`[DEBUG] Upload URL request completed in ${Date.now() - urlRequestStartTime}ms`);
|
|
255
|
+
console.log(`[DEBUG] Upload ID: ${id}`);
|
|
256
|
+
console.log(`[DEBUG] Upload path: ${path}`);
|
|
257
|
+
console.log(`[DEBUG] Backblaze upload URL provided: ${Boolean(b2)}`);
|
|
258
|
+
if (b2) {
|
|
259
|
+
console.log(`[DEBUG] Backblaze strategy: ${b2.strategy}`);
|
|
119
260
|
}
|
|
120
261
|
}
|
|
121
|
-
const { id, message, path, token } = await api_gateway_1.ApiGateway.getBinaryUploadUrl(apiUrl, apiKey, filePath?.endsWith('.apk') ? 'android' : 'ios');
|
|
122
262
|
if (!path)
|
|
123
263
|
throw new Error(message);
|
|
124
264
|
// Extract app metadata using the service
|
|
265
|
+
if (debug) {
|
|
266
|
+
console.log('[DEBUG] Extracting app metadata...');
|
|
267
|
+
}
|
|
125
268
|
const metadataExtractor = new metadata_extractor_service_1.MetadataExtractorService();
|
|
126
269
|
const metadata = await metadataExtractor.extract(filePath);
|
|
127
270
|
if (!metadata) {
|
|
128
271
|
throw new Error(`Failed to extract metadata from ${filePath}. Supported formats: .apk, .app, .zip`);
|
|
129
272
|
}
|
|
273
|
+
if (debug) {
|
|
274
|
+
console.log(`[DEBUG] Metadata extracted: ${JSON.stringify(metadata)}`);
|
|
275
|
+
}
|
|
130
276
|
const env = apiUrl === 'https://api.devicecloud.dev' ? 'prod' : 'dev';
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
277
|
+
let supabaseSuccess = false;
|
|
278
|
+
let backblazeSuccess = false;
|
|
279
|
+
let lastError = null;
|
|
280
|
+
// Try Supabase upload first
|
|
281
|
+
if (debug) {
|
|
282
|
+
console.log(`[DEBUG] Uploading to Supabase storage (${env})...`);
|
|
283
|
+
console.log(`[DEBUG] File size: ${(file.size / 1024 / 1024).toFixed(2)} MB`);
|
|
284
|
+
}
|
|
285
|
+
try {
|
|
286
|
+
const uploadStartTime = Date.now();
|
|
287
|
+
await supabase_gateway_1.SupabaseGateway.uploadToSignedUrl(env, path, token, file, debug);
|
|
288
|
+
supabaseSuccess = true;
|
|
289
|
+
if (debug) {
|
|
290
|
+
const uploadDuration = Date.now() - uploadStartTime;
|
|
291
|
+
const uploadSpeed = (file.size / 1024 / 1024) / (uploadDuration / 1000);
|
|
292
|
+
console.log(`[DEBUG] Supabase upload completed in ${uploadDuration}ms`);
|
|
293
|
+
console.log(`[DEBUG] Average upload speed: ${uploadSpeed.toFixed(2)} MB/s`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
298
|
+
if (debug) {
|
|
299
|
+
console.error(`[DEBUG] === SUPABASE UPLOAD FAILED ===`);
|
|
300
|
+
console.error(`[DEBUG] Error message: ${lastError.message}`);
|
|
301
|
+
console.error(`[DEBUG] Error name: ${lastError.name}`);
|
|
302
|
+
if (lastError.stack) {
|
|
303
|
+
console.error(`[DEBUG] Error stack:\n${lastError.stack}`);
|
|
304
|
+
}
|
|
305
|
+
console.error(`[DEBUG] Upload path: ${path}`);
|
|
306
|
+
console.error(`[DEBUG] File size: ${file.size} bytes`);
|
|
307
|
+
console.log('[DEBUG] Will attempt Backblaze fallback if available...');
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
// Try Backblaze upload (either as backup or fallback)
|
|
311
|
+
if (b2) {
|
|
312
|
+
if (debug) {
|
|
313
|
+
if (supabaseSuccess) {
|
|
314
|
+
console.log('[DEBUG] Starting Backblaze backup upload...');
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
console.log('[DEBUG] Starting Backblaze fallback upload...');
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
try {
|
|
321
|
+
const b2UploadStartTime = Date.now();
|
|
322
|
+
if (b2.strategy === 'simple' && b2.simple) {
|
|
323
|
+
// Simple upload for files < 100MB
|
|
324
|
+
backblazeSuccess = await uploadToBackblaze(b2.simple.uploadUrl, b2.simple.authorizationToken, `organizations/${path}`, file, debug);
|
|
325
|
+
}
|
|
326
|
+
else if (b2.strategy === 'large' && b2.large) {
|
|
327
|
+
// Multi-part upload for files >= 100MB (uses streaming to avoid memory issues)
|
|
328
|
+
backblazeSuccess = await uploadLargeFileToBackblaze(apiUrl, apiKey, b2.large.fileId, b2.large.uploadPartUrls, `organizations/${path}`, filePath, file.size, debug, file);
|
|
329
|
+
}
|
|
330
|
+
if (debug) {
|
|
331
|
+
const b2UploadDuration = Date.now() - b2UploadStartTime;
|
|
332
|
+
if (backblazeSuccess) {
|
|
333
|
+
console.log(`[DEBUG] Backblaze upload completed successfully in ${b2UploadDuration}ms`);
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
console.log(`[DEBUG] Backblaze upload failed after ${b2UploadDuration}ms`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
// This catch block should rarely be reached now since upload functions handle their own errors
|
|
342
|
+
const b2Error = error instanceof Error ? error : new Error(String(error));
|
|
343
|
+
if (debug) {
|
|
344
|
+
console.error(`[DEBUG] === UNEXPECTED BACKBLAZE UPLOAD ERROR ===`);
|
|
345
|
+
console.error(`[DEBUG] Error message: ${b2Error.message}`);
|
|
346
|
+
console.error(`[DEBUG] Error name: ${b2Error.name}`);
|
|
347
|
+
if (b2Error.stack) {
|
|
348
|
+
console.error(`[DEBUG] Error stack:\n${b2Error.stack}`);
|
|
349
|
+
}
|
|
350
|
+
console.error(`[DEBUG] Upload strategy: ${b2.strategy}`);
|
|
351
|
+
}
|
|
352
|
+
backblazeSuccess = false;
|
|
353
|
+
// Only update lastError if Supabase also failed
|
|
354
|
+
if (!supabaseSuccess) {
|
|
355
|
+
lastError = b2Error;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
else if (debug && !supabaseSuccess) {
|
|
360
|
+
console.log('[DEBUG] Backblaze not configured, cannot fallback');
|
|
361
|
+
}
|
|
362
|
+
// Check if at least one upload succeeded
|
|
363
|
+
if (!supabaseSuccess && !backblazeSuccess) {
|
|
364
|
+
if (debug) {
|
|
365
|
+
console.error(`[DEBUG] === ALL UPLOADS FAILED ===`);
|
|
366
|
+
console.error(`[DEBUG] Supabase upload: FAILED`);
|
|
367
|
+
console.error(`[DEBUG] Backblaze upload: ${b2 ? 'FAILED' : 'NOT CONFIGURED'}`);
|
|
368
|
+
if (lastError) {
|
|
369
|
+
console.error(`[DEBUG] Final error details:`);
|
|
370
|
+
console.error(`[DEBUG] - Message: ${lastError.message}`);
|
|
371
|
+
console.error(`[DEBUG] - Name: ${lastError.name}`);
|
|
372
|
+
console.error(`[DEBUG] - Stack: ${lastError.stack}`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
throw new Error(`All uploads failed. ${lastError ? `Last error: ${JSON.stringify({ message: lastError.message, name: lastError.name, stack: lastError.stack })}` : 'No upload targets available.'}`);
|
|
376
|
+
}
|
|
377
|
+
if (debug) {
|
|
378
|
+
console.log(`[DEBUG] Upload summary - Supabase: ${supabaseSuccess ? '✓' : '✗'}, Backblaze: ${backblazeSuccess ? '✓' : '✗'}`);
|
|
379
|
+
if (!supabaseSuccess && backblazeSuccess) {
|
|
380
|
+
console.log('[DEBUG] ⚠ Warning: File only exists in Backblaze (Supabase failed)');
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (debug) {
|
|
384
|
+
console.log('[DEBUG] Finalizing upload...');
|
|
385
|
+
console.log(`[DEBUG] Target endpoint: ${apiUrl}/uploads/finaliseUpload`);
|
|
386
|
+
console.log(`[DEBUG] Supabase upload status: ${supabaseSuccess ? 'SUCCESS' : 'FAILED'}`);
|
|
387
|
+
console.log(`[DEBUG] Backblaze upload status: ${backblazeSuccess ? 'SUCCESS' : 'FAILED'}`);
|
|
388
|
+
}
|
|
389
|
+
const finalizeStartTime = Date.now();
|
|
390
|
+
await api_gateway_1.ApiGateway.finaliseUpload(apiUrl, apiKey, id, metadata, path, sha, supabaseSuccess, backblazeSuccess);
|
|
391
|
+
if (debug) {
|
|
392
|
+
console.log(`[DEBUG] Upload finalization completed in ${Date.now() - finalizeStartTime}ms`);
|
|
393
|
+
console.log(`[DEBUG] Total upload time: ${Date.now() - startTime}ms`);
|
|
135
394
|
}
|
|
136
395
|
return id;
|
|
137
|
-
}
|
|
138
|
-
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Upload file to Backblaze using signed URL (simple upload for files < 100MB)
|
|
399
|
+
* @param uploadUrl - Backblaze upload URL
|
|
400
|
+
* @param authorizationToken - Authorization token for the upload
|
|
401
|
+
* @param fileName - Name/path of the file
|
|
402
|
+
* @param file - File to upload
|
|
403
|
+
* @param debug - Whether debug logging is enabled
|
|
404
|
+
* @returns Promise that resolves when upload completes or fails gracefully
|
|
405
|
+
*/
|
|
406
|
+
async function uploadToBackblaze(uploadUrl, authorizationToken, fileName, file, debug) {
|
|
407
|
+
try {
|
|
408
|
+
const arrayBuffer = await file.arrayBuffer();
|
|
409
|
+
// Calculate SHA1 hash for Backblaze (B2 requires SHA1, not SHA256)
|
|
410
|
+
const sha1 = (0, node_crypto_1.createHash)('sha1');
|
|
411
|
+
sha1.update(Buffer.from(arrayBuffer));
|
|
412
|
+
const sha1Hex = sha1.digest('hex');
|
|
413
|
+
// Detect if this is an S3 pre-signed URL (authorization token is empty)
|
|
414
|
+
const isS3PreSignedUrl = !authorizationToken || authorizationToken === '';
|
|
415
|
+
if (debug) {
|
|
416
|
+
console.log(`[DEBUG] Uploading to Backblaze URL: ${uploadUrl}`);
|
|
417
|
+
console.log(`[DEBUG] Upload method: ${isS3PreSignedUrl ? 'S3 pre-signed URL (PUT)' : 'B2 native API (POST)'}`);
|
|
418
|
+
console.log(`[DEBUG] File name: ${fileName}`);
|
|
419
|
+
console.log(`[DEBUG] File SHA1: ${sha1Hex}`);
|
|
420
|
+
}
|
|
421
|
+
// Build headers based on upload method
|
|
422
|
+
const headers = {
|
|
423
|
+
'Content-Length': file.size.toString(),
|
|
424
|
+
'Content-Type': file.type || 'application/octet-stream',
|
|
425
|
+
'X-Bz-Content-Sha1': sha1Hex,
|
|
426
|
+
};
|
|
427
|
+
// S3 pre-signed URLs have auth embedded in URL, native B2 uses Authorization header
|
|
428
|
+
if (!isS3PreSignedUrl) {
|
|
429
|
+
headers.Authorization = authorizationToken;
|
|
430
|
+
headers['X-Bz-File-Name'] = encodeURIComponent(fileName);
|
|
431
|
+
}
|
|
432
|
+
const response = await fetch(uploadUrl, {
|
|
433
|
+
body: arrayBuffer,
|
|
434
|
+
headers,
|
|
435
|
+
method: isS3PreSignedUrl ? 'PUT' : 'POST',
|
|
436
|
+
});
|
|
437
|
+
if (!response.ok) {
|
|
438
|
+
const errorText = await response.text();
|
|
439
|
+
if (debug) {
|
|
440
|
+
console.error(`[DEBUG] Backblaze upload failed with status ${response.status}: ${errorText}`);
|
|
441
|
+
}
|
|
442
|
+
// Don't throw - we don't want Backblaze failures to block the primary upload
|
|
443
|
+
console.warn(`Warning: Backblaze upload failed with status ${response.status}`);
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
if (debug) {
|
|
447
|
+
console.log('[DEBUG] Backblaze upload successful');
|
|
448
|
+
}
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
catch (error) {
|
|
452
|
+
if (debug) {
|
|
453
|
+
console.error('[DEBUG] Backblaze upload exception:', error);
|
|
454
|
+
}
|
|
455
|
+
// Don't throw - we don't want Backblaze failures to block the primary upload
|
|
456
|
+
console.warn(`Warning: Backblaze upload failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Helper function to read a chunk from a file stream
|
|
462
|
+
* @param filePath - Path to the file
|
|
463
|
+
* @param start - Start byte position
|
|
464
|
+
* @param end - End byte position (exclusive)
|
|
465
|
+
* @returns Promise resolving to Buffer containing the chunk
|
|
466
|
+
*/
|
|
467
|
+
async function readFileChunk(filePath, start, end) {
|
|
468
|
+
return new Promise((resolve, reject) => {
|
|
469
|
+
const chunks = [];
|
|
470
|
+
const stream = (0, node_fs_1.createReadStream)(filePath, { start, end: end - 1 }); // end is inclusive in createReadStream
|
|
471
|
+
stream.on('data', (chunk) => {
|
|
472
|
+
chunks.push(chunk);
|
|
473
|
+
});
|
|
474
|
+
stream.on('end', () => {
|
|
475
|
+
resolve(Buffer.concat(chunks));
|
|
476
|
+
});
|
|
477
|
+
stream.on('error', (error) => {
|
|
478
|
+
reject(error);
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Helper function to read a chunk from a File/Blob object
|
|
484
|
+
* @param file - File or Blob object
|
|
485
|
+
* @param start - Start byte position
|
|
486
|
+
* @param end - End byte position (exclusive)
|
|
487
|
+
* @returns Promise resolving to Buffer containing the chunk
|
|
488
|
+
*/
|
|
489
|
+
async function readFileObjectChunk(file, start, end) {
|
|
490
|
+
const slice = file.slice(start, end);
|
|
491
|
+
const arrayBuffer = await slice.arrayBuffer();
|
|
492
|
+
return Buffer.from(arrayBuffer);
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Upload large file to Backblaze using multi-part upload with streaming (for files >= 5MB)
|
|
496
|
+
* Uses file streaming to avoid loading entire file into memory, preventing OOM errors on large files
|
|
497
|
+
* @param apiUrl - API base URL
|
|
498
|
+
* @param apiKey - API key for authentication
|
|
499
|
+
* @param fileId - Backblaze file ID
|
|
500
|
+
* @param uploadPartUrls - Array of upload URLs for each part
|
|
501
|
+
* @param fileName - Name/path of the file in Backblaze
|
|
502
|
+
* @param filePath - Local file path to upload from (used when fileObject is not provided)
|
|
503
|
+
* @param fileSize - Total size of the file in bytes
|
|
504
|
+
* @param debug - Whether debug logging is enabled
|
|
505
|
+
* @param fileObject - Optional File/Blob object to upload from (used for .app bundles that are compressed in memory)
|
|
506
|
+
* @returns Promise that resolves when upload completes or fails gracefully
|
|
507
|
+
*/
|
|
508
|
+
async function uploadLargeFileToBackblaze(apiUrl, apiKey, fileId, uploadPartUrls, _fileName, filePath, fileSize, debug, fileObject) {
|
|
509
|
+
try {
|
|
510
|
+
const partSha1Array = [];
|
|
511
|
+
// Calculate part size (divide file evenly across all parts)
|
|
512
|
+
const partSize = Math.ceil(fileSize / uploadPartUrls.length);
|
|
513
|
+
if (debug) {
|
|
514
|
+
console.log(`[DEBUG] Uploading large file in ${uploadPartUrls.length} parts (streaming mode)`);
|
|
515
|
+
console.log(`[DEBUG] Part size: ${(partSize / 1024 / 1024).toFixed(2)} MB`);
|
|
516
|
+
console.log(`[DEBUG] Reading from: ${fileObject ? 'in-memory File object' : filePath}`);
|
|
517
|
+
}
|
|
518
|
+
// Upload each part using streaming to avoid loading entire file into memory
|
|
519
|
+
for (let i = 0; i < uploadPartUrls.length; i++) {
|
|
520
|
+
const partNumber = i + 1;
|
|
521
|
+
const start = i * partSize;
|
|
522
|
+
const end = Math.min(start + partSize, fileSize);
|
|
523
|
+
const partLength = end - start;
|
|
524
|
+
if (debug) {
|
|
525
|
+
console.log(`[DEBUG] Reading part ${partNumber}/${uploadPartUrls.length} bytes ${start}-${end}`);
|
|
526
|
+
}
|
|
527
|
+
// Read part from File object (for .app bundles) or from disk
|
|
528
|
+
const partBuffer = fileObject
|
|
529
|
+
? await readFileObjectChunk(fileObject, start, end)
|
|
530
|
+
: await readFileChunk(filePath, start, end);
|
|
531
|
+
// Calculate SHA1 for this part
|
|
532
|
+
const sha1 = (0, node_crypto_1.createHash)('sha1');
|
|
533
|
+
sha1.update(partBuffer);
|
|
534
|
+
const sha1Hex = sha1.digest('hex');
|
|
535
|
+
partSha1Array.push(sha1Hex);
|
|
536
|
+
if (debug) {
|
|
537
|
+
console.log(`[DEBUG] Uploading part ${partNumber}/${uploadPartUrls.length} (${(partLength / 1024 / 1024).toFixed(2)} MB, SHA1: ${sha1Hex})`);
|
|
538
|
+
}
|
|
539
|
+
const response = await fetch(uploadPartUrls[i].uploadUrl, {
|
|
540
|
+
body: new Uint8Array(partBuffer),
|
|
541
|
+
headers: {
|
|
542
|
+
Authorization: uploadPartUrls[i].authorizationToken,
|
|
543
|
+
'Content-Length': partLength.toString(),
|
|
544
|
+
'X-Bz-Content-Sha1': sha1Hex,
|
|
545
|
+
'X-Bz-Part-Number': partNumber.toString(),
|
|
546
|
+
},
|
|
547
|
+
method: 'POST',
|
|
548
|
+
});
|
|
549
|
+
if (!response.ok) {
|
|
550
|
+
const errorText = await response.text();
|
|
551
|
+
if (debug) {
|
|
552
|
+
console.error(`[DEBUG] Part ${partNumber} upload failed with status ${response.status}: ${errorText}`);
|
|
553
|
+
}
|
|
554
|
+
throw new Error(`Part ${partNumber} upload failed with status ${response.status}`);
|
|
555
|
+
}
|
|
556
|
+
if (debug) {
|
|
557
|
+
console.log(`[DEBUG] Part ${partNumber}/${uploadPartUrls.length} uploaded successfully`);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
// Finish the large file upload
|
|
561
|
+
if (debug) {
|
|
562
|
+
console.log('[DEBUG] Finishing large file upload...');
|
|
563
|
+
}
|
|
564
|
+
await api_gateway_1.ApiGateway.finishLargeFile(apiUrl, apiKey, fileId, partSha1Array);
|
|
565
|
+
if (debug) {
|
|
566
|
+
console.log('[DEBUG] Large file upload completed successfully');
|
|
567
|
+
}
|
|
568
|
+
return true;
|
|
569
|
+
}
|
|
570
|
+
catch (error) {
|
|
571
|
+
if (debug) {
|
|
572
|
+
console.error('[DEBUG] Large file upload exception:', error);
|
|
573
|
+
}
|
|
574
|
+
// Don't throw - we don't want Backblaze failures to block the primary upload
|
|
575
|
+
console.warn(`Warning: Backblaze large file upload failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
576
|
+
return false;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
139
579
|
async function getFileHashFromFile(file) {
|
|
140
580
|
return new Promise((resolve, reject) => {
|
|
141
581
|
const hash = (0, node_crypto_1.createHash)('sha256');
|
|
@@ -168,21 +608,21 @@ async function getFileHashFromFile(file) {
|
|
|
168
608
|
const writeJSONFile = (filePath, data, logger) => {
|
|
169
609
|
try {
|
|
170
610
|
(0, node_fs_1.writeFileSync)(filePath, JSON.stringify(data, null, 2));
|
|
171
|
-
logger.log(
|
|
611
|
+
logger.log(styling_1.colors.dim('JSON output written to: ') + styling_1.colors.highlight(path.resolve(filePath)));
|
|
172
612
|
}
|
|
173
613
|
catch (error) {
|
|
174
614
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
175
615
|
const isPermissionError = errorMessage.includes('EACCES') || errorMessage.includes('EPERM');
|
|
176
616
|
const isNoSuchFileError = errorMessage.includes('ENOENT');
|
|
177
|
-
logger.warn(`Failed to write JSON output to file: ${filePath}`);
|
|
617
|
+
logger.warn(styling_1.colors.warning('⚠') + ' ' + styling_1.colors.error(`Failed to write JSON output to file: ${filePath}`));
|
|
178
618
|
if (isPermissionError) {
|
|
179
|
-
logger.warn('Permission denied - check file/directory write permissions');
|
|
180
|
-
logger.warn('Try running with appropriate permissions or choose a different output location');
|
|
619
|
+
logger.warn(styling_1.colors.dim(' Permission denied - check file/directory write permissions'));
|
|
620
|
+
logger.warn(styling_1.colors.dim(' Try running with appropriate permissions or choose a different output location'));
|
|
181
621
|
}
|
|
182
622
|
else if (isNoSuchFileError) {
|
|
183
|
-
logger.warn('Directory does not exist - create the directory first or choose an existing path');
|
|
623
|
+
logger.warn(styling_1.colors.dim(' Directory does not exist - create the directory first or choose an existing path'));
|
|
184
624
|
}
|
|
185
|
-
logger.warn(
|
|
625
|
+
logger.warn(styling_1.colors.dim(' Error details: ') + errorMessage);
|
|
186
626
|
}
|
|
187
627
|
};
|
|
188
628
|
exports.writeJSONFile = writeJSONFile;
|
|
@@ -44,8 +44,38 @@ export declare class ResultsPollingService {
|
|
|
44
44
|
private buildPollingResult;
|
|
45
45
|
private calculateStatusSummary;
|
|
46
46
|
private displayFinalResults;
|
|
47
|
+
/**
|
|
48
|
+
* Fetch results from API and log debug information
|
|
49
|
+
* @param apiUrl API base URL
|
|
50
|
+
* @param apiKey API authentication key
|
|
51
|
+
* @param uploadId Upload ID to fetch results for
|
|
52
|
+
* @param debug Whether debug logging is enabled
|
|
53
|
+
* @param logger Optional logger function
|
|
54
|
+
* @returns Promise resolving to test results
|
|
55
|
+
*/
|
|
56
|
+
private fetchAndLogResults;
|
|
47
57
|
private filterLatestResults;
|
|
58
|
+
/**
|
|
59
|
+
* Handle completed tests and return final result
|
|
60
|
+
* @param updatedResults Test results from API
|
|
61
|
+
* @param options Completion handling options
|
|
62
|
+
* @returns Promise resolving to final polling result
|
|
63
|
+
*/
|
|
64
|
+
private handleCompletedTests;
|
|
48
65
|
private handlePollingError;
|
|
66
|
+
/**
|
|
67
|
+
* Initialize the polling display UI
|
|
68
|
+
* @param json Whether to output in JSON format
|
|
69
|
+
* @param logger Optional logger function for output
|
|
70
|
+
* @returns void
|
|
71
|
+
*/
|
|
72
|
+
private initializePollingDisplay;
|
|
73
|
+
/**
|
|
74
|
+
* Sleep for the specified number of milliseconds
|
|
75
|
+
* @param ms Number of milliseconds to sleep
|
|
76
|
+
* @returns Promise that resolves after the delay
|
|
77
|
+
*/
|
|
78
|
+
private sleep;
|
|
49
79
|
private updateDisplayStatus;
|
|
50
80
|
}
|
|
51
81
|
export {};
|