@devicecloud.dev/dcd 4.1.6 → 4.1.9-beta.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/dist/commands/cloud.d.ts +0 -1
- package/dist/commands/cloud.js +45 -39
- package/dist/commands/upload.js +8 -1
- package/dist/config/flags/device.flags.d.ts +0 -1
- package/dist/config/flags/device.flags.js +0 -4
- package/dist/constants.d.ts +0 -1
- package/dist/gateways/api-gateway.d.ts +15 -4
- package/dist/gateways/api-gateway.js +35 -5
- package/dist/gateways/supabase-gateway.d.ts +28 -0
- package/dist/gateways/supabase-gateway.js +159 -19
- package/dist/methods.d.ts +10 -1
- package/dist/methods.js +386 -32
- package/dist/services/device-validation.service.js +7 -7
- package/dist/services/moropo.service.js +6 -6
- package/dist/services/report-download.service.js +4 -4
- package/dist/services/results-polling.service.js +13 -13
- package/dist/services/test-submission.service.d.ts +0 -1
- package/dist/services/test-submission.service.js +8 -9
- package/dist/services/version.service.js +4 -4
- package/dist/types/generated/schema.types.d.ts +117 -8
- package/dist/types/schema.types.d.ts +113 -6
- package/oclif.manifest.json +1 -7
- package/package.json +4 -2
package/dist/methods.js
CHANGED
|
@@ -77,7 +77,8 @@ const verifyAppZip = async (zipPath) => {
|
|
|
77
77
|
zip.close();
|
|
78
78
|
};
|
|
79
79
|
exports.verifyAppZip = verifyAppZip;
|
|
80
|
-
const uploadBinary = async (
|
|
80
|
+
const uploadBinary = async (config) => {
|
|
81
|
+
const { filePath, apiUrl, apiKey, ignoreShaCheck = false, log = true, debug = false } = config;
|
|
81
82
|
if (log) {
|
|
82
83
|
core_1.ux.action.start(styling_1.colors.bold('Checking and uploading binary'), styling_1.colors.dim('Initializing'), {
|
|
83
84
|
stdout: true,
|
|
@@ -107,7 +108,7 @@ const uploadBinary = async (filePath, apiUrl, apiKey, ignoreShaCheck = false, lo
|
|
|
107
108
|
}
|
|
108
109
|
}
|
|
109
110
|
// Perform the upload
|
|
110
|
-
const uploadId = await performUpload(
|
|
111
|
+
const uploadId = await performUpload({ apiKey, apiUrl, debug, file, filePath, sha, startTime });
|
|
111
112
|
if (log) {
|
|
112
113
|
core_1.ux.action.stop(styling_1.colors.success('\n✓ Binary uploaded with ID: ') + (0, styling_1.formatId)(uploadId));
|
|
113
114
|
}
|
|
@@ -232,68 +233,421 @@ async function checkExistingUpload(apiUrl, apiKey, sha, debug) {
|
|
|
232
233
|
}
|
|
233
234
|
}
|
|
234
235
|
/**
|
|
235
|
-
*
|
|
236
|
-
* @param
|
|
237
|
-
* @param
|
|
238
|
-
* @param
|
|
239
|
-
* @param
|
|
240
|
-
* @
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
236
|
+
* Uploads file to Supabase using resumable uploads
|
|
237
|
+
* @param env - Environment (dev or prod)
|
|
238
|
+
* @param tempPath - Temporary staging path for upload
|
|
239
|
+
* @param file - File to upload
|
|
240
|
+
* @param debug - Enable debug logging
|
|
241
|
+
* @returns Upload result with success status and any error
|
|
242
|
+
*/
|
|
243
|
+
async function uploadToSupabase(env, tempPath, file, debug) {
|
|
244
|
+
if (debug) {
|
|
245
|
+
console.log(`[DEBUG] Uploading to Supabase storage (${env}) using resumable uploads...`);
|
|
246
|
+
console.log(`[DEBUG] Staging path: ${tempPath}`);
|
|
247
|
+
console.log(`[DEBUG] File size: ${(file.size / 1024 / 1024).toFixed(2)} MB`);
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
const uploadStartTime = Date.now();
|
|
251
|
+
await supabase_gateway_1.SupabaseGateway.uploadResumable(env, tempPath, file, debug);
|
|
252
|
+
if (debug) {
|
|
253
|
+
const uploadDuration = Date.now() - uploadStartTime;
|
|
254
|
+
const uploadDurationSeconds = uploadDuration / 1000;
|
|
255
|
+
const uploadSpeed = (file.size / 1024 / 1024) / uploadDurationSeconds;
|
|
256
|
+
console.log(`[DEBUG] Supabase resumable upload completed in ${uploadDurationSeconds.toFixed(2)}s (${uploadDuration}ms)`);
|
|
257
|
+
console.log(`[DEBUG] Average upload speed: ${uploadSpeed.toFixed(2)} MB/s`);
|
|
258
|
+
}
|
|
259
|
+
return { error: null, success: true };
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
const uploadError = error instanceof Error ? error : new Error(String(error));
|
|
263
|
+
if (debug) {
|
|
264
|
+
console.error(`[DEBUG] === SUPABASE RESUMABLE UPLOAD FAILED ===`);
|
|
265
|
+
console.error(`[DEBUG] Error message: ${uploadError.message}`);
|
|
266
|
+
console.error(`[DEBUG] Error name: ${uploadError.name}`);
|
|
267
|
+
if (uploadError.stack) {
|
|
268
|
+
console.error(`[DEBUG] Error stack:\n${uploadError.stack}`);
|
|
269
|
+
}
|
|
270
|
+
console.error(`[DEBUG] Staging path: ${tempPath}`);
|
|
271
|
+
console.error(`[DEBUG] File size: ${file.size} bytes`);
|
|
272
|
+
console.log('[DEBUG] Will attempt Backblaze fallback if available...');
|
|
273
|
+
}
|
|
274
|
+
return { error: uploadError, success: false };
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Handles Backblaze upload with appropriate strategy
|
|
279
|
+
* @param config - Configuration object for Backblaze upload
|
|
280
|
+
* @returns Upload result with success status and any error
|
|
244
281
|
*/
|
|
245
|
-
async function
|
|
282
|
+
async function handleBackblazeUpload(config) {
|
|
283
|
+
const { b2, apiUrl, apiKey, finalPath, file, filePath, debug, supabaseSuccess } = config;
|
|
284
|
+
if (!b2) {
|
|
285
|
+
if (debug && !supabaseSuccess) {
|
|
286
|
+
console.log('[DEBUG] Backblaze not configured, cannot fallback');
|
|
287
|
+
}
|
|
288
|
+
return { error: null, success: false };
|
|
289
|
+
}
|
|
290
|
+
if (debug) {
|
|
291
|
+
console.log(supabaseSuccess ? '[DEBUG] Starting Backblaze backup upload...' : '[DEBUG] Starting Backblaze fallback upload...');
|
|
292
|
+
}
|
|
293
|
+
try {
|
|
294
|
+
const b2UploadStartTime = Date.now();
|
|
295
|
+
let backblazeSuccess = false;
|
|
296
|
+
if (b2.strategy === 'simple' && b2.simple) {
|
|
297
|
+
const simple = b2.simple;
|
|
298
|
+
backblazeSuccess = await uploadToBackblaze(simple.uploadUrl, simple.authorizationToken, `organizations/${finalPath}`, file, debug);
|
|
299
|
+
}
|
|
300
|
+
else if (b2.strategy === 'large' && b2.large) {
|
|
301
|
+
const large = b2.large;
|
|
302
|
+
backblazeSuccess = await uploadLargeFileToBackblaze({
|
|
303
|
+
apiKey,
|
|
304
|
+
apiUrl,
|
|
305
|
+
debug,
|
|
306
|
+
fileId: large.fileId,
|
|
307
|
+
fileName: `organizations/${finalPath}`,
|
|
308
|
+
fileObject: file,
|
|
309
|
+
filePath,
|
|
310
|
+
fileSize: file.size,
|
|
311
|
+
uploadPartUrls: large.uploadPartUrls,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
if (debug) {
|
|
315
|
+
const duration = Date.now() - b2UploadStartTime;
|
|
316
|
+
const durationSeconds = duration / 1000;
|
|
317
|
+
console.log(backblazeSuccess
|
|
318
|
+
? `[DEBUG] Backblaze upload completed successfully in ${durationSeconds.toFixed(2)}s (${duration}ms)`
|
|
319
|
+
: `[DEBUG] Backblaze upload failed after ${durationSeconds.toFixed(2)}s (${duration}ms)`);
|
|
320
|
+
}
|
|
321
|
+
return { error: null, success: backblazeSuccess };
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
const b2Error = error instanceof Error ? error : new Error(String(error));
|
|
325
|
+
if (debug) {
|
|
326
|
+
console.error(`[DEBUG] === UNEXPECTED BACKBLAZE UPLOAD ERROR ===`);
|
|
327
|
+
console.error(`[DEBUG] Error message: ${b2Error.message}`);
|
|
328
|
+
console.error(`[DEBUG] Error name: ${b2Error.name}`);
|
|
329
|
+
if (b2Error.stack) {
|
|
330
|
+
console.error(`[DEBUG] Error stack:\n${b2Error.stack}`);
|
|
331
|
+
}
|
|
332
|
+
console.error(`[DEBUG] Upload strategy: ${b2.strategy}`);
|
|
333
|
+
}
|
|
334
|
+
return { error: b2Error, success: false };
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Requests upload URL and paths from API
|
|
339
|
+
* @param apiUrl - API base URL
|
|
340
|
+
* @param apiKey - API authentication key
|
|
341
|
+
* @param filePath - Path to the file being uploaded
|
|
342
|
+
* @param fileSize - Size of the file in bytes
|
|
343
|
+
* @param debug - Enable debug logging
|
|
344
|
+
* @returns Promise resolving to upload paths and configuration
|
|
345
|
+
*/
|
|
346
|
+
async function requestUploadPaths(apiUrl, apiKey, filePath, fileSize, debug) {
|
|
347
|
+
const platform = filePath?.endsWith('.apk') ? 'android' : 'ios';
|
|
246
348
|
if (debug) {
|
|
247
349
|
console.log('[DEBUG] Requesting upload URL...');
|
|
248
350
|
console.log(`[DEBUG] Target endpoint: ${apiUrl}/uploads/getBinaryUploadUrl`);
|
|
249
|
-
console.log(`[DEBUG] Platform: ${
|
|
351
|
+
console.log(`[DEBUG] Platform: ${platform}`);
|
|
250
352
|
}
|
|
251
353
|
const urlRequestStartTime = Date.now();
|
|
252
|
-
const { id,
|
|
354
|
+
const { id, tempPath, finalPath, b2 } = await api_gateway_1.ApiGateway.getBinaryUploadUrl(apiUrl, apiKey, platform, fileSize);
|
|
253
355
|
if (debug) {
|
|
356
|
+
const hasStrategy = b2 && typeof b2 === 'object' && 'strategy' in b2;
|
|
254
357
|
console.log(`[DEBUG] Upload URL request completed in ${Date.now() - urlRequestStartTime}ms`);
|
|
255
358
|
console.log(`[DEBUG] Upload ID: ${id}`);
|
|
256
|
-
console.log(`[DEBUG]
|
|
359
|
+
console.log(`[DEBUG] Temp path (TUS upload): ${tempPath}`);
|
|
360
|
+
console.log(`[DEBUG] Final path (after finalize): ${finalPath}`);
|
|
361
|
+
console.log(`[DEBUG] Backblaze upload URL provided: ${Boolean(b2)}`);
|
|
362
|
+
if (hasStrategy)
|
|
363
|
+
console.log(`[DEBUG] Backblaze strategy: ${b2.strategy}`);
|
|
257
364
|
}
|
|
258
|
-
if (!
|
|
259
|
-
throw new Error(
|
|
260
|
-
|
|
261
|
-
|
|
365
|
+
if (!tempPath)
|
|
366
|
+
throw new Error('No upload path provided by API');
|
|
367
|
+
return { b2, finalPath, id, tempPath };
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Extracts metadata from the binary file
|
|
371
|
+
* @param filePath - Path to the binary file
|
|
372
|
+
* @param debug - Enable debug logging
|
|
373
|
+
* @returns Promise resolving to extracted metadata containing appId and platform
|
|
374
|
+
*/
|
|
375
|
+
async function extractBinaryMetadata(filePath, debug) {
|
|
376
|
+
if (debug)
|
|
262
377
|
console.log('[DEBUG] Extracting app metadata...');
|
|
263
|
-
}
|
|
264
378
|
const metadataExtractor = new metadata_extractor_service_1.MetadataExtractorService();
|
|
265
379
|
const metadata = await metadataExtractor.extract(filePath);
|
|
266
380
|
if (!metadata) {
|
|
267
381
|
throw new Error(`Failed to extract metadata from ${filePath}. Supported formats: .apk, .app, .zip`);
|
|
268
382
|
}
|
|
269
|
-
if (debug)
|
|
383
|
+
if (debug)
|
|
270
384
|
console.log(`[DEBUG] Metadata extracted: ${JSON.stringify(metadata)}`);
|
|
385
|
+
return metadata;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Validates upload results and throws if all uploads failed
|
|
389
|
+
* @param supabaseSuccess - Whether Supabase upload succeeded
|
|
390
|
+
* @param backblazeSuccess - Whether Backblaze upload succeeded
|
|
391
|
+
* @param lastError - Last error encountered during uploads
|
|
392
|
+
* @param b2 - Backblaze configuration
|
|
393
|
+
* @param debug - Enable debug logging
|
|
394
|
+
* @returns void - throws error if all uploads failed
|
|
395
|
+
*/
|
|
396
|
+
function validateUploadResults(supabaseSuccess, backblazeSuccess, lastError, b2, debug) {
|
|
397
|
+
if (supabaseSuccess || backblazeSuccess) {
|
|
398
|
+
return;
|
|
271
399
|
}
|
|
272
|
-
const env = apiUrl === 'https://api.devicecloud.dev' ? 'prod' : 'dev';
|
|
273
400
|
if (debug) {
|
|
274
|
-
console.
|
|
275
|
-
console.
|
|
401
|
+
console.error(`[DEBUG] === ALL UPLOADS FAILED ===`);
|
|
402
|
+
console.error(`[DEBUG] Supabase upload: FAILED`);
|
|
403
|
+
console.error(`[DEBUG] Backblaze upload: ${b2 ? 'FAILED' : 'NOT CONFIGURED'}`);
|
|
404
|
+
if (lastError) {
|
|
405
|
+
console.error(`[DEBUG] Final error details:`);
|
|
406
|
+
console.error(`[DEBUG] - Message: ${lastError.message}`);
|
|
407
|
+
console.error(`[DEBUG] - Name: ${lastError.name}`);
|
|
408
|
+
console.error(`[DEBUG] - Stack: ${lastError.stack}`);
|
|
409
|
+
}
|
|
276
410
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
411
|
+
throw new Error(`All uploads failed. ${lastError ? `Last error: ${JSON.stringify({ message: lastError.message, name: lastError.name, stack: lastError.stack })}` : 'No upload targets available.'}`);
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Performs the actual file upload
|
|
415
|
+
* @param config Configuration object for the upload
|
|
416
|
+
* @returns Promise resolving to upload ID
|
|
417
|
+
*/
|
|
418
|
+
async function performUpload(config) {
|
|
419
|
+
const { filePath, apiUrl, apiKey, file, sha, debug, startTime } = config;
|
|
420
|
+
// Request upload URL and paths
|
|
421
|
+
const { id, tempPath, finalPath, b2 } = await requestUploadPaths(apiUrl, apiKey, filePath, file.size, debug);
|
|
422
|
+
// Extract app metadata
|
|
423
|
+
const metadata = await extractBinaryMetadata(filePath, debug);
|
|
424
|
+
const env = apiUrl === 'https://api.devicecloud.dev' ? 'prod' : 'dev';
|
|
425
|
+
// Upload to Supabase
|
|
426
|
+
const supabaseResult = await uploadToSupabase(env, tempPath, file, debug);
|
|
427
|
+
let lastError = supabaseResult.error;
|
|
428
|
+
// Upload to Backblaze
|
|
429
|
+
const backblazeResult = await handleBackblazeUpload({
|
|
430
|
+
apiKey,
|
|
431
|
+
apiUrl,
|
|
432
|
+
b2: b2,
|
|
433
|
+
debug,
|
|
434
|
+
file,
|
|
435
|
+
filePath,
|
|
436
|
+
finalPath,
|
|
437
|
+
supabaseSuccess: supabaseResult.success,
|
|
438
|
+
});
|
|
439
|
+
// Update lastError if Supabase also failed
|
|
440
|
+
if (!supabaseResult.success && backblazeResult.error) {
|
|
441
|
+
lastError = backblazeResult.error;
|
|
284
442
|
}
|
|
443
|
+
// Validate results
|
|
444
|
+
validateUploadResults(supabaseResult.success, backblazeResult.success, lastError, b2, debug);
|
|
445
|
+
// Log upload summary
|
|
285
446
|
if (debug) {
|
|
447
|
+
const hasWarning = !supabaseResult.success && backblazeResult.success;
|
|
448
|
+
console.log(`[DEBUG] Upload summary - Supabase: ${supabaseResult.success ? '✓' : '✗'}, Backblaze: ${backblazeResult.success ? '✓' : '✗'}`);
|
|
286
449
|
console.log('[DEBUG] Finalizing upload...');
|
|
287
450
|
console.log(`[DEBUG] Target endpoint: ${apiUrl}/uploads/finaliseUpload`);
|
|
451
|
+
console.log(`[DEBUG] Moving from temp: ${tempPath}`);
|
|
452
|
+
console.log(`[DEBUG] Moving to final: ${finalPath}`);
|
|
453
|
+
console.log(`[DEBUG] Supabase upload status: ${supabaseResult.success ? 'SUCCESS' : 'FAILED'}`);
|
|
454
|
+
console.log(`[DEBUG] Backblaze upload status: ${backblazeResult.success ? 'SUCCESS' : 'FAILED'}`);
|
|
455
|
+
if (hasWarning)
|
|
456
|
+
console.log('[DEBUG] ⚠ Warning: File only exists in Backblaze (Supabase failed)');
|
|
288
457
|
}
|
|
458
|
+
// Finalize upload
|
|
289
459
|
const finalizeStartTime = Date.now();
|
|
290
|
-
await api_gateway_1.ApiGateway.finaliseUpload(
|
|
460
|
+
await api_gateway_1.ApiGateway.finaliseUpload({
|
|
461
|
+
apiKey,
|
|
462
|
+
backblazeSuccess: backblazeResult.success,
|
|
463
|
+
baseUrl: apiUrl,
|
|
464
|
+
id,
|
|
465
|
+
metadata,
|
|
466
|
+
path: tempPath,
|
|
467
|
+
sha: sha,
|
|
468
|
+
supabaseSuccess: supabaseResult.success,
|
|
469
|
+
});
|
|
291
470
|
if (debug) {
|
|
292
471
|
console.log(`[DEBUG] Upload finalization completed in ${Date.now() - finalizeStartTime}ms`);
|
|
293
472
|
console.log(`[DEBUG] Total upload time: ${Date.now() - startTime}ms`);
|
|
294
473
|
}
|
|
295
474
|
return id;
|
|
296
475
|
}
|
|
476
|
+
/**
|
|
477
|
+
* Upload file to Backblaze using signed URL (simple upload for files < 100MB)
|
|
478
|
+
* @param uploadUrl - Backblaze upload URL
|
|
479
|
+
* @param authorizationToken - Authorization token for the upload
|
|
480
|
+
* @param fileName - Name/path of the file
|
|
481
|
+
* @param file - File to upload
|
|
482
|
+
* @param debug - Whether debug logging is enabled
|
|
483
|
+
* @returns Promise that resolves when upload completes or fails gracefully
|
|
484
|
+
*/
|
|
485
|
+
async function uploadToBackblaze(uploadUrl, authorizationToken, fileName, file, debug) {
|
|
486
|
+
try {
|
|
487
|
+
const arrayBuffer = await file.arrayBuffer();
|
|
488
|
+
// Calculate SHA1 hash for Backblaze (B2 requires SHA1, not SHA256)
|
|
489
|
+
const sha1 = (0, node_crypto_1.createHash)('sha1');
|
|
490
|
+
sha1.update(Buffer.from(arrayBuffer));
|
|
491
|
+
const sha1Hex = sha1.digest('hex');
|
|
492
|
+
// Detect if this is an S3 pre-signed URL (authorization token is empty)
|
|
493
|
+
const isS3PreSignedUrl = !authorizationToken || authorizationToken === '';
|
|
494
|
+
if (debug) {
|
|
495
|
+
console.log(`[DEBUG] Uploading to Backblaze URL: ${uploadUrl}`);
|
|
496
|
+
console.log(`[DEBUG] Upload method: ${isS3PreSignedUrl ? 'S3 pre-signed URL (PUT)' : 'B2 native API (POST)'}`);
|
|
497
|
+
console.log(`[DEBUG] File name: ${fileName}`);
|
|
498
|
+
console.log(`[DEBUG] File SHA1: ${sha1Hex}`);
|
|
499
|
+
}
|
|
500
|
+
// Build headers based on upload method
|
|
501
|
+
const headers = {
|
|
502
|
+
'Content-Length': file.size.toString(),
|
|
503
|
+
'Content-Type': file.type || 'application/octet-stream',
|
|
504
|
+
'X-Bz-Content-Sha1': sha1Hex,
|
|
505
|
+
};
|
|
506
|
+
// S3 pre-signed URLs have auth embedded in URL, native B2 uses Authorization header
|
|
507
|
+
if (!isS3PreSignedUrl) {
|
|
508
|
+
headers.Authorization = authorizationToken;
|
|
509
|
+
headers['X-Bz-File-Name'] = encodeURIComponent(fileName);
|
|
510
|
+
}
|
|
511
|
+
const response = await fetch(uploadUrl, {
|
|
512
|
+
body: arrayBuffer,
|
|
513
|
+
headers,
|
|
514
|
+
method: isS3PreSignedUrl ? 'PUT' : 'POST',
|
|
515
|
+
});
|
|
516
|
+
if (!response.ok) {
|
|
517
|
+
const errorText = await response.text();
|
|
518
|
+
if (debug) {
|
|
519
|
+
console.error(`[DEBUG] Backblaze upload failed with status ${response.status}: ${errorText}`);
|
|
520
|
+
}
|
|
521
|
+
// Don't throw - we don't want Backblaze failures to block the primary upload
|
|
522
|
+
console.warn(`Warning: Backblaze upload failed with status ${response.status}`);
|
|
523
|
+
return false;
|
|
524
|
+
}
|
|
525
|
+
if (debug) {
|
|
526
|
+
console.log('[DEBUG] Backblaze upload successful');
|
|
527
|
+
}
|
|
528
|
+
return true;
|
|
529
|
+
}
|
|
530
|
+
catch (error) {
|
|
531
|
+
if (debug) {
|
|
532
|
+
console.error('[DEBUG] Backblaze upload exception:', error);
|
|
533
|
+
}
|
|
534
|
+
// Don't throw - we don't want Backblaze failures to block the primary upload
|
|
535
|
+
console.warn(`Warning: Backblaze upload failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Helper function to read a chunk from a file stream
|
|
541
|
+
* @param filePath - Path to the file
|
|
542
|
+
* @param start - Start byte position
|
|
543
|
+
* @param end - End byte position (exclusive)
|
|
544
|
+
* @returns Promise resolving to Buffer containing the chunk
|
|
545
|
+
*/
|
|
546
|
+
async function readFileChunk(filePath, start, end) {
|
|
547
|
+
return new Promise((resolve, reject) => {
|
|
548
|
+
const chunks = [];
|
|
549
|
+
const stream = (0, node_fs_1.createReadStream)(filePath, { start, end: end - 1 }); // end is inclusive in createReadStream
|
|
550
|
+
stream.on('data', (chunk) => {
|
|
551
|
+
chunks.push(chunk);
|
|
552
|
+
});
|
|
553
|
+
stream.on('end', () => {
|
|
554
|
+
resolve(Buffer.concat(chunks));
|
|
555
|
+
});
|
|
556
|
+
stream.on('error', (error) => {
|
|
557
|
+
reject(error);
|
|
558
|
+
});
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Helper function to read a chunk from a File/Blob object
|
|
563
|
+
* @param file - File or Blob object
|
|
564
|
+
* @param start - Start byte position
|
|
565
|
+
* @param end - End byte position (exclusive)
|
|
566
|
+
* @returns Promise resolving to Buffer containing the chunk
|
|
567
|
+
*/
|
|
568
|
+
async function readFileObjectChunk(file, start, end) {
|
|
569
|
+
const slice = file.slice(start, end);
|
|
570
|
+
const arrayBuffer = await slice.arrayBuffer();
|
|
571
|
+
return Buffer.from(arrayBuffer);
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Upload large file to Backblaze using multi-part upload with streaming (for files >= 5MB)
|
|
575
|
+
* Uses file streaming to avoid loading entire file into memory, preventing OOM errors on large files
|
|
576
|
+
* @param config - Configuration object for the large file upload
|
|
577
|
+
* @returns Promise that resolves when upload completes or fails gracefully
|
|
578
|
+
*/
|
|
579
|
+
async function uploadLargeFileToBackblaze(config) {
|
|
580
|
+
const { apiUrl, apiKey, fileId, uploadPartUrls, filePath, fileSize, debug, fileObject } = config;
|
|
581
|
+
try {
|
|
582
|
+
const partSha1Array = [];
|
|
583
|
+
// Calculate part size (divide file evenly across all parts)
|
|
584
|
+
const partSize = Math.ceil(fileSize / uploadPartUrls.length);
|
|
585
|
+
if (debug) {
|
|
586
|
+
console.log(`[DEBUG] Uploading large file in ${uploadPartUrls.length} parts (streaming mode)`);
|
|
587
|
+
console.log(`[DEBUG] Part size: ${(partSize / 1024 / 1024).toFixed(2)} MB`);
|
|
588
|
+
console.log(`[DEBUG] Reading from: ${fileObject ? 'in-memory File object' : filePath}`);
|
|
589
|
+
}
|
|
590
|
+
// Upload each part using streaming to avoid loading entire file into memory
|
|
591
|
+
for (let i = 0; i < uploadPartUrls.length; i++) {
|
|
592
|
+
const partNumber = i + 1;
|
|
593
|
+
const start = i * partSize;
|
|
594
|
+
const end = Math.min(start + partSize, fileSize);
|
|
595
|
+
const partLength = end - start;
|
|
596
|
+
if (debug) {
|
|
597
|
+
console.log(`[DEBUG] Reading part ${partNumber}/${uploadPartUrls.length} bytes ${start}-${end}`);
|
|
598
|
+
}
|
|
599
|
+
// Read part from File object (for .app bundles) or from disk
|
|
600
|
+
const partBuffer = fileObject
|
|
601
|
+
? await readFileObjectChunk(fileObject, start, end)
|
|
602
|
+
: await readFileChunk(filePath, start, end);
|
|
603
|
+
// Calculate SHA1 for this part
|
|
604
|
+
const sha1 = (0, node_crypto_1.createHash)('sha1');
|
|
605
|
+
sha1.update(partBuffer);
|
|
606
|
+
const sha1Hex = sha1.digest('hex');
|
|
607
|
+
partSha1Array.push(sha1Hex);
|
|
608
|
+
if (debug) {
|
|
609
|
+
console.log(`[DEBUG] Uploading part ${partNumber}/${uploadPartUrls.length} (${(partLength / 1024 / 1024).toFixed(2)} MB, SHA1: ${sha1Hex})`);
|
|
610
|
+
}
|
|
611
|
+
const response = await fetch(uploadPartUrls[i].uploadUrl, {
|
|
612
|
+
body: new Uint8Array(partBuffer),
|
|
613
|
+
headers: {
|
|
614
|
+
Authorization: uploadPartUrls[i].authorizationToken,
|
|
615
|
+
'Content-Length': partLength.toString(),
|
|
616
|
+
'X-Bz-Content-Sha1': sha1Hex,
|
|
617
|
+
'X-Bz-Part-Number': partNumber.toString(),
|
|
618
|
+
},
|
|
619
|
+
method: 'POST',
|
|
620
|
+
});
|
|
621
|
+
if (!response.ok) {
|
|
622
|
+
const errorText = await response.text();
|
|
623
|
+
if (debug) {
|
|
624
|
+
console.error(`[DEBUG] Part ${partNumber} upload failed with status ${response.status}: ${errorText}`);
|
|
625
|
+
}
|
|
626
|
+
throw new Error(`Part ${partNumber} upload failed with status ${response.status}`);
|
|
627
|
+
}
|
|
628
|
+
if (debug) {
|
|
629
|
+
console.log(`[DEBUG] Part ${partNumber}/${uploadPartUrls.length} uploaded successfully`);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
// Finish the large file upload
|
|
633
|
+
if (debug) {
|
|
634
|
+
console.log('[DEBUG] Finishing large file upload...');
|
|
635
|
+
}
|
|
636
|
+
await api_gateway_1.ApiGateway.finishLargeFile(apiUrl, apiKey, fileId, partSha1Array);
|
|
637
|
+
if (debug) {
|
|
638
|
+
console.log('[DEBUG] Large file upload completed successfully');
|
|
639
|
+
}
|
|
640
|
+
return true;
|
|
641
|
+
}
|
|
642
|
+
catch (error) {
|
|
643
|
+
if (debug) {
|
|
644
|
+
console.error('[DEBUG] Large file upload exception:', error);
|
|
645
|
+
}
|
|
646
|
+
// Don't throw - we don't want Backblaze failures to block the primary upload
|
|
647
|
+
console.warn(`Warning: Backblaze large file upload failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
648
|
+
return false;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
297
651
|
async function getFileHashFromFile(file) {
|
|
298
652
|
return new Promise((resolve, reject) => {
|
|
299
653
|
const hash = (0, node_crypto_1.createHash)('sha256');
|
|
@@ -34,10 +34,10 @@ class DeviceValidationService {
|
|
|
34
34
|
throw new Error(`${androidDeviceID} ${googlePlay ? '(Play Store) ' : ''}only supports these Android API levels: ${supportedAndroidVersions.join(', ')}`);
|
|
35
35
|
}
|
|
36
36
|
if (debug && logger) {
|
|
37
|
-
logger(`DEBUG
|
|
38
|
-
logger(`DEBUG
|
|
39
|
-
logger(`DEBUG
|
|
40
|
-
logger(`DEBUG
|
|
37
|
+
logger(`[DEBUG] Android device: ${androidDeviceID}`);
|
|
38
|
+
logger(`[DEBUG] Android API level: ${version}`);
|
|
39
|
+
logger(`[DEBUG] Google Play enabled: ${googlePlay}`);
|
|
40
|
+
logger(`[DEBUG] Supported Android versions: ${supportedAndroidVersions.join(', ')}`);
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
/**
|
|
@@ -65,9 +65,9 @@ class DeviceValidationService {
|
|
|
65
65
|
throw new Error(`${iOSDeviceID} only supports these iOS versions: ${supportediOSVersions.join(', ')}`);
|
|
66
66
|
}
|
|
67
67
|
if (debug && logger) {
|
|
68
|
-
logger(`DEBUG
|
|
69
|
-
logger(`DEBUG
|
|
70
|
-
logger(`DEBUG
|
|
68
|
+
logger(`[DEBUG] iOS device: ${iOSDeviceID}`);
|
|
69
|
+
logger(`[DEBUG] iOS version: ${version}`);
|
|
70
|
+
logger(`[DEBUG] Supported iOS versions: ${supportediOSVersions.join(', ')}`);
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
}
|
|
@@ -18,8 +18,8 @@ class MoropoService {
|
|
|
18
18
|
*/
|
|
19
19
|
async downloadAndExtract(options) {
|
|
20
20
|
const { apiKey, branchName = 'main', debug = false, quiet = false, json = false, logger } = options;
|
|
21
|
-
this.logDebug(debug, logger, 'DEBUG
|
|
22
|
-
this.logDebug(debug, logger, `DEBUG
|
|
21
|
+
this.logDebug(debug, logger, '[DEBUG] Moropo v1 API key detected, downloading tests from Moropo API');
|
|
22
|
+
this.logDebug(debug, logger, `[DEBUG] Using branch name: ${branchName}`);
|
|
23
23
|
try {
|
|
24
24
|
if (!quiet && !json) {
|
|
25
25
|
core_1.ux.action.start('Downloading Moropo tests', 'Initializing', {
|
|
@@ -37,7 +37,7 @@ class MoropoService {
|
|
|
37
37
|
throw new Error(`Failed to download Moropo tests: ${response.statusText}`);
|
|
38
38
|
}
|
|
39
39
|
const moropoDir = path.join(os.tmpdir(), `moropo-tests-${Date.now()}`);
|
|
40
|
-
this.logDebug(debug, logger, `DEBUG
|
|
40
|
+
this.logDebug(debug, logger, `[DEBUG] Extracting Moropo tests to: ${moropoDir}`);
|
|
41
41
|
// Create moropo directory if it doesn't exist
|
|
42
42
|
if (!fs.existsSync(moropoDir)) {
|
|
43
43
|
fs.mkdirSync(moropoDir, { recursive: true });
|
|
@@ -51,17 +51,17 @@ class MoropoService {
|
|
|
51
51
|
if (!quiet && !json) {
|
|
52
52
|
core_1.ux.action.stop('completed');
|
|
53
53
|
}
|
|
54
|
-
this.logDebug(debug, logger, 'DEBUG
|
|
54
|
+
this.logDebug(debug, logger, '[DEBUG] Successfully extracted Moropo tests');
|
|
55
55
|
// Create config.yaml file
|
|
56
56
|
this.createConfigFile(moropoDir);
|
|
57
|
-
this.logDebug(debug, logger, 'DEBUG
|
|
57
|
+
this.logDebug(debug, logger, '[DEBUG] Created config.yaml file');
|
|
58
58
|
return moropoDir;
|
|
59
59
|
}
|
|
60
60
|
catch (error) {
|
|
61
61
|
if (!quiet && !json) {
|
|
62
62
|
core_1.ux.action.stop('failed');
|
|
63
63
|
}
|
|
64
|
-
this.logDebug(debug, logger, `DEBUG
|
|
64
|
+
this.logDebug(debug, logger, `[DEBUG] Error downloading/extracting Moropo tests: ${error}`);
|
|
65
65
|
throw new Error(`Failed to download/extract Moropo tests: ${error}`);
|
|
66
66
|
}
|
|
67
67
|
}
|
|
@@ -16,7 +16,7 @@ class ReportDownloadService {
|
|
|
16
16
|
const { apiUrl, apiKey, uploadId, downloadType, artifactsPath = './artifacts.zip', debug = false, logger, warnLogger, } = options;
|
|
17
17
|
try {
|
|
18
18
|
if (debug && logger) {
|
|
19
|
-
logger(`DEBUG
|
|
19
|
+
logger(`[DEBUG] Downloading artifacts: ${downloadType}`);
|
|
20
20
|
}
|
|
21
21
|
await api_gateway_1.ApiGateway.downloadArtifactsZip(apiUrl, apiKey, uploadId, downloadType, artifactsPath);
|
|
22
22
|
if (logger) {
|
|
@@ -26,7 +26,7 @@ class ReportDownloadService {
|
|
|
26
26
|
}
|
|
27
27
|
catch (error) {
|
|
28
28
|
if (debug && logger) {
|
|
29
|
-
logger(`DEBUG
|
|
29
|
+
logger(`[DEBUG] Error downloading artifacts: ${error}`);
|
|
30
30
|
}
|
|
31
31
|
if (warnLogger) {
|
|
32
32
|
warnLogger('Failed to download artifacts');
|
|
@@ -83,7 +83,7 @@ class ReportDownloadService {
|
|
|
83
83
|
const { apiUrl, apiKey, uploadId, debug = false, logger, warnLogger } = options;
|
|
84
84
|
try {
|
|
85
85
|
if (debug && logger) {
|
|
86
|
-
logger(`DEBUG
|
|
86
|
+
logger(`[DEBUG] Downloading ${type.toUpperCase()} report`);
|
|
87
87
|
}
|
|
88
88
|
await api_gateway_1.ApiGateway.downloadReportGeneric(apiUrl, apiKey, uploadId, type, filePath);
|
|
89
89
|
if (logger) {
|
|
@@ -92,7 +92,7 @@ class ReportDownloadService {
|
|
|
92
92
|
}
|
|
93
93
|
catch (error) {
|
|
94
94
|
if (debug && logger) {
|
|
95
|
-
logger(`DEBUG
|
|
95
|
+
logger(`[DEBUG] Error downloading ${type.toUpperCase()} report: ${error}`);
|
|
96
96
|
}
|
|
97
97
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
98
98
|
if (warnLogger) {
|
|
@@ -37,7 +37,7 @@ class ResultsPollingService {
|
|
|
37
37
|
let sequentialPollFailures = 0;
|
|
38
38
|
let previousSummary = '';
|
|
39
39
|
if (debug && logger) {
|
|
40
|
-
logger(`DEBUG
|
|
40
|
+
logger(`[DEBUG] Starting polling loop for results`);
|
|
41
41
|
}
|
|
42
42
|
// Poll in a loop until all tests complete
|
|
43
43
|
// eslint-disable-next-line no-constant-condition
|
|
@@ -186,16 +186,16 @@ class ResultsPollingService {
|
|
|
186
186
|
*/
|
|
187
187
|
async fetchAndLogResults(apiUrl, apiKey, uploadId, debug, logger) {
|
|
188
188
|
if (debug && logger) {
|
|
189
|
-
logger(`DEBUG
|
|
189
|
+
logger(`[DEBUG] Polling for results: ${uploadId}`);
|
|
190
190
|
}
|
|
191
191
|
const { results: updatedResults } = await api_gateway_1.ApiGateway.getResultsForUpload(apiUrl, apiKey, uploadId);
|
|
192
192
|
if (!updatedResults) {
|
|
193
193
|
throw new Error('no results');
|
|
194
194
|
}
|
|
195
195
|
if (debug && logger) {
|
|
196
|
-
logger(`DEBUG
|
|
196
|
+
logger(`[DEBUG] Poll received ${updatedResults.length} results`);
|
|
197
197
|
for (const result of updatedResults) {
|
|
198
|
-
logger(`DEBUG
|
|
198
|
+
logger(`[DEBUG] Result status: ${result.test_file_name} - ${result.status}`);
|
|
199
199
|
}
|
|
200
200
|
}
|
|
201
201
|
return updatedResults;
|
|
@@ -216,39 +216,39 @@ class ResultsPollingService {
|
|
|
216
216
|
async handleCompletedTests(updatedResults, options) {
|
|
217
217
|
const { uploadId, consoleUrl, json, debug, logger } = options;
|
|
218
218
|
if (debug && logger) {
|
|
219
|
-
logger(`DEBUG
|
|
219
|
+
logger(`[DEBUG] All tests completed, stopping poll`);
|
|
220
220
|
}
|
|
221
221
|
this.displayFinalResults(updatedResults, consoleUrl, json, logger);
|
|
222
222
|
const output = this.buildPollingResult(updatedResults, uploadId, consoleUrl);
|
|
223
223
|
if (output.status === 'FAILED') {
|
|
224
224
|
if (debug && logger) {
|
|
225
|
-
logger(`DEBUG
|
|
225
|
+
logger(`[DEBUG] Some tests failed, returning failed status`);
|
|
226
226
|
}
|
|
227
227
|
throw new RunFailedError(output);
|
|
228
228
|
}
|
|
229
229
|
if (debug && logger) {
|
|
230
|
-
logger(`DEBUG
|
|
230
|
+
logger(`[DEBUG] All tests passed, returning success status`);
|
|
231
231
|
}
|
|
232
232
|
return output;
|
|
233
233
|
}
|
|
234
234
|
async handlePollingError(error, sequentialPollFailures, debug, logger) {
|
|
235
235
|
if (debug && logger) {
|
|
236
|
-
logger(`DEBUG
|
|
237
|
-
logger(`DEBUG
|
|
236
|
+
logger(`[DEBUG] Error polling for results: ${error}`);
|
|
237
|
+
logger(`[DEBUG] Sequential poll failures: ${sequentialPollFailures}`);
|
|
238
238
|
}
|
|
239
239
|
if (sequentialPollFailures > this.MAX_SEQUENTIAL_FAILURES) {
|
|
240
240
|
if (debug && logger) {
|
|
241
|
-
logger('DEBUG
|
|
241
|
+
logger('[DEBUG] Checking internet connectivity...');
|
|
242
242
|
}
|
|
243
243
|
const connectivityCheck = await (0, connectivity_1.checkInternetConnectivity)();
|
|
244
244
|
if (debug && logger) {
|
|
245
|
-
logger(`DEBUG
|
|
245
|
+
logger(`[DEBUG] ${connectivityCheck.message}`);
|
|
246
246
|
for (const result of connectivityCheck.endpointResults) {
|
|
247
247
|
if (result.success) {
|
|
248
|
-
logger(`DEBUG
|
|
248
|
+
logger(`[DEBUG] ✓ ${result.endpoint} - ${result.statusCode} (${result.latencyMs}ms)`);
|
|
249
249
|
}
|
|
250
250
|
else {
|
|
251
|
-
logger(`DEBUG
|
|
251
|
+
logger(`[DEBUG] ✗ ${result.endpoint} - ${result.error} (${result.latencyMs}ms)`);
|
|
252
252
|
}
|
|
253
253
|
}
|
|
254
254
|
}
|