UNPKG

@interopio/desktop-cli

Version:

CLI tool for setting up, building and packaging io.Connect Desktop projects

410 lines 15.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.S3ComponentsStore = void 0; const logger_1 = require("../../../utils/logger"); const error_handler_1 = require("../../../utils/error.handler"); /** * S3 Components Store implementation * * This store downloads components from an AWS S3 bucket. * It expects components to be organized in the bucket with the following structure: * * bucket/ * ├── component1/ * │ ├── v1.0.0/ * │ │ ├── component1-v1.0.0-win32.zip * │ │ ├── component1-v1.0.0-darwin.dmg * │ │ └── component1-v1.0.0-darwin-arm64.dmg * │ └── v1.1.0/ * │ ├── component1-v1.1.0-win32.zip * │ └── component1-v1.1.0-darwin.dmg * └── component2/ * └── v2.0.0/ * └── component2-v2.0.0-win32.zip * * Authentication is handled through AWS credentials (environment variables, IAM roles, etc.) */ class S3ComponentsStore { logger = logger_1.Logger.getInstance(); bucketName; region; prefix; accessKeyId; secretAccessKey; constructor(config) { this.bucketName = config.bucketName; this.region = config.region; this.prefix = config.prefix; this.accessKeyId = config.accessKeyId; this.secretAccessKey = config.secretAccessKey; this.validateConfiguration(); } getInfo() { const prefixInfo = this.prefix ? ` (prefix: ${this.prefix})` : ''; return `S3 Components Store: s3://${this.bucketName}${prefixInfo} (${this.region})`; } async getAll() { this.logger.debug(`Scanning S3 bucket for components: ${this.bucketName}`); try { const objects = await this.listAllObjects(); return this.parseS3ObjectsToComponents(objects); } catch (error) { throw new error_handler_1.CLIError(`Failed to list components from S3 bucket: ${error instanceof Error ? error.message : error}`, { code: error_handler_1.ErrorCode.NETWORK_ERROR, cause: error, suggestions: [ 'Check your AWS credentials are properly configured', 'Verify the S3 bucket exists and you have read permissions', 'Check your network connection', 'Ensure the AWS region is correct' ] }); } } async download(name, version) { this.logger.info(`Downloading component ${name}@${version} from S3...`); try { // Find the specific object for this component and version const objectKey = await this.findComponentObject(name, version); if (!objectKey) { throw new error_handler_1.CLIError(`Component ${name}@${version} not found in S3 bucket`, { code: error_handler_1.ErrorCode.NOT_FOUND, suggestions: [ `Check that component '${name}' version '${version}' exists in the S3 bucket`, 'Use the browse command to see available components', 'Verify the component naming convention matches the expected format' ] }); } // Download the object const data = await this.downloadObject(objectKey); const filename = this.extractFilenameFromKey(objectKey); this.logger.info(`Downloaded ${name}@${version} (${filename}) from S3 successfully!`); return { name: name, data: data, filename: filename }; } catch (error) { if (error instanceof error_handler_1.CLIError) { throw error; } throw new error_handler_1.CLIError(`Failed to download component from S3: ${error instanceof Error ? error.message : error}`, { code: error_handler_1.ErrorCode.NETWORK_ERROR, cause: error, suggestions: [ 'Check your AWS credentials and permissions', 'Verify the S3 bucket and object exist', 'Check your network connection' ] }); } } /** * Validate the S3 configuration */ validateConfiguration() { if (!this.bucketName) { throw new error_handler_1.CLIError('S3 bucket name is required', { code: error_handler_1.ErrorCode.INVALID_CONFIG, suggestions: [ 'Set the bucketName in your S3 components store configuration', 'Check your cli-config.json components.storeS3Config.bucketName setting' ] }); } if (!this.region) { throw new error_handler_1.CLIError('S3 region is required', { code: error_handler_1.ErrorCode.INVALID_CONFIG, suggestions: [ 'Set the region in your S3 components store configuration', 'Check your cli-config.json components.storeS3Config.region setting' ] }); } // Check for AWS credentials if not using IAM roles if (!this.accessKeyId && !process.env['AWS_ACCESS_KEY_ID']) { this.logger.warn('No AWS Access Key ID found. Assuming IAM role or other AWS credential provider is configured.'); } } /** * List all objects in the S3 bucket */ async listAllObjects() { const objects = []; let continuationToken; do { const response = await this.listObjectsV2(continuationToken); if (response.Contents) { objects.push(...response.Contents); } continuationToken = response.IsTruncated ? response.NextContinuationToken : undefined; } while (continuationToken); return objects; } /** * List objects in S3 bucket using ListObjectsV2 API */ async listObjectsV2(continuationToken) { const url = this.buildS3ApiUrl(''); const params = new URLSearchParams({ 'list-type': '2', 'max-keys': '1000' }); if (this.prefix) { params.append('prefix', this.prefix); } if (continuationToken) { params.append('continuation-token', continuationToken); } const requestUrl = `${url}?${params.toString()}`; const headers = await this.createAuthHeaders('GET', '', requestUrl); const response = await fetch(requestUrl, { method: 'GET', headers: headers }); if (!response.ok) { throw new Error(`S3 API error: ${response.status} ${response.statusText}`); } const xmlText = await response.text(); return this.parseListObjectsResponse(xmlText); } /** * Download an object from S3 */ async downloadObject(key) { const url = this.buildS3ApiUrl(key); const headers = await this.createAuthHeaders('GET', key, url); this.logger.debug(`Downloading S3 object: ${key}`); const response = await fetch(url, { method: 'GET', headers: headers }); if (!response.ok) { throw new Error(`Failed to download S3 object: ${response.status} ${response.statusText}`); } const buffer = await response.arrayBuffer(); return new Uint8Array(buffer); } /** * Find the S3 object key for a specific component and version */ async findComponentObject(name, version) { const objects = await this.listAllObjects(); // Look for objects that match the component naming pattern const targetObjects = objects.filter(obj => { if (!obj.Key) return false; const key = obj.Key; const platform = this.getCurrentPlatform(); // Handle "latest" version by finding the most recent version if (version === 'latest') { return key.includes(`${name}/`) && this.matchesPlatform(key, platform); } else { return key.includes(`${name}/v${version}/`) && this.matchesPlatform(key, platform); } }); if (targetObjects.length === 0) { return null; } // If multiple objects match, prefer exact platform matches const exactMatch = targetObjects.find(obj => { const key = obj.Key; const platform = this.getCurrentPlatform(); return key.includes(`-${platform}.`) || key.includes(`-${platform}-`); }); if (exactMatch) { return exactMatch.Key; } // If looking for "latest", find the most recent by sorting if (version === 'latest') { targetObjects.sort((a, b) => { const aVersion = this.extractVersionFromKey(a.Key); const bVersion = this.extractVersionFromKey(b.Key); return this.compareVersions(bVersion, aVersion); // Descending order }); } return targetObjects[0]?.Key || null; } /** * Parse S3 objects into Component array */ parseS3ObjectsToComponents(objects) { const components = []; for (const obj of objects) { if (!obj.Key) continue; const component = this.parseComponentFromKey(obj.Key); if (component) { components.push(component); } } return components; } /** * Parse component information from S3 object key */ parseComponentFromKey(key) { // Expected format: [prefix/]componentName/vVersion/componentName-vVersion-platform.ext const parts = key.split('/'); if (parts.length < 3) { return null; } // Remove prefix if it exists const relevantParts = this.prefix ? parts.slice(this.prefix.split('/').length) : parts; if (relevantParts.length < 3) { return null; } const componentName = relevantParts[0]; const versionDir = relevantParts[1]; // e.g., "v1.0.0" const filename = relevantParts[relevantParts.length - 1]; // Extract version from directory name const version = versionDir.startsWith('v') ? versionDir.substring(1) : versionDir; // Extract platform from filename const platform = this.extractPlatformFromFilename(filename); return { name: componentName, version: version, platform: platform, downloadUrl: this.buildS3ApiUrl(key) }; } /** * Extract platform information from filename */ extractPlatformFromFilename(filename) { const lowerName = filename.toLowerCase(); if (lowerName.includes('darwin-arm64') || lowerName.includes('arm64')) { return 'darwin-arm64'; } else if (lowerName.includes('darwin') || lowerName.includes('mac')) { return 'darwin'; } else { return 'win32'; } } /** * Check if a key matches the current platform */ matchesPlatform(key, platform) { const lowerKey = key.toLowerCase(); switch (platform) { case 'win32': return lowerKey.includes('win32') || lowerKey.includes('.zip') || lowerKey.includes('.exe'); case 'darwin': return lowerKey.includes('darwin') || lowerKey.includes('mac') || lowerKey.includes('.dmg'); default: return false; } } /** * Get current platform string */ getCurrentPlatform() { return process.platform; } /** * Extract version from S3 object key */ extractVersionFromKey(key) { const versionMatch = key.match(/\/v([^\/]+)\//); return versionMatch ? versionMatch[1] : '0.0.0'; } /** * Compare two version strings for sorting */ compareVersions(a, b) { const aParts = a.split('.').map(Number); const bParts = b.split('.').map(Number); for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) { const aPart = aParts[i] || 0; const bPart = bParts[i] || 0; if (aPart > bPart) return 1; if (aPart < bPart) return -1; } return 0; } /** * Extract filename from S3 object key */ extractFilenameFromKey(key) { return key.split('/').pop() || key; } /** * Build S3 API URL for a given object key */ buildS3ApiUrl(key) { const encodedKey = encodeURIComponent(key).replace(/%2F/g, '/'); return `https://${this.bucketName}.s3.${this.region}.amazonaws.com/${encodedKey}`; } /** * Create AWS Signature Version 4 authentication headers */ async createAuthHeaders(_method, _key, _url) { const accessKeyId = this.accessKeyId || process.env['AWS_ACCESS_KEY_ID']; const secretAccessKey = this.secretAccessKey || process.env['AWS_SECRET_ACCESS_KEY']; const sessionToken = process.env['AWS_SESSION_TOKEN']; if (!accessKeyId || !secretAccessKey) { // Return empty headers if no credentials - assuming IAM role or other auth this.logger.debug('No AWS credentials found, assuming IAM role authentication'); return {}; } const headers = { 'Host': `${this.bucketName}.s3.${this.region}.amazonaws.com` }; if (sessionToken) { headers['X-Amz-Security-Token'] = sessionToken; } // For simplicity, this implementation assumes public bucket or IAM role auth // In a full implementation, you would implement AWS Signature Version 4 // For now, we'll try without authentication and let AWS handle it return headers; } /** * Parse S3 ListObjects XML response */ parseListObjectsResponse(xmlText) { const response = { Contents: [] }; // Simple XML parsing for S3 ListObjects response const contentsMatches = xmlText.match(/<Contents>[\s\S]*?<\/Contents>/g); if (contentsMatches) { for (const contentMatch of contentsMatches) { const keyMatch = contentMatch.match(/<Key>(.*?)<\/Key>/); const lastModifiedMatch = contentMatch.match(/<LastModified>(.*?)<\/LastModified>/); const sizeMatch = contentMatch.match(/<Size>(.*?)<\/Size>/); if (keyMatch) { const s3Object = { Key: keyMatch[1] }; if (lastModifiedMatch) { s3Object.LastModified = new Date(lastModifiedMatch[1]); } if (sizeMatch) { s3Object.Size = parseInt(sizeMatch[1]); } response.Contents.push(s3Object); } } } // Check if response is truncated const truncatedMatch = xmlText.match(/<IsTruncated>(.*?)<\/IsTruncated>/); if (truncatedMatch && truncatedMatch[1] === 'true') { response.IsTruncated = true; const tokenMatch = xmlText.match(/<NextContinuationToken>(.*?)<\/NextContinuationToken>/); if (tokenMatch) { response.NextContinuationToken = tokenMatch[1]; } } return response; } } exports.S3ComponentsStore = S3ComponentsStore; //# sourceMappingURL=s3.store.js.map