@interopio/desktop-cli
Version:
CLI tool for setting up, building and packaging io.Connect Desktop projects
410 lines • 15.9 kB
JavaScript
"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