@devicecloud.dev/dcd 4.1.6 → 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.
|
@@ -12,13 +12,29 @@ export declare const ApiGateway: {
|
|
|
12
12
|
exists: boolean;
|
|
13
13
|
}>;
|
|
14
14
|
downloadArtifactsZip(baseUrl: string, apiKey: string, uploadId: string, results: "ALL" | "FAILED", artifactsPath?: string): Promise<void>;
|
|
15
|
-
finaliseUpload(baseUrl: string, apiKey: string, id: string, metadata: TAppMetadata, path: string, sha: string): Promise<Record<string, never>>;
|
|
16
|
-
getBinaryUploadUrl(baseUrl: string, apiKey: string, platform: "android" | "ios"): Promise<{
|
|
15
|
+
finaliseUpload(baseUrl: string, apiKey: string, id: string, metadata: TAppMetadata, path: string, sha: string, supabaseSuccess: boolean, backblazeSuccess: boolean): Promise<Record<string, never>>;
|
|
16
|
+
getBinaryUploadUrl(baseUrl: string, apiKey: string, platform: "android" | "ios", fileSize: number): Promise<{
|
|
17
17
|
message: string;
|
|
18
18
|
path: string;
|
|
19
19
|
token: string;
|
|
20
20
|
id: string;
|
|
21
|
+
b2?: {
|
|
22
|
+
strategy: "simple" | "large";
|
|
23
|
+
simple?: {
|
|
24
|
+
uploadUrl: string;
|
|
25
|
+
authorizationToken: string;
|
|
26
|
+
};
|
|
27
|
+
large?: {
|
|
28
|
+
fileId: string;
|
|
29
|
+
fileName: string;
|
|
30
|
+
uploadPartUrls: Array<{
|
|
31
|
+
uploadUrl: string;
|
|
32
|
+
authorizationToken: string;
|
|
33
|
+
}>;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
21
36
|
}>;
|
|
37
|
+
finishLargeFile(baseUrl: string, apiKey: string, fileId: string, partSha1Array: string[]): Promise<any>;
|
|
22
38
|
getResultsForUpload(baseUrl: string, apiKey: string, uploadId: string): Promise<{
|
|
23
39
|
statusCode?: number;
|
|
24
40
|
results?: import("../types/generated/schema.types").components["schemas"]["TResultResponse"][];
|
|
@@ -99,9 +99,9 @@ exports.ApiGateway = {
|
|
|
99
99
|
await finished(Readable.fromWeb(res.body).pipe(fileStream));
|
|
100
100
|
},
|
|
101
101
|
// eslint-disable-next-line max-params
|
|
102
|
-
async finaliseUpload(baseUrl, apiKey, id, metadata, path, sha) {
|
|
102
|
+
async finaliseUpload(baseUrl, apiKey, id, metadata, path, sha, supabaseSuccess, backblazeSuccess) {
|
|
103
103
|
const res = await fetch(`${baseUrl}/uploads/finaliseUpload`, {
|
|
104
|
-
body: JSON.stringify({ id, metadata, path, sha }),
|
|
104
|
+
body: JSON.stringify({ id, metadata, path, sha, supabaseSuccess, backblazeSuccess }),
|
|
105
105
|
headers: {
|
|
106
106
|
'content-type': 'application/json',
|
|
107
107
|
'x-app-api-key': apiKey,
|
|
@@ -113,9 +113,9 @@ exports.ApiGateway = {
|
|
|
113
113
|
}
|
|
114
114
|
return res.json();
|
|
115
115
|
},
|
|
116
|
-
async getBinaryUploadUrl(baseUrl, apiKey, platform) {
|
|
116
|
+
async getBinaryUploadUrl(baseUrl, apiKey, platform, fileSize) {
|
|
117
117
|
const res = await fetch(`${baseUrl}/uploads/getBinaryUploadUrl`, {
|
|
118
|
-
body: JSON.stringify({ platform }),
|
|
118
|
+
body: JSON.stringify({ platform, fileSize }),
|
|
119
119
|
headers: {
|
|
120
120
|
'content-type': 'application/json',
|
|
121
121
|
'x-app-api-key': apiKey,
|
|
@@ -127,6 +127,20 @@ exports.ApiGateway = {
|
|
|
127
127
|
}
|
|
128
128
|
return res.json();
|
|
129
129
|
},
|
|
130
|
+
async finishLargeFile(baseUrl, apiKey, fileId, partSha1Array) {
|
|
131
|
+
const res = await fetch(`${baseUrl}/uploads/finishLargeFile`, {
|
|
132
|
+
body: JSON.stringify({ fileId, partSha1Array }),
|
|
133
|
+
headers: {
|
|
134
|
+
'content-type': 'application/json',
|
|
135
|
+
'x-app-api-key': apiKey,
|
|
136
|
+
},
|
|
137
|
+
method: 'POST',
|
|
138
|
+
});
|
|
139
|
+
if (!res.ok) {
|
|
140
|
+
await this.handleApiError(res, 'Failed to finish large file');
|
|
141
|
+
}
|
|
142
|
+
return res.json();
|
|
143
|
+
},
|
|
130
144
|
async getResultsForUpload(baseUrl, apiKey, uploadId) {
|
|
131
145
|
// TODO: merge with getUploadStatus
|
|
132
146
|
const res = await fetch(`${baseUrl}/results/${uploadId}`, {
|
|
@@ -48,26 +48,43 @@ class SupabaseGateway {
|
|
|
48
48
|
}
|
|
49
49
|
catch (error) {
|
|
50
50
|
if (debug) {
|
|
51
|
-
console.error(`[DEBUG]
|
|
51
|
+
console.error(`[DEBUG] === SUPABASE UPLOAD EXCEPTION ===`);
|
|
52
|
+
console.error(`[DEBUG] Exception caught:`, error);
|
|
52
53
|
console.error(`[DEBUG] Error type: ${error instanceof Error ? error.name : typeof error}`);
|
|
53
54
|
console.error(`[DEBUG] Error message: ${error instanceof Error ? error.message : String(error)}`);
|
|
55
|
+
if (error instanceof Error && error.stack) {
|
|
56
|
+
console.error(`[DEBUG] Error stack:\n${error.stack}`);
|
|
57
|
+
}
|
|
58
|
+
// Log additional context
|
|
59
|
+
console.error(`[DEBUG] Upload context:`);
|
|
60
|
+
console.error(`[DEBUG] - Environment: ${env}`);
|
|
61
|
+
console.error(`[DEBUG] - Supabase URL: ${SUPABASE_URL}`);
|
|
62
|
+
console.error(`[DEBUG] - Upload path: ${path}`);
|
|
63
|
+
console.error(`[DEBUG] - File name: ${file.name}`);
|
|
64
|
+
console.error(`[DEBUG] - File size: ${file.size} bytes`);
|
|
54
65
|
// Check for common network errors
|
|
55
66
|
if (error instanceof Error) {
|
|
56
|
-
|
|
67
|
+
const errorMsg = error.message.toLowerCase();
|
|
68
|
+
if (errorMsg.includes('econnrefused') || errorMsg.includes('connection refused')) {
|
|
57
69
|
console.error(`[DEBUG] Network error: Connection refused - check internet connectivity`);
|
|
58
70
|
}
|
|
59
|
-
else if (
|
|
71
|
+
else if (errorMsg.includes('etimedout') || errorMsg.includes('timeout')) {
|
|
60
72
|
console.error(`[DEBUG] Network error: Connection timeout - check internet connectivity or try again`);
|
|
61
73
|
}
|
|
62
|
-
else if (
|
|
74
|
+
else if (errorMsg.includes('enotfound') || errorMsg.includes('not found')) {
|
|
63
75
|
console.error(`[DEBUG] Network error: DNS lookup failed - check internet connectivity`);
|
|
64
76
|
}
|
|
65
|
-
else if (
|
|
66
|
-
console.error(`[DEBUG] Network error: Connection reset - network unstable or interrupted`);
|
|
77
|
+
else if (errorMsg.includes('econnreset') || errorMsg.includes('connection reset') || errorMsg.includes('connection lost')) {
|
|
78
|
+
console.error(`[DEBUG] Network error: Connection reset/lost - network unstable or interrupted`);
|
|
79
|
+
}
|
|
80
|
+
else if (errorMsg.includes('network')) {
|
|
81
|
+
console.error(`[DEBUG] Network-related error detected - check internet connectivity`);
|
|
67
82
|
}
|
|
68
83
|
}
|
|
69
84
|
}
|
|
70
|
-
throw
|
|
85
|
+
// Re-throw with additional context
|
|
86
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
87
|
+
throw new Error(`Supabase upload error: ${errorMsg}`);
|
|
71
88
|
}
|
|
72
89
|
}
|
|
73
90
|
}
|
package/dist/methods.js
CHANGED
|
@@ -249,11 +249,15 @@ async function performUpload(filePath, apiUrl, apiKey, file, sha, debug, startTi
|
|
|
249
249
|
console.log(`[DEBUG] Platform: ${filePath?.endsWith('.apk') ? 'android' : 'ios'}`);
|
|
250
250
|
}
|
|
251
251
|
const urlRequestStartTime = Date.now();
|
|
252
|
-
const { id, message, path, token } = await api_gateway_1.ApiGateway.getBinaryUploadUrl(apiUrl, apiKey, filePath?.endsWith('.apk') ? 'android' : 'ios');
|
|
252
|
+
const { id, message, path, token, b2 } = await api_gateway_1.ApiGateway.getBinaryUploadUrl(apiUrl, apiKey, filePath?.endsWith('.apk') ? 'android' : 'ios', file.size);
|
|
253
253
|
if (debug) {
|
|
254
254
|
console.log(`[DEBUG] Upload URL request completed in ${Date.now() - urlRequestStartTime}ms`);
|
|
255
255
|
console.log(`[DEBUG] Upload ID: ${id}`);
|
|
256
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}`);
|
|
260
|
+
}
|
|
257
261
|
}
|
|
258
262
|
if (!path)
|
|
259
263
|
throw new Error(message);
|
|
@@ -270,30 +274,308 @@ async function performUpload(filePath, apiUrl, apiKey, file, sha, debug, startTi
|
|
|
270
274
|
console.log(`[DEBUG] Metadata extracted: ${JSON.stringify(metadata)}`);
|
|
271
275
|
}
|
|
272
276
|
const env = apiUrl === 'https://api.devicecloud.dev' ? 'prod' : 'dev';
|
|
277
|
+
let supabaseSuccess = false;
|
|
278
|
+
let backblazeSuccess = false;
|
|
279
|
+
let lastError = null;
|
|
280
|
+
// Try Supabase upload first
|
|
273
281
|
if (debug) {
|
|
274
282
|
console.log(`[DEBUG] Uploading to Supabase storage (${env})...`);
|
|
275
283
|
console.log(`[DEBUG] File size: ${(file.size / 1024 / 1024).toFixed(2)} MB`);
|
|
276
284
|
}
|
|
277
|
-
|
|
278
|
-
|
|
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
|
+
}
|
|
279
377
|
if (debug) {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
+
}
|
|
284
382
|
}
|
|
285
383
|
if (debug) {
|
|
286
384
|
console.log('[DEBUG] Finalizing upload...');
|
|
287
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'}`);
|
|
288
388
|
}
|
|
289
389
|
const finalizeStartTime = Date.now();
|
|
290
|
-
await api_gateway_1.ApiGateway.finaliseUpload(apiUrl, apiKey, id, metadata, path, sha);
|
|
390
|
+
await api_gateway_1.ApiGateway.finaliseUpload(apiUrl, apiKey, id, metadata, path, sha, supabaseSuccess, backblazeSuccess);
|
|
291
391
|
if (debug) {
|
|
292
392
|
console.log(`[DEBUG] Upload finalization completed in ${Date.now() - finalizeStartTime}ms`);
|
|
293
393
|
console.log(`[DEBUG] Total upload time: ${Date.now() - startTime}ms`);
|
|
294
394
|
}
|
|
295
395
|
return id;
|
|
296
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
|
+
}
|
|
297
579
|
async function getFileHashFromFile(file) {
|
|
298
580
|
return new Promise((resolve, reject) => {
|
|
299
581
|
const hash = (0, node_crypto_1.createHash)('sha256');
|
|
@@ -443,6 +443,21 @@ export interface components {
|
|
|
443
443
|
path: string;
|
|
444
444
|
token: string;
|
|
445
445
|
id: string;
|
|
446
|
+
b2?: {
|
|
447
|
+
strategy: 'simple' | 'large';
|
|
448
|
+
simple?: {
|
|
449
|
+
uploadUrl: string;
|
|
450
|
+
authorizationToken: string;
|
|
451
|
+
};
|
|
452
|
+
large?: {
|
|
453
|
+
fileId: string;
|
|
454
|
+
fileName: string;
|
|
455
|
+
uploadPartUrls: Array<{
|
|
456
|
+
uploadUrl: string;
|
|
457
|
+
authorizationToken: string;
|
|
458
|
+
}>;
|
|
459
|
+
};
|
|
460
|
+
};
|
|
446
461
|
};
|
|
447
462
|
ICheckForExistingUploadArgs: {
|
|
448
463
|
sha: string;
|
package/oclif.manifest.json
CHANGED