@devicecloud.dev/dcd 4.4.9 → 5.0.0-beta.1
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 +75 -2
- package/dist/commands/artifacts.d.ts +47 -18
- package/dist/commands/artifacts.js +69 -64
- package/dist/commands/cloud.d.ts +228 -88
- package/dist/commands/cloud.js +430 -342
- package/dist/commands/list.d.ts +39 -38
- package/dist/commands/list.js +124 -131
- package/dist/commands/live.d.ts +2 -0
- package/dist/commands/live.js +520 -0
- package/dist/commands/login.d.ts +17 -0
- package/dist/commands/login.js +252 -0
- package/dist/commands/logout.d.ts +2 -0
- package/dist/commands/logout.js +30 -0
- package/dist/commands/status.d.ts +23 -42
- package/dist/commands/status.js +170 -179
- package/dist/commands/switch-org.d.ts +12 -0
- package/dist/commands/switch-org.js +76 -0
- package/dist/commands/upgrade.d.ts +2 -0
- package/dist/commands/upgrade.js +120 -0
- package/dist/commands/upload.d.ts +33 -18
- package/dist/commands/upload.js +72 -78
- package/dist/commands/whoami.d.ts +2 -0
- package/dist/commands/whoami.js +31 -0
- package/dist/config/environments.d.ts +31 -0
- package/dist/config/environments.js +52 -0
- package/dist/config/flags/api.flags.d.ts +10 -2
- package/dist/config/flags/api.flags.js +13 -14
- package/dist/config/flags/binary.flags.d.ts +17 -4
- package/dist/config/flags/binary.flags.js +14 -18
- package/dist/config/flags/device.flags.d.ts +49 -11
- package/dist/config/flags/device.flags.js +43 -38
- package/dist/config/flags/environment.flags.d.ts +27 -6
- package/dist/config/flags/environment.flags.js +24 -29
- package/dist/config/flags/execution.flags.d.ts +35 -8
- package/dist/config/flags/execution.flags.js +31 -41
- package/dist/config/flags/github.flags.d.ts +23 -5
- package/dist/config/flags/github.flags.js +19 -15
- package/dist/config/flags/output.flags.d.ts +57 -13
- package/dist/config/flags/output.flags.js +48 -47
- package/dist/constants.d.ts +218 -51
- package/dist/constants.js +17 -20
- package/dist/gateways/api-gateway.d.ts +72 -16
- package/dist/gateways/api-gateway.js +298 -104
- package/dist/gateways/cli-auth-gateway.d.ts +13 -0
- package/dist/gateways/cli-auth-gateway.js +54 -0
- package/dist/gateways/realtime-gateway.d.ts +32 -0
- package/dist/gateways/realtime-gateway.js +103 -0
- package/dist/gateways/supabase-gateway.d.ts +11 -11
- package/dist/gateways/supabase-gateway.js +20 -48
- package/dist/index.d.ts +2 -1
- package/dist/index.js +98 -4
- package/dist/mcp/context.d.ts +33 -0
- package/dist/mcp/context.js +33 -0
- package/dist/mcp/helpers.d.ts +16 -0
- package/dist/mcp/helpers.js +34 -0
- package/dist/mcp/index.d.ts +2 -0
- package/dist/mcp/index.js +24 -0
- package/dist/mcp/server.d.ts +7 -0
- package/dist/mcp/server.js +27 -0
- package/dist/mcp/tools/download-artifacts.d.ts +11 -0
- package/dist/mcp/tools/download-artifacts.js +84 -0
- package/dist/mcp/tools/get-status.d.ts +7 -0
- package/dist/mcp/tools/get-status.js +39 -0
- package/dist/mcp/tools/list-devices.d.ts +7 -0
- package/dist/mcp/tools/list-devices.js +27 -0
- package/dist/mcp/tools/list-runs.d.ts +3 -0
- package/dist/mcp/tools/list-runs.js +60 -0
- package/dist/mcp/tools/run-cloud-test.d.ts +14 -0
- package/dist/mcp/tools/run-cloud-test.js +233 -0
- package/dist/methods.d.ts +34 -5
- package/dist/methods.js +266 -215
- package/dist/services/device-validation.service.d.ts +9 -1
- package/dist/services/device-validation.service.js +56 -40
- package/dist/services/execution-plan.service.js +40 -31
- package/dist/services/execution-plan.utils.d.ts +3 -0
- package/dist/services/execution-plan.utils.js +25 -55
- package/dist/services/flow-paths.d.ts +17 -0
- package/dist/services/flow-paths.js +52 -0
- package/dist/services/metadata-extractor.service.d.ts +0 -2
- package/dist/services/metadata-extractor.service.js +75 -78
- package/dist/services/moropo.service.js +33 -34
- package/dist/services/report-download.service.d.ts +12 -1
- package/dist/services/report-download.service.js +34 -27
- package/dist/services/results-polling.service.d.ts +23 -9
- package/dist/services/results-polling.service.js +257 -123
- package/dist/services/telemetry.service.d.ts +49 -0
- package/dist/services/telemetry.service.js +252 -0
- package/dist/services/test-submission.service.d.ts +21 -4
- package/dist/services/test-submission.service.js +51 -33
- package/dist/services/version.service.d.ts +4 -3
- package/dist/services/version.service.js +28 -16
- package/dist/types/domain/auth.types.d.ts +20 -0
- package/dist/types/domain/auth.types.js +1 -0
- package/dist/types/domain/device.types.js +8 -11
- package/dist/types/domain/live.types.d.ts +76 -0
- package/dist/types/domain/live.types.js +3 -0
- package/dist/types/generated/schema.types.js +1 -2
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.js +2 -18
- package/dist/types.js +1 -2
- package/dist/utils/auth.d.ts +13 -0
- package/dist/utils/auth.js +141 -0
- package/dist/utils/ci.d.ts +12 -0
- package/dist/utils/ci.js +39 -0
- package/dist/utils/cli.d.ts +35 -0
- package/dist/utils/cli.js +118 -0
- package/dist/utils/compatibility.d.ts +2 -1
- package/dist/utils/compatibility.js +6 -8
- package/dist/utils/config-store.d.ts +35 -0
- package/dist/utils/config-store.js +115 -0
- package/dist/utils/connectivity.js +8 -7
- package/dist/utils/expo.js +29 -24
- package/dist/utils/orgs.d.ts +11 -0
- package/dist/utils/orgs.js +36 -0
- package/dist/utils/paths.d.ts +11 -0
- package/dist/utils/paths.js +21 -0
- package/dist/utils/progress.d.ts +13 -0
- package/dist/utils/progress.js +47 -0
- package/dist/utils/styling.d.ts +42 -36
- package/dist/utils/styling.js +78 -82
- package/dist/utils/ui.d.ts +41 -0
- package/dist/utils/ui.js +95 -0
- package/package.json +36 -45
- package/bin/dev.cmd +0 -3
- package/bin/dev.js +0 -6
- package/bin/run.cmd +0 -3
- package/bin/run.js +0 -7
- package/dist/types/schema.types.d.ts +0 -2702
- package/dist/types/schema.types.js +0 -3
- package/oclif.manifest.json +0 -884
package/dist/methods.js
CHANGED
|
@@ -1,86 +1,101 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const styling_1 = require("./utils/styling");
|
|
1
|
+
import { ux } from './utils/progress.js';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { createReadStream, createWriteStream, mkdirSync, readdirSync, writeFileSync, } from 'node:fs';
|
|
4
|
+
import { access, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
5
|
+
import * as os from 'node:os';
|
|
6
|
+
import * as path from 'node:path';
|
|
7
|
+
import { pipeline } from 'node:stream/promises';
|
|
8
|
+
import StreamZip from 'node-stream-zip';
|
|
9
|
+
import * as yazl from 'yazl';
|
|
10
|
+
import { inferEnvFromApiUrl } from './config/environments.js';
|
|
11
|
+
import { ApiError, ApiGateway } from './gateways/api-gateway.js';
|
|
12
|
+
import { SupabaseGateway } from './gateways/supabase-gateway.js';
|
|
13
|
+
import { MetadataExtractorService } from './services/metadata-extractor.service.js';
|
|
14
|
+
import { colors, formatId } from './utils/styling.js';
|
|
16
15
|
const mimeTypeLookupByExtension = {
|
|
17
16
|
apk: 'application/vnd.android.package-archive',
|
|
18
17
|
yaml: 'application/x-yaml',
|
|
19
18
|
zip: 'application/zip',
|
|
20
19
|
};
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
chunks.push(chunk);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
archive.pipe(writable);
|
|
31
|
-
await archive.finalize();
|
|
32
|
-
// once done, concatenate chunks
|
|
33
|
-
return Buffer.concat(chunks);
|
|
34
|
-
};
|
|
35
|
-
exports.toBuffer = toBuffer;
|
|
36
|
-
const compressFolderToBlob = async (sourceDir) => {
|
|
37
|
-
const archive = archiver('zip', {
|
|
38
|
-
zlib: { level: 9 },
|
|
39
|
-
});
|
|
40
|
-
archive.on('error', (err) => {
|
|
41
|
-
throw err;
|
|
42
|
-
});
|
|
43
|
-
archive.directory(sourceDir, sourceDir.split('/').pop());
|
|
44
|
-
const buffer = await (0, exports.toBuffer)(archive);
|
|
45
|
-
return new Blob([buffer], { type: 'application/zip' });
|
|
46
|
-
};
|
|
47
|
-
exports.compressFolderToBlob = compressFolderToBlob;
|
|
48
|
-
const compressFilesFromRelativePath = async (basePath, files, commonRoot) => {
|
|
49
|
-
const archive = archiver('zip', {
|
|
50
|
-
zlib: { level: 9 },
|
|
20
|
+
async function zipToBuffer(zipfile) {
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
// Typed as Uint8Array[] so Buffer.concat infers Buffer<ArrayBuffer> — matches
|
|
23
|
+
// the `BlobPart` shape that callers pass to `new Blob([...])`.
|
|
24
|
+
const chunks = [];
|
|
25
|
+
zipfile.outputStream.on('data', (chunk) => chunks.push(chunk));
|
|
26
|
+
zipfile.outputStream.on('end', () => resolve(Buffer.concat(chunks)));
|
|
27
|
+
zipfile.outputStream.on('error', reject);
|
|
28
|
+
zipfile.end();
|
|
51
29
|
});
|
|
52
|
-
|
|
53
|
-
|
|
30
|
+
}
|
|
31
|
+
/** Normalize a filesystem path to a POSIX-style zip entry name. */
|
|
32
|
+
function toZipEntryName(relativePath) {
|
|
33
|
+
return relativePath.split(path.sep).join('/').replace(/^\/+/, '');
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Zip a .app directory to a temp file on disk, streaming entries through
|
|
37
|
+
* yazl so the archive is never held in memory (iOS bundles run to GBs).
|
|
38
|
+
* Returns the temp directory holding the zip — callers remove it after the
|
|
39
|
+
* upload completes.
|
|
40
|
+
*/
|
|
41
|
+
async function compressFolderToTempZip(sourceDir) {
|
|
42
|
+
const zipfile = new yazl.ZipFile();
|
|
43
|
+
const rootName = path.basename(sourceDir);
|
|
44
|
+
const entries = readdirSync(sourceDir, {
|
|
45
|
+
recursive: true,
|
|
46
|
+
withFileTypes: true,
|
|
54
47
|
});
|
|
48
|
+
for (const entry of entries) {
|
|
49
|
+
if (!entry.isFile())
|
|
50
|
+
continue;
|
|
51
|
+
const absolutePath = path.join(entry.parentPath, entry.name);
|
|
52
|
+
const relativePath = path.relative(sourceDir, absolutePath);
|
|
53
|
+
zipfile.addFile(absolutePath, toZipEntryName(path.join(rootName, relativePath)));
|
|
54
|
+
}
|
|
55
|
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'dcd-app-zip-'));
|
|
56
|
+
const zipPath = path.join(tempDir, `${rootName}.zip`);
|
|
57
|
+
zipfile.end();
|
|
58
|
+
await pipeline(zipfile.outputStream, createWriteStream(zipPath));
|
|
59
|
+
return { tempDir, zipPath };
|
|
60
|
+
}
|
|
61
|
+
export const compressFilesFromRelativePath = async (basePath, files, commonRoot) => {
|
|
62
|
+
const zipfile = new yazl.ZipFile();
|
|
55
63
|
for (const file of files) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
64
|
+
// Anchored prefix strip — replace() would remove the first occurrence
|
|
65
|
+
// of commonRoot anywhere in the path, not just at the start.
|
|
66
|
+
zipfile.addFile(path.resolve(basePath, file), toZipEntryName(file.startsWith(commonRoot) ? file.slice(commonRoot.length) : file));
|
|
59
67
|
}
|
|
60
|
-
|
|
61
|
-
// await writeFile('./my-zip.zip', buffer);
|
|
62
|
-
return buffer;
|
|
68
|
+
return zipToBuffer(zipfile);
|
|
63
69
|
};
|
|
64
|
-
|
|
65
|
-
const verifyAppZip = async (zipPath) => {
|
|
70
|
+
export const verifyAppZip = async (zipPath) => {
|
|
66
71
|
// eslint-disable-next-line import/namespace, new-cap
|
|
67
72
|
const zip = await new StreamZip.async({
|
|
68
73
|
file: zipPath,
|
|
69
74
|
storeEntries: true,
|
|
70
75
|
});
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
try {
|
|
77
|
+
const entries = await zip.entries();
|
|
78
|
+
// Derive top-level names from all entries rather than requiring an
|
|
79
|
+
// explicit directory entry — zips created without directory entries
|
|
80
|
+
// (e.g. Python's zipfile) are valid but have no ".app/" entry itself.
|
|
81
|
+
// macOS metadata (__MACOSX resource forks, .DS_Store) doesn't count
|
|
82
|
+
// toward the "exactly one .app" rule.
|
|
83
|
+
const topLevelNames = new Set(Object.values(entries)
|
|
84
|
+
.filter((entry) => !entry.name.startsWith('__MACOSX/') &&
|
|
85
|
+
entry.name.split('/').pop() !== '.DS_Store')
|
|
86
|
+
.map((entry) => entry.name.split('/')[0]));
|
|
87
|
+
if (topLevelNames.size !== 1 || ![...topLevelNames][0].endsWith('.app')) {
|
|
88
|
+
throw new Error('Zip file must contain exactly one entry which is a .app, check the contents of the zip file');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
zip.close();
|
|
76
93
|
}
|
|
77
|
-
zip.close();
|
|
78
94
|
};
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
const { filePath, apiUrl, apiKey, ignoreShaCheck = false, log = true, debug = false } = config;
|
|
95
|
+
export const uploadBinary = async (config) => {
|
|
96
|
+
const { filePath, apiUrl, auth, ignoreShaCheck = false, log = true, debug = false } = config;
|
|
82
97
|
if (log) {
|
|
83
|
-
|
|
98
|
+
ux.action.start(colors.bold('Checking and uploading binary'), colors.dim('Initializing'), {
|
|
84
99
|
stdout: true,
|
|
85
100
|
});
|
|
86
101
|
}
|
|
@@ -91,32 +106,33 @@ const uploadBinary = async (config) => {
|
|
|
91
106
|
console.log(`[DEBUG] Ignore SHA check: ${ignoreShaCheck}`);
|
|
92
107
|
}
|
|
93
108
|
const startTime = Date.now();
|
|
109
|
+
let source;
|
|
94
110
|
try {
|
|
95
111
|
// Prepare file for upload
|
|
96
|
-
|
|
112
|
+
source = await prepareFileForUpload(filePath, debug, startTime);
|
|
97
113
|
// Calculate SHA hash
|
|
98
|
-
const sha = await calculateFileHash(
|
|
114
|
+
const sha = await calculateFileHash(source, debug, log);
|
|
99
115
|
// Check for existing upload with same SHA
|
|
100
116
|
if (!ignoreShaCheck && sha) {
|
|
101
|
-
const { exists, binaryId } = await checkExistingUpload(apiUrl,
|
|
117
|
+
const { exists, binaryId } = await checkExistingUpload(apiUrl, auth, sha, debug);
|
|
102
118
|
if (exists && binaryId) {
|
|
103
119
|
if (log) {
|
|
104
|
-
|
|
105
|
-
|
|
120
|
+
ux.info(colors.dim('SHA hash matches existing binary with ID: ') + formatId(binaryId) + colors.dim(', skipping upload. Force upload with --ignore-sha-check'));
|
|
121
|
+
ux.action.stop(colors.info('Skipping upload'));
|
|
106
122
|
}
|
|
107
123
|
return binaryId;
|
|
108
124
|
}
|
|
109
125
|
}
|
|
110
126
|
// Perform the upload
|
|
111
|
-
const uploadId = await performUpload({
|
|
127
|
+
const uploadId = await performUpload({ auth, apiUrl, debug, filePath, sha, source, startTime });
|
|
112
128
|
if (log) {
|
|
113
|
-
|
|
129
|
+
ux.action.stop(colors.success('\n✓ Binary uploaded with ID: ') + formatId(uploadId));
|
|
114
130
|
}
|
|
115
131
|
return uploadId;
|
|
116
132
|
}
|
|
117
133
|
catch (error) {
|
|
118
134
|
if (log) {
|
|
119
|
-
|
|
135
|
+
ux.action.stop(colors.error('✗ Failed'));
|
|
120
136
|
}
|
|
121
137
|
if (debug) {
|
|
122
138
|
console.error('[DEBUG] === BINARY UPLOAD FAILED ===');
|
|
@@ -128,38 +144,35 @@ const uploadBinary = async (config) => {
|
|
|
128
144
|
}
|
|
129
145
|
console.error(`[DEBUG] Failed after ${Date.now() - startTime}ms`);
|
|
130
146
|
}
|
|
131
|
-
// Add helpful context for common errors
|
|
132
|
-
if (error instanceof Error) {
|
|
133
|
-
if (error.name === 'NetworkError') {
|
|
134
|
-
throw error; // NetworkError already has detailed troubleshooting info
|
|
135
|
-
}
|
|
136
|
-
// Re-throw with original message
|
|
137
|
-
throw error;
|
|
138
|
-
}
|
|
139
147
|
throw error;
|
|
140
148
|
}
|
|
149
|
+
finally {
|
|
150
|
+
if (source?.cleanupDir) {
|
|
151
|
+
await rm(source.cleanupDir, { recursive: true, force: true }).catch(() => { });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
141
154
|
};
|
|
142
|
-
exports.uploadBinary = uploadBinary;
|
|
143
155
|
/**
|
|
144
|
-
* Prepares a file for upload
|
|
156
|
+
* Prepares a file for upload: .app directories are zipped to a temp file on
|
|
157
|
+
* disk; everything else is described in place. Nothing is read into memory.
|
|
145
158
|
* @param filePath Path to the file to upload
|
|
146
159
|
* @param debug Whether debug logging is enabled
|
|
147
160
|
* @param startTime Timestamp when upload started
|
|
148
|
-
* @returns Promise resolving to
|
|
161
|
+
* @returns Promise resolving to the upload source descriptor
|
|
149
162
|
*/
|
|
150
163
|
async function prepareFileForUpload(filePath, debug, startTime) {
|
|
151
164
|
if (debug) {
|
|
152
165
|
console.log('[DEBUG] Preparing file for upload...');
|
|
153
166
|
}
|
|
154
|
-
let
|
|
167
|
+
let source;
|
|
155
168
|
if (filePath?.endsWith('.app')) {
|
|
156
169
|
if (debug) {
|
|
157
170
|
console.log('[DEBUG] Compressing .app folder to zip...');
|
|
158
171
|
}
|
|
159
|
-
// Validate that the .app directory exists before attempting to compress
|
|
160
|
-
//
|
|
172
|
+
// Validate that the .app directory exists before attempting to compress —
|
|
173
|
+
// zipping a non-existent path silently produces an empty 22-byte zip.
|
|
161
174
|
try {
|
|
162
|
-
await
|
|
175
|
+
await access(filePath);
|
|
163
176
|
}
|
|
164
177
|
catch {
|
|
165
178
|
// Provide helpful error message for common quoting issues
|
|
@@ -181,29 +194,36 @@ async function prepareFileForUpload(filePath, debug, startTime) {
|
|
|
181
194
|
.join('\n');
|
|
182
195
|
throw new Error(errorMessage);
|
|
183
196
|
}
|
|
184
|
-
const
|
|
185
|
-
|
|
197
|
+
const { tempDir, zipPath } = await compressFolderToTempZip(filePath);
|
|
198
|
+
const { size } = await stat(zipPath);
|
|
199
|
+
source = {
|
|
200
|
+
contentType: 'application/zip',
|
|
201
|
+
cleanupDir: tempDir,
|
|
202
|
+
diskPath: zipPath,
|
|
203
|
+
name: filePath + '.zip',
|
|
204
|
+
size,
|
|
205
|
+
};
|
|
186
206
|
if (debug) {
|
|
187
|
-
console.log(`[DEBUG] Compressed file size: ${(
|
|
207
|
+
console.log(`[DEBUG] Compressed file size: ${(size / 1024 / 1024).toFixed(2)} MB`);
|
|
188
208
|
}
|
|
189
209
|
}
|
|
190
210
|
else {
|
|
211
|
+
const { size } = await stat(filePath);
|
|
191
212
|
if (debug) {
|
|
192
|
-
console.log(
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
file = new File([binaryBlob], filePath);
|
|
213
|
+
console.log(`[DEBUG] File size: ${(size / 1024 / 1024).toFixed(2)} MB`);
|
|
214
|
+
}
|
|
215
|
+
source = {
|
|
216
|
+
contentType: mimeTypeLookupByExtension[filePath.split('.').pop()] ||
|
|
217
|
+
'application/octet-stream',
|
|
218
|
+
diskPath: filePath,
|
|
219
|
+
name: filePath,
|
|
220
|
+
size,
|
|
221
|
+
};
|
|
202
222
|
}
|
|
203
223
|
if (debug) {
|
|
204
224
|
console.log(`[DEBUG] File preparation completed in ${Date.now() - startTime}ms`);
|
|
205
225
|
}
|
|
206
|
-
return
|
|
226
|
+
return source;
|
|
207
227
|
}
|
|
208
228
|
/**
|
|
209
229
|
* Calculates SHA-256 hash for a file
|
|
@@ -212,13 +232,13 @@ async function prepareFileForUpload(filePath, debug, startTime) {
|
|
|
212
232
|
* @param log Whether to log warnings
|
|
213
233
|
* @returns Promise resolving to SHA-256 hash or undefined if failed
|
|
214
234
|
*/
|
|
215
|
-
async function calculateFileHash(
|
|
235
|
+
async function calculateFileHash(source, debug, log) {
|
|
216
236
|
try {
|
|
217
237
|
if (debug) {
|
|
218
238
|
console.log('[DEBUG] Calculating SHA-256 hash...');
|
|
219
239
|
}
|
|
220
240
|
const hashStartTime = Date.now();
|
|
221
|
-
const sha = await
|
|
241
|
+
const sha = await getFileHashFromPath(source.diskPath);
|
|
222
242
|
if (debug) {
|
|
223
243
|
console.log(`[DEBUG] SHA-256 hash: ${sha}`);
|
|
224
244
|
console.log(`[DEBUG] Hash calculation completed in ${Date.now() - hashStartTime}ms`);
|
|
@@ -238,19 +258,19 @@ async function calculateFileHash(file, debug, log) {
|
|
|
238
258
|
/**
|
|
239
259
|
* Checks if an upload with the same SHA already exists
|
|
240
260
|
* @param apiUrl API base URL
|
|
241
|
-
* @param
|
|
261
|
+
* @param auth AuthContext carrying request headers
|
|
242
262
|
* @param sha SHA-256 hash to check
|
|
243
263
|
* @param debug Whether debug logging is enabled
|
|
244
264
|
* @returns Promise resolving to object with exists flag and optional binaryId
|
|
245
265
|
*/
|
|
246
|
-
async function checkExistingUpload(apiUrl,
|
|
266
|
+
async function checkExistingUpload(apiUrl, auth, sha, debug) {
|
|
247
267
|
try {
|
|
248
268
|
if (debug) {
|
|
249
269
|
console.log('[DEBUG] Checking for existing upload with matching SHA...');
|
|
250
270
|
console.log(`[DEBUG] Target endpoint: ${apiUrl}/uploads/checkForExistingUpload`);
|
|
251
271
|
}
|
|
252
272
|
const shaCheckStartTime = Date.now();
|
|
253
|
-
const { appBinaryId, exists } = await
|
|
273
|
+
const { appBinaryId, exists } = await ApiGateway.checkForExistingUpload(apiUrl, auth, sha);
|
|
254
274
|
if (debug) {
|
|
255
275
|
console.log(`[DEBUG] SHA check completed in ${Date.now() - shaCheckStartTime}ms`);
|
|
256
276
|
console.log(`[DEBUG] Existing binary found: ${exists}`);
|
|
@@ -261,6 +281,11 @@ async function checkExistingUpload(apiUrl, apiKey, sha, debug) {
|
|
|
261
281
|
return { binaryId: appBinaryId, exists };
|
|
262
282
|
}
|
|
263
283
|
catch (error) {
|
|
284
|
+
// Invalid credentials will fail every subsequent request — surface now
|
|
285
|
+
// rather than after the user has waited through a potentially huge upload.
|
|
286
|
+
if (error instanceof ApiError && (error.status === 401 || error.status === 403)) {
|
|
287
|
+
throw error;
|
|
288
|
+
}
|
|
264
289
|
if (debug) {
|
|
265
290
|
console.error('[DEBUG] === SHA CHECK FAILED ===');
|
|
266
291
|
console.error('[DEBUG] Continuing with upload despite SHA check failure');
|
|
@@ -286,19 +311,19 @@ async function checkExistingUpload(apiUrl, apiKey, sha, debug) {
|
|
|
286
311
|
* @param debug - Enable debug logging
|
|
287
312
|
* @returns Upload result with success status and any error
|
|
288
313
|
*/
|
|
289
|
-
async function uploadToSupabase(env, tempPath,
|
|
314
|
+
async function uploadToSupabase(env, tempPath, source, debug) {
|
|
290
315
|
if (debug) {
|
|
291
316
|
console.log(`[DEBUG] Uploading to Supabase storage (${env}) using resumable uploads...`);
|
|
292
317
|
console.log(`[DEBUG] Staging path: ${tempPath}`);
|
|
293
|
-
console.log(`[DEBUG] File size: ${(
|
|
318
|
+
console.log(`[DEBUG] File size: ${(source.size / 1024 / 1024).toFixed(2)} MB`);
|
|
294
319
|
}
|
|
295
320
|
try {
|
|
296
321
|
const uploadStartTime = Date.now();
|
|
297
|
-
await
|
|
322
|
+
await SupabaseGateway.uploadResumable(env, tempPath, source, debug);
|
|
298
323
|
if (debug) {
|
|
299
324
|
const uploadDuration = Date.now() - uploadStartTime;
|
|
300
325
|
const uploadDurationSeconds = uploadDuration / 1000;
|
|
301
|
-
const uploadSpeed = (
|
|
326
|
+
const uploadSpeed = (source.size / 1024 / 1024) / uploadDurationSeconds;
|
|
302
327
|
console.log(`[DEBUG] Supabase resumable upload completed in ${uploadDurationSeconds.toFixed(2)}s (${uploadDuration}ms)`);
|
|
303
328
|
console.log(`[DEBUG] Average upload speed: ${uploadSpeed.toFixed(2)} MB/s`);
|
|
304
329
|
}
|
|
@@ -314,7 +339,7 @@ async function uploadToSupabase(env, tempPath, file, debug) {
|
|
|
314
339
|
console.error(`[DEBUG] Error stack:\n${uploadError.stack}`);
|
|
315
340
|
}
|
|
316
341
|
console.error(`[DEBUG] Staging path: ${tempPath}`);
|
|
317
|
-
console.error(`[DEBUG] File size: ${
|
|
342
|
+
console.error(`[DEBUG] File size: ${source.size} bytes`);
|
|
318
343
|
console.log('[DEBUG] Will attempt Backblaze fallback if available...');
|
|
319
344
|
}
|
|
320
345
|
return { error: uploadError, success: false };
|
|
@@ -326,7 +351,7 @@ async function uploadToSupabase(env, tempPath, file, debug) {
|
|
|
326
351
|
* @returns Upload result with success status and any error
|
|
327
352
|
*/
|
|
328
353
|
async function handleBackblazeUpload(config) {
|
|
329
|
-
const { b2, apiUrl,
|
|
354
|
+
const { b2, apiUrl, auth, finalPath, source, debug } = config;
|
|
330
355
|
if (!b2) {
|
|
331
356
|
if (debug) {
|
|
332
357
|
console.log('[DEBUG] Backblaze not configured, will fall back to Supabase');
|
|
@@ -341,19 +366,18 @@ async function handleBackblazeUpload(config) {
|
|
|
341
366
|
let backblazeSuccess = false;
|
|
342
367
|
if (b2.strategy === 'simple' && b2.simple) {
|
|
343
368
|
const simple = b2.simple;
|
|
344
|
-
backblazeSuccess = await uploadToBackblaze(simple.uploadUrl, simple.authorizationToken, `organizations/${finalPath}`,
|
|
369
|
+
backblazeSuccess = await uploadToBackblaze(simple.uploadUrl, simple.authorizationToken, `organizations/${finalPath}`, source, debug);
|
|
345
370
|
}
|
|
346
371
|
else if (b2.strategy === 'large' && b2.large) {
|
|
347
372
|
const large = b2.large;
|
|
348
373
|
backblazeSuccess = await uploadLargeFileToBackblaze({
|
|
349
|
-
|
|
374
|
+
auth,
|
|
350
375
|
apiUrl,
|
|
351
376
|
debug,
|
|
352
377
|
fileId: large.fileId,
|
|
353
378
|
fileName: `organizations/${finalPath}`,
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
fileSize: file.size,
|
|
379
|
+
filePath: source.diskPath,
|
|
380
|
+
fileSize: source.size,
|
|
357
381
|
uploadPartUrls: large.uploadPartUrls,
|
|
358
382
|
});
|
|
359
383
|
}
|
|
@@ -383,13 +407,13 @@ async function handleBackblazeUpload(config) {
|
|
|
383
407
|
/**
|
|
384
408
|
* Requests upload URL and paths from API
|
|
385
409
|
* @param apiUrl - API base URL
|
|
386
|
-
* @param
|
|
410
|
+
* @param auth - AuthContext carrying request headers
|
|
387
411
|
* @param filePath - Path to the file being uploaded
|
|
388
412
|
* @param fileSize - Size of the file in bytes
|
|
389
413
|
* @param debug - Enable debug logging
|
|
390
414
|
* @returns Promise resolving to upload paths and configuration
|
|
391
415
|
*/
|
|
392
|
-
async function requestUploadPaths(apiUrl,
|
|
416
|
+
async function requestUploadPaths(apiUrl, auth, filePath, fileSize, debug) {
|
|
393
417
|
const platform = filePath?.endsWith('.apk') ? 'android' : 'ios';
|
|
394
418
|
if (debug) {
|
|
395
419
|
console.log('[DEBUG] Requesting upload URL...');
|
|
@@ -398,7 +422,7 @@ async function requestUploadPaths(apiUrl, apiKey, filePath, fileSize, debug) {
|
|
|
398
422
|
}
|
|
399
423
|
try {
|
|
400
424
|
const urlRequestStartTime = Date.now();
|
|
401
|
-
const { id, tempPath, finalPath, b2 } = await
|
|
425
|
+
const { id, tempPath, finalPath, b2 } = await ApiGateway.getBinaryUploadUrl(apiUrl, auth, platform, fileSize);
|
|
402
426
|
if (debug) {
|
|
403
427
|
const hasStrategy = b2 && typeof b2 === 'object' && 'strategy' in b2;
|
|
404
428
|
console.log(`[DEBUG] Upload URL request completed in ${Date.now() - urlRequestStartTime}ms`);
|
|
@@ -421,9 +445,11 @@ async function requestUploadPaths(apiUrl, apiKey, filePath, fileSize, debug) {
|
|
|
421
445
|
// Add context to the error
|
|
422
446
|
if (error instanceof Error) {
|
|
423
447
|
if (error.name === 'NetworkError') {
|
|
424
|
-
throw new Error(`Failed to request upload URL from API.\n\n${error.message}
|
|
448
|
+
throw new Error(`Failed to request upload URL from API.\n\n${error.message}`, { cause: error });
|
|
425
449
|
}
|
|
426
|
-
throw new Error(`Failed to request upload URL: ${error.message}
|
|
450
|
+
throw new Error(`Failed to request upload URL: ${error.message}`, {
|
|
451
|
+
cause: error,
|
|
452
|
+
});
|
|
427
453
|
}
|
|
428
454
|
throw error;
|
|
429
455
|
}
|
|
@@ -437,7 +463,7 @@ async function requestUploadPaths(apiUrl, apiKey, filePath, fileSize, debug) {
|
|
|
437
463
|
async function extractBinaryMetadata(filePath, debug) {
|
|
438
464
|
if (debug)
|
|
439
465
|
console.log('[DEBUG] Extracting app metadata...');
|
|
440
|
-
const metadataExtractor = new
|
|
466
|
+
const metadataExtractor = new MetadataExtractorService();
|
|
441
467
|
const metadata = await metadataExtractor.extract(filePath);
|
|
442
468
|
if (!metadata) {
|
|
443
469
|
throw new Error(`Failed to extract metadata from ${filePath}. Supported formats: .apk, .app, .zip`);
|
|
@@ -478,29 +504,27 @@ function validateUploadResults(supabaseSuccess, backblazeSuccess, lastError, b2,
|
|
|
478
504
|
* @returns Promise resolving to upload ID
|
|
479
505
|
*/
|
|
480
506
|
async function performUpload(config) {
|
|
481
|
-
const { filePath, apiUrl,
|
|
507
|
+
const { filePath, apiUrl, auth, source, sha, debug, startTime } = config;
|
|
482
508
|
// Request upload URL and paths
|
|
483
|
-
const { id, tempPath, finalPath, b2 } = await requestUploadPaths(apiUrl,
|
|
509
|
+
const { id, tempPath, finalPath, b2 } = await requestUploadPaths(apiUrl, auth, filePath, source.size, debug);
|
|
484
510
|
// Extract app metadata
|
|
485
511
|
const metadata = await extractBinaryMetadata(filePath, debug);
|
|
486
|
-
const env = apiUrl
|
|
512
|
+
const env = inferEnvFromApiUrl(apiUrl);
|
|
487
513
|
// Upload to Backblaze first (primary)
|
|
488
514
|
const backblazeResult = await handleBackblazeUpload({
|
|
489
|
-
|
|
515
|
+
auth,
|
|
490
516
|
apiUrl,
|
|
491
517
|
b2: b2,
|
|
492
518
|
debug,
|
|
493
|
-
file,
|
|
494
|
-
filePath,
|
|
495
519
|
finalPath,
|
|
520
|
+
source,
|
|
496
521
|
});
|
|
497
522
|
let lastError = backblazeResult.error;
|
|
498
523
|
// Always upload to Supabase (re-enabled as always-on alongside Backblaze)
|
|
499
|
-
let supabaseResult = { error: null, success: false };
|
|
500
524
|
if (debug) {
|
|
501
525
|
console.log('[DEBUG] Uploading to Supabase...');
|
|
502
526
|
}
|
|
503
|
-
supabaseResult = await uploadToSupabase(env, tempPath,
|
|
527
|
+
const supabaseResult = await uploadToSupabase(env, tempPath, source, debug);
|
|
504
528
|
if (!supabaseResult.success && supabaseResult.error) {
|
|
505
529
|
lastError = supabaseResult.error;
|
|
506
530
|
}
|
|
@@ -520,15 +544,16 @@ async function performUpload(config) {
|
|
|
520
544
|
}
|
|
521
545
|
// Finalize upload
|
|
522
546
|
const finalizeStartTime = Date.now();
|
|
523
|
-
await
|
|
524
|
-
|
|
547
|
+
await ApiGateway.finaliseUpload({
|
|
548
|
+
auth,
|
|
525
549
|
backblazeSuccess: backblazeResult.success,
|
|
526
550
|
baseUrl: apiUrl,
|
|
527
|
-
bytes:
|
|
551
|
+
bytes: source.size,
|
|
528
552
|
id,
|
|
529
553
|
metadata,
|
|
530
554
|
path: tempPath,
|
|
531
|
-
sha
|
|
555
|
+
// sha is undefined when hash calculation failed — omit it explicitly
|
|
556
|
+
...(sha ? { sha } : {}),
|
|
532
557
|
supabaseSuccess: supabaseResult.success,
|
|
533
558
|
});
|
|
534
559
|
if (debug) {
|
|
@@ -537,21 +562,89 @@ async function performUpload(config) {
|
|
|
537
562
|
}
|
|
538
563
|
return id;
|
|
539
564
|
}
|
|
565
|
+
/**
|
|
566
|
+
* Uploads an already-built flow zip directly to storage, mirroring the binary
|
|
567
|
+
* client-direct path: getFlowUploadUrl → upload to storage (Backblaze if
|
|
568
|
+
* offered + TUS to Supabase) → return the storage reference for submitFlowTest.
|
|
569
|
+
*
|
|
570
|
+
* The zip arrives as an in-memory Buffer (from `compressFilesFromRelativePath`);
|
|
571
|
+
* it's written to a temp file so the exact same disk-streaming uploaders the
|
|
572
|
+
* binary path uses can be reused unchanged. `supabaseSuccess`/`backblazeSuccess`
|
|
573
|
+
* are reported honestly based on which uploads succeeded.
|
|
574
|
+
*
|
|
575
|
+
* Errors from `getFlowUploadUrl` propagate untouched so callers can detect a
|
|
576
|
+
* 404 from an older API and fall back to the legacy multipart endpoint.
|
|
577
|
+
*/
|
|
578
|
+
export const uploadFlowZip = async (config) => {
|
|
579
|
+
const { apiUrl, auth, buffer, debug = false } = config;
|
|
580
|
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'dcd-flow-zip-'));
|
|
581
|
+
const diskPath = path.join(tempDir, 'flowFile.zip');
|
|
582
|
+
try {
|
|
583
|
+
await writeFile(diskPath, buffer);
|
|
584
|
+
const source = {
|
|
585
|
+
contentType: 'application/zip',
|
|
586
|
+
diskPath,
|
|
587
|
+
name: 'flowFile.zip',
|
|
588
|
+
size: buffer.length,
|
|
589
|
+
};
|
|
590
|
+
// Same response shape as getBinaryUploadUrl. A 404 here means the API
|
|
591
|
+
// predates the client-direct flow path — let it propagate so the caller
|
|
592
|
+
// falls back to the multipart endpoint.
|
|
593
|
+
const { id, tempPath, finalPath, b2 } = await ApiGateway.getFlowUploadUrl(apiUrl, auth, buffer.length);
|
|
594
|
+
if (!tempPath)
|
|
595
|
+
throw new Error('No upload path provided by API');
|
|
596
|
+
const env = inferEnvFromApiUrl(apiUrl);
|
|
597
|
+
// Upload to Backblaze first (primary, if configured)
|
|
598
|
+
const backblazeResult = await handleBackblazeUpload({
|
|
599
|
+
auth,
|
|
600
|
+
apiUrl,
|
|
601
|
+
b2: b2,
|
|
602
|
+
debug,
|
|
603
|
+
finalPath,
|
|
604
|
+
source,
|
|
605
|
+
});
|
|
606
|
+
let lastError = backblazeResult.error;
|
|
607
|
+
// Always upload to Supabase (always-on alongside Backblaze)
|
|
608
|
+
const supabaseResult = await uploadToSupabase(env, tempPath, source, debug);
|
|
609
|
+
if (!supabaseResult.success && supabaseResult.error) {
|
|
610
|
+
lastError = supabaseResult.error;
|
|
611
|
+
}
|
|
612
|
+
validateUploadResults(supabaseResult.success, backblazeResult.success, lastError, b2, debug);
|
|
613
|
+
if (debug) {
|
|
614
|
+
console.log(`[DEBUG] Flow zip upload summary - Backblaze: ${backblazeResult.success ? '✓' : '✗'}, Supabase: ${supabaseResult.success ? '✓' : '✗'}`);
|
|
615
|
+
}
|
|
616
|
+
return {
|
|
617
|
+
backblazeSuccess: backblazeResult.success,
|
|
618
|
+
bytes: buffer.length,
|
|
619
|
+
id,
|
|
620
|
+
path: tempPath,
|
|
621
|
+
supabaseSuccess: supabaseResult.success,
|
|
622
|
+
useTus: true,
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
finally {
|
|
626
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => { });
|
|
627
|
+
}
|
|
628
|
+
};
|
|
540
629
|
/**
|
|
541
630
|
* Upload file to Backblaze using signed URL (simple upload for files < 100MB)
|
|
542
631
|
* @param uploadUrl - Backblaze upload URL
|
|
543
632
|
* @param authorizationToken - Authorization token for the upload
|
|
544
633
|
* @param fileName - Name/path of the file
|
|
545
|
-
* @param
|
|
634
|
+
* @param source - Upload source descriptor (streamed from disk)
|
|
546
635
|
* @param debug - Whether debug logging is enabled
|
|
547
636
|
* @returns Promise that resolves when upload completes or fails gracefully
|
|
548
637
|
*/
|
|
549
|
-
async function uploadToBackblaze(uploadUrl, authorizationToken, fileName,
|
|
638
|
+
async function uploadToBackblaze(uploadUrl, authorizationToken, fileName, source, debug) {
|
|
550
639
|
try {
|
|
551
|
-
|
|
640
|
+
// The API only picks the simple strategy for small binaries (large files
|
|
641
|
+
// get the multi-part path), so one transient buffer here is bounded.
|
|
642
|
+
// S3 pre-signed PUTs reject chunked transfer encoding, which rules out a
|
|
643
|
+
// plain stream body.
|
|
644
|
+
const body = await readFile(source.diskPath);
|
|
552
645
|
// Calculate SHA1 hash for Backblaze (B2 requires SHA1, not SHA256)
|
|
553
|
-
const sha1 =
|
|
554
|
-
sha1.update(
|
|
646
|
+
const sha1 = createHash('sha1');
|
|
647
|
+
sha1.update(body);
|
|
555
648
|
const sha1Hex = sha1.digest('hex');
|
|
556
649
|
// Detect if this is an S3 pre-signed URL (authorization token is empty)
|
|
557
650
|
const isS3PreSignedUrl = !authorizationToken || authorizationToken === '';
|
|
@@ -563,8 +656,8 @@ async function uploadToBackblaze(uploadUrl, authorizationToken, fileName, file,
|
|
|
563
656
|
}
|
|
564
657
|
// Build headers based on upload method
|
|
565
658
|
const headers = {
|
|
566
|
-
'Content-Length':
|
|
567
|
-
'Content-Type':
|
|
659
|
+
'Content-Length': body.length.toString(),
|
|
660
|
+
'Content-Type': source.contentType || 'application/octet-stream',
|
|
568
661
|
'X-Bz-Content-Sha1': sha1Hex,
|
|
569
662
|
};
|
|
570
663
|
// S3 pre-signed URLs have auth embedded in URL, native B2 uses Authorization header
|
|
@@ -573,7 +666,8 @@ async function uploadToBackblaze(uploadUrl, authorizationToken, fileName, file,
|
|
|
573
666
|
headers['X-Bz-File-Name'] = encodeURIComponent(fileName);
|
|
574
667
|
}
|
|
575
668
|
const response = await fetch(uploadUrl, {
|
|
576
|
-
|
|
669
|
+
// Zero-copy view over the buffer (new Uint8Array(buffer) would clone it).
|
|
670
|
+
body: new Uint8Array(body.buffer, body.byteOffset, body.byteLength),
|
|
577
671
|
headers,
|
|
578
672
|
method: isS3PreSignedUrl ? 'PUT' : 'POST',
|
|
579
673
|
});
|
|
@@ -625,7 +719,7 @@ async function uploadToBackblaze(uploadUrl, authorizationToken, fileName, file,
|
|
|
625
719
|
async function readFileChunk(filePath, start, end) {
|
|
626
720
|
return new Promise((resolve, reject) => {
|
|
627
721
|
const chunks = [];
|
|
628
|
-
const stream =
|
|
722
|
+
const stream = createReadStream(filePath, { start, end: end - 1 }); // end is inclusive in createReadStream
|
|
629
723
|
stream.on('data', (chunk) => {
|
|
630
724
|
chunks.push(chunk);
|
|
631
725
|
});
|
|
@@ -637,38 +731,13 @@ async function readFileChunk(filePath, start, end) {
|
|
|
637
731
|
});
|
|
638
732
|
});
|
|
639
733
|
}
|
|
640
|
-
/**
|
|
641
|
-
* Helper function to read a chunk from a File/Blob object
|
|
642
|
-
* @param file - File or Blob object
|
|
643
|
-
* @param start - Start byte position
|
|
644
|
-
* @param end - End byte position (exclusive)
|
|
645
|
-
* @returns Promise resolving to Buffer containing the chunk
|
|
646
|
-
*/
|
|
647
|
-
async function readFileObjectChunk(file, start, end) {
|
|
648
|
-
const slice = file.slice(start, end);
|
|
649
|
-
const arrayBuffer = await slice.arrayBuffer();
|
|
650
|
-
return Buffer.from(arrayBuffer);
|
|
651
|
-
}
|
|
652
|
-
/**
|
|
653
|
-
* Reads a file chunk from either a File object or disk
|
|
654
|
-
* @param fileObject - Optional File object to read from
|
|
655
|
-
* @param filePath - Path to file on disk
|
|
656
|
-
* @param start - Start byte position
|
|
657
|
-
* @param end - End byte position
|
|
658
|
-
* @returns Promise resolving to Buffer containing the chunk
|
|
659
|
-
*/
|
|
660
|
-
async function readChunk(fileObject, filePath, start, end) {
|
|
661
|
-
return fileObject
|
|
662
|
-
? readFileObjectChunk(fileObject, start, end)
|
|
663
|
-
: readFileChunk(filePath, start, end);
|
|
664
|
-
}
|
|
665
734
|
/**
|
|
666
735
|
* Calculates SHA1 hash for a buffer
|
|
667
736
|
* @param buffer - Buffer to hash
|
|
668
737
|
* @returns SHA1 hash as hex string
|
|
669
738
|
*/
|
|
670
739
|
function calculateSha1(buffer) {
|
|
671
|
-
const sha1 =
|
|
740
|
+
const sha1 = createHash('sha1');
|
|
672
741
|
sha1.update(buffer);
|
|
673
742
|
return sha1.digest('hex');
|
|
674
743
|
}
|
|
@@ -706,7 +775,9 @@ async function uploadPartToBackblaze(config) {
|
|
|
706
775
|
if (debug) {
|
|
707
776
|
console.error(`[DEBUG] Network error uploading part ${partNumber} - could be DNS, connection, or SSL issue`);
|
|
708
777
|
}
|
|
709
|
-
throw new Error(`Part ${partNumber} upload failed due to network error
|
|
778
|
+
throw new Error(`Part ${partNumber} upload failed due to network error`, {
|
|
779
|
+
cause: error,
|
|
780
|
+
});
|
|
710
781
|
}
|
|
711
782
|
throw error;
|
|
712
783
|
}
|
|
@@ -744,14 +815,14 @@ function logBackblazeUploadError(error, debug) {
|
|
|
744
815
|
* @returns Promise that resolves when upload completes or fails gracefully
|
|
745
816
|
*/
|
|
746
817
|
async function uploadLargeFileToBackblaze(config) {
|
|
747
|
-
const { apiUrl,
|
|
818
|
+
const { apiUrl, auth, fileId, uploadPartUrls, filePath, fileSize, debug } = config;
|
|
748
819
|
try {
|
|
749
820
|
const partSha1Array = [];
|
|
750
821
|
const partSize = Math.ceil(fileSize / uploadPartUrls.length);
|
|
751
822
|
if (debug) {
|
|
752
823
|
console.log(`[DEBUG] Uploading large file in ${uploadPartUrls.length} parts (streaming mode)`);
|
|
753
824
|
console.log(`[DEBUG] Part size: ${(partSize / 1024 / 1024).toFixed(2)} MB`);
|
|
754
|
-
console.log(`[DEBUG] Reading from: ${
|
|
825
|
+
console.log(`[DEBUG] Reading from: ${filePath}`);
|
|
755
826
|
}
|
|
756
827
|
// Upload each part using streaming to avoid loading entire file into memory
|
|
757
828
|
for (let i = 0; i < uploadPartUrls.length; i++) {
|
|
@@ -762,7 +833,7 @@ async function uploadLargeFileToBackblaze(config) {
|
|
|
762
833
|
if (debug) {
|
|
763
834
|
console.log(`[DEBUG] Reading part ${partNumber}/${uploadPartUrls.length} bytes ${start}-${end}`);
|
|
764
835
|
}
|
|
765
|
-
const partBuffer = await
|
|
836
|
+
const partBuffer = await readFileChunk(filePath, start, end);
|
|
766
837
|
const sha1Hex = calculateSha1(partBuffer);
|
|
767
838
|
partSha1Array.push(sha1Hex);
|
|
768
839
|
await uploadPartToBackblaze({
|
|
@@ -776,18 +847,11 @@ async function uploadLargeFileToBackblaze(config) {
|
|
|
776
847
|
uploadUrl: uploadPartUrls[i].uploadUrl,
|
|
777
848
|
});
|
|
778
849
|
}
|
|
779
|
-
// Validate all parts were uploaded
|
|
780
|
-
if (partSha1Array.length !== uploadPartUrls.length) {
|
|
781
|
-
const errorMsg = `Part count mismatch: uploaded ${partSha1Array.length} parts but expected ${uploadPartUrls.length}`;
|
|
782
|
-
if (debug)
|
|
783
|
-
console.error(`[DEBUG] ${errorMsg}`);
|
|
784
|
-
throw new Error(errorMsg);
|
|
785
|
-
}
|
|
786
850
|
if (debug) {
|
|
787
851
|
console.log('[DEBUG] Finishing large file upload...');
|
|
788
852
|
console.log(`[DEBUG] Finalizing ${partSha1Array.length} parts with fileId: ${fileId}`);
|
|
789
853
|
}
|
|
790
|
-
await
|
|
854
|
+
await ApiGateway.finishLargeFile(apiUrl, auth, fileId, partSha1Array);
|
|
791
855
|
if (debug)
|
|
792
856
|
console.log('[DEBUG] Large file upload completed successfully');
|
|
793
857
|
return true;
|
|
@@ -797,27 +861,12 @@ async function uploadLargeFileToBackblaze(config) {
|
|
|
797
861
|
return false;
|
|
798
862
|
}
|
|
799
863
|
}
|
|
800
|
-
async function
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
try {
|
|
807
|
-
let readerResult = await reader.read();
|
|
808
|
-
while (!readerResult.done) {
|
|
809
|
-
const { value } = readerResult;
|
|
810
|
-
hash.update(value);
|
|
811
|
-
readerResult = await reader.read();
|
|
812
|
-
}
|
|
813
|
-
resolve(hash.digest('hex'));
|
|
814
|
-
}
|
|
815
|
-
catch (error) {
|
|
816
|
-
reject(error);
|
|
817
|
-
}
|
|
818
|
-
};
|
|
819
|
-
processChunks();
|
|
820
|
-
});
|
|
864
|
+
async function getFileHashFromPath(filePath) {
|
|
865
|
+
const hash = createHash('sha256');
|
|
866
|
+
for await (const chunk of createReadStream(filePath)) {
|
|
867
|
+
hash.update(chunk);
|
|
868
|
+
}
|
|
869
|
+
return hash.digest('hex');
|
|
821
870
|
}
|
|
822
871
|
/**
|
|
823
872
|
* Writes JSON data to a file with error handling
|
|
@@ -826,33 +875,36 @@ async function getFileHashFromFile(file) {
|
|
|
826
875
|
* @param logger - Logger object with log and warn methods
|
|
827
876
|
* @returns true if successful, false if an error occurred
|
|
828
877
|
*/
|
|
829
|
-
const writeJSONFile = (filePath, data, logger) => {
|
|
878
|
+
export const writeJSONFile = (filePath, data, logger) => {
|
|
830
879
|
try {
|
|
831
|
-
|
|
832
|
-
|
|
880
|
+
const directory = path.dirname(filePath);
|
|
881
|
+
if (directory !== '.') {
|
|
882
|
+
mkdirSync(directory, { recursive: true });
|
|
883
|
+
}
|
|
884
|
+
writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
885
|
+
logger.log(colors.dim('JSON output written to: ') + colors.highlight(path.resolve(filePath)));
|
|
833
886
|
}
|
|
834
887
|
catch (error) {
|
|
835
888
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
836
889
|
const isPermissionError = errorMessage.includes('EACCES') || errorMessage.includes('EPERM');
|
|
837
890
|
const isNoSuchFileError = errorMessage.includes('ENOENT');
|
|
838
|
-
logger.warn(
|
|
891
|
+
logger.warn(colors.error(`Failed to write JSON output to file: ${filePath}`));
|
|
839
892
|
if (isPermissionError) {
|
|
840
|
-
logger.warn(
|
|
841
|
-
logger.warn(
|
|
893
|
+
logger.warn(colors.dim(' Permission denied - check file/directory write permissions'));
|
|
894
|
+
logger.warn(colors.dim(' Try running with appropriate permissions or choose a different output location'));
|
|
842
895
|
}
|
|
843
896
|
else if (isNoSuchFileError) {
|
|
844
|
-
logger.warn(
|
|
897
|
+
logger.warn(colors.dim(' Directory does not exist - create the directory first or choose an existing path'));
|
|
845
898
|
}
|
|
846
|
-
logger.warn(
|
|
899
|
+
logger.warn(colors.dim(' Error details: ') + errorMessage);
|
|
847
900
|
}
|
|
848
901
|
};
|
|
849
|
-
exports.writeJSONFile = writeJSONFile;
|
|
850
902
|
/**
|
|
851
903
|
* Formats duration in seconds into a human readable string
|
|
852
904
|
* @param durationSeconds - Duration in seconds
|
|
853
905
|
* @returns Formatted duration string (e.g. "2m 30s" or "45s")
|
|
854
906
|
*/
|
|
855
|
-
const formatDurationSeconds = (durationSeconds) => {
|
|
907
|
+
export const formatDurationSeconds = (durationSeconds) => {
|
|
856
908
|
const minutes = Math.floor(durationSeconds / 60);
|
|
857
909
|
const seconds = durationSeconds % 60;
|
|
858
910
|
if (minutes > 0) {
|
|
@@ -860,4 +912,3 @@ const formatDurationSeconds = (durationSeconds) => {
|
|
|
860
912
|
}
|
|
861
913
|
return `${durationSeconds}s`;
|
|
862
914
|
};
|
|
863
|
-
exports.formatDurationSeconds = formatDurationSeconds;
|