UNPKG

@devicecloud.dev/dcd

Version:

Better cloud maestro testing

203 lines (202 loc) 8.03 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.formatDurationSeconds = exports.writeJSONFile = exports.uploadBinary = exports.verifyAppZip = exports.compressFilesFromRelativePath = exports.compressFolderToBlob = exports.toBuffer = void 0; const core_1 = require("@oclif/core"); const archiver = require("archiver"); const node_crypto_1 = require("node:crypto"); const node_fs_1 = require("node:fs"); const promises_1 = require("node:fs/promises"); const path = require("node:path"); const node_stream_1 = require("node:stream"); const StreamZip = require("node-stream-zip"); const api_gateway_1 = require("./gateways/api-gateway"); const supabase_gateway_1 = require("./gateways/supabase-gateway"); const metadata_extractor_service_1 = require("./services/metadata-extractor.service"); const mimeTypeLookupByExtension = { apk: 'application/vnd.android.package-archive', yaml: 'application/x-yaml', zip: 'application/zip', }; const toBuffer = async (archive) => { const chunks = []; const writable = new node_stream_1.Writable(); writable._write = (chunk, _, callback) => { // save to array to concatenate later chunks.push(chunk); callback(); }; // pipe to writable archive.pipe(writable); await archive.finalize(); // once done, concatenate chunks return Buffer.concat(chunks); }; exports.toBuffer = toBuffer; const compressFolderToBlob = async (sourceDir) => { const archive = archiver('zip', { zlib: { level: 9 }, }); archive.on('error', (err) => { throw err; }); archive.directory(sourceDir, sourceDir.split('/').pop()); const buffer = await (0, exports.toBuffer)(archive); return new Blob([buffer], { type: 'application/zip' }); }; exports.compressFolderToBlob = compressFolderToBlob; const compressFilesFromRelativePath = async (basePath, files, commonRoot) => { const archive = archiver('zip', { zlib: { level: 9 }, }); archive.on('error', (err) => { throw err; }); for (const file of files) { archive.file(path.resolve(basePath, file), { name: file.replace(commonRoot, ''), }); } const buffer = await (0, exports.toBuffer)(archive); // await writeFile('./my-zip.zip', buffer); return buffer; }; exports.compressFilesFromRelativePath = compressFilesFromRelativePath; const verifyAppZip = async (zipPath) => { // eslint-disable-next-line import/namespace, new-cap const zip = await new StreamZip.async({ file: zipPath, storeEntries: true, }); const entries = await zip.entries(); const topLevelEntries = Object.values(entries).filter((entry) => !entry.name.split('/')[1]); if (topLevelEntries.length !== 1 || !topLevelEntries[0].name.endsWith('.app/')) { throw new Error('Zip file must contain exactly one entry which is a .app, check the contents of the zip file'); } zip.close(); }; exports.verifyAppZip = verifyAppZip; const uploadBinary = async (filePath, apiUrl, apiKey, ignoreShaCheck = false, log = true) => { if (log) { core_1.ux.action.start('Checking and uploading binary', 'Initializing', { stdout: true, }); } let file; if (filePath?.endsWith('.app')) { const zippedAppBlob = await (0, exports.compressFolderToBlob)(filePath); file = new File([zippedAppBlob], filePath + '.zip'); } else { const fileBuffer = await (0, promises_1.readFile)(filePath); const binaryBlob = new Blob([new Uint8Array(fileBuffer)], { type: mimeTypeLookupByExtension[filePath.split('.').pop()], }); file = new File([binaryBlob], filePath); } let sha; try { sha = await getFileHashFromFile(file); } catch (error) { if (log) { console.warn('Warning: Failed to get file hash', error); } } if (!ignoreShaCheck && sha) { try { const { appBinaryId, exists } = await api_gateway_1.ApiGateway.checkForExistingUpload(apiUrl, apiKey, sha); if (exists) { if (log) { core_1.ux.info(`sha hash matches existing binary with id: ${appBinaryId}, skipping upload. Force upload with --ignore-sha-check`); core_1.ux.action.stop(`Skipping upload.`); } return appBinaryId; } } catch { // ignore error } } const { id, message, path, token } = await api_gateway_1.ApiGateway.getBinaryUploadUrl(apiUrl, apiKey, filePath?.endsWith('.apk') ? 'android' : 'ios'); if (!path) throw new Error(message); // Extract app metadata using the service const metadataExtractor = new metadata_extractor_service_1.MetadataExtractorService(); const metadata = await metadataExtractor.extract(filePath); if (!metadata) { throw new Error(`Failed to extract metadata from ${filePath}. Supported formats: .apk, .app, .zip`); } const env = apiUrl === 'https://api.devicecloud.dev' ? 'prod' : 'dev'; await supabase_gateway_1.SupabaseGateway.uploadToSignedUrl(env, path, token, file); await api_gateway_1.ApiGateway.finaliseUpload(apiUrl, apiKey, id, metadata, path, sha); if (log) { core_1.ux.action.stop(`\nBinary uploaded with id: ${id}`); } return id; }; exports.uploadBinary = uploadBinary; async function getFileHashFromFile(file) { return new Promise((resolve, reject) => { const hash = (0, node_crypto_1.createHash)('sha256'); const stream = file.stream(); const reader = stream.getReader(); const processChunks = async () => { try { let readerResult = await reader.read(); while (!readerResult.done) { const { value } = readerResult; hash.update(value); readerResult = await reader.read(); } resolve(hash.digest('hex')); } catch (error) { reject(error); } }; processChunks(); }); } /** * Writes JSON data to a file with error handling * @param filePath - Path to the output JSON file * @param data - Data to be serialized to JSON * @param logger - Logger object with log and warn methods * @returns true if successful, false if an error occurred */ const writeJSONFile = (filePath, data, logger) => { try { (0, node_fs_1.writeFileSync)(filePath, JSON.stringify(data, null, 2)); logger.log(`JSON output written to: ${path.resolve(filePath)}`); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); const isPermissionError = errorMessage.includes('EACCES') || errorMessage.includes('EPERM'); const isNoSuchFileError = errorMessage.includes('ENOENT'); logger.warn(`Failed to write JSON output to file: ${filePath}`); if (isPermissionError) { logger.warn('Permission denied - check file/directory write permissions'); logger.warn('Try running with appropriate permissions or choose a different output location'); } else if (isNoSuchFileError) { logger.warn('Directory does not exist - create the directory first or choose an existing path'); } logger.warn(`Error details: ${errorMessage}`); } }; exports.writeJSONFile = writeJSONFile; /** * Formats duration in seconds into a human readable string * @param durationSeconds - Duration in seconds * @returns Formatted duration string (e.g. "2m 30s" or "45s") */ const formatDurationSeconds = (durationSeconds) => { const minutes = Math.floor(durationSeconds / 60); const seconds = durationSeconds % 60; if (minutes > 0) { return `${minutes}m ${seconds}s`; } return `${durationSeconds}s`; }; exports.formatDurationSeconds = formatDurationSeconds;