UNPKG

@interopio/desktop-cli

Version:

io.Connect Desktop Seed Repository CLI Tools

412 lines 18.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ComponentDownloader = void 0; const fs_extra_1 = __importDefault(require("fs-extra")); const path_1 = __importDefault(require("path")); const os_1 = __importDefault(require("os")); const crypto_1 = __importDefault(require("crypto")); const axios_1 = __importDefault(require("axios")); const https_proxy_agent_1 = require("https-proxy-agent"); const http_proxy_agent_1 = require("http-proxy-agent"); const unzipper_1 = __importDefault(require("unzipper")); const utils_1 = require("../utils"); class ComponentDownloader { constructor(proxyConfig, sslConfig, cacheConfig) { this.proxyConfig = proxyConfig; this.sslConfig = sslConfig; // Properly merge cache config, only using defined values this.cacheConfig = { enabled: cacheConfig?.enabled ?? true, directory: cacheConfig?.directory ?? path_1.default.join(os_1.default.tmpdir(), 'iocd-cli-cache'), maxAge: cacheConfig?.maxAge ?? (24 * 60 * 60 * 1000), // 24 hours maxSize: cacheConfig?.maxSize ?? (1024 * 1024 * 1024), // 1GB }; this.cacheDir = this.cacheConfig.directory; this.httpClient = this.createHttpClient(); } createHttpClient() { const config = { timeout: 30000, // 30 seconds default maxContentLength: Infinity, maxBodyLength: Infinity, }; // Configure SSL if (this.sslConfig) { config.httpsAgent = new (require('https').Agent)({ rejectUnauthorized: this.sslConfig.rejectUnauthorized ?? true, ca: this.sslConfig.ca, cert: this.sslConfig.cert, key: this.sslConfig.key, }); } // Configure proxy if (this.proxyConfig) { const proxyUrl = this.buildProxyUrl(); if (this.proxyConfig.protocol === 'https') { config.httpsAgent = new https_proxy_agent_1.HttpsProxyAgent(proxyUrl); } else { config.httpAgent = new http_proxy_agent_1.HttpProxyAgent(proxyUrl); } utils_1.Logger.debug(`Using proxy: ${this.proxyConfig.protocol}://${this.proxyConfig.host}:${this.proxyConfig.port}`); } const client = axios_1.default.create(config); // Add request interceptor for logging client.interceptors.request.use((config) => { utils_1.Logger.debug(`HTTP Request: ${config.method?.toUpperCase()} ${config.url}`); return config; }); // Add response interceptor for error handling client.interceptors.response.use((response) => { utils_1.Logger.debug(`HTTP Response: ${response.status} ${response.statusText}`); return response; }, (error) => { if (error.response) { utils_1.Logger.error(`HTTP Error: ${error.response.status} ${error.response.statusText}`); } else if (error.request) { utils_1.Logger.error(`Network Error: ${error.message}`); } else { utils_1.Logger.error(`Request Error: ${error.message}`); } return Promise.reject(error); }); return client; } buildProxyUrl() { if (!this.proxyConfig) { throw new Error('Proxy configuration is required'); } const { protocol, host, port, auth } = this.proxyConfig; let url = `${protocol}://`; if (auth) { url += `${encodeURIComponent(auth.username)}:${encodeURIComponent(auth.password)}@`; } url += `${host}:${port}`; return url; } async downloadComponent(options) { const { url, targetPath, filename, useCache = this.cacheConfig.enabled, maxRetries = 3, timeout = 300000, // 5 minutes headers = {} } = options; const finalFilename = filename || this.extractFilenameFromUrl(url); const finalPath = path_1.default.join(targetPath, finalFilename); utils_1.Logger.info(`Downloading component from: ${url}`); utils_1.Logger.info(`Target path: ${finalPath}`); // Check cache first if (useCache) { const cachedFile = await this.getCachedFile(url); if (cachedFile) { utils_1.Logger.info('Using cached component'); await utils_1.FileUtils.ensureDir(targetPath); await fs_extra_1.default.copy(cachedFile, finalPath); return finalPath; } } // Ensure target directory exists await utils_1.FileUtils.ensureDir(targetPath); let lastError = null; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { utils_1.Logger.info(`Download attempt ${attempt}/${maxRetries}`); const response = await this.httpClient({ method: 'GET', url, responseType: 'stream', timeout, headers: { 'User-Agent': 'iocd-cli/10.0.0', ...headers } }); // Create write stream const writer = fs_extra_1.default.createWriteStream(finalPath); response.data.pipe(writer); // Track progress const totalLength = parseInt(response.headers['content-length'] || '0', 10); let downloadedLength = 0; response.data.on('data', (chunk) => { downloadedLength += chunk.length; if (totalLength > 0) { const percent = Math.round((downloadedLength / totalLength) * 100); if (percent % 10 === 0) { utils_1.Logger.debug(`Download progress: ${percent}%`); } } }); // Wait for download to complete await new Promise((resolve, reject) => { writer.on('finish', resolve); writer.on('error', reject); response.data.on('error', reject); }); utils_1.Logger.success(`Component downloaded successfully: ${finalPath}`); // Cache the downloaded file if (useCache) { await this.cacheFile(url, finalPath); } return finalPath; } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); utils_1.Logger.warning(`Download attempt ${attempt} failed: ${lastError.message}`); if (attempt < maxRetries) { const delay = Math.pow(2, attempt) * 1000; // Exponential backoff utils_1.Logger.info(`Retrying in ${delay}ms...`); await new Promise(resolve => setTimeout(resolve, delay)); } // Clean up partial download if (await utils_1.FileUtils.exists(finalPath)) { await fs_extra_1.default.unlink(finalPath); } } } throw new Error(`Failed to download component after ${maxRetries} attempts: ${lastError?.message}`); } async extractComponent(archivePath, extractPath) { utils_1.Logger.info(`Extracting component: ${archivePath} -> ${extractPath}`); await utils_1.FileUtils.ensureDir(extractPath); const fileExtension = path_1.default.extname(archivePath).toLowerCase(); try { if (fileExtension === '.zip') { await this.extractZip(archivePath, extractPath); } else if (fileExtension === '.tar' || fileExtension === '.gz') { await this.extractTar(archivePath, extractPath); } else { throw new Error(`Unsupported archive format: ${fileExtension}`); } utils_1.Logger.success(`Component extracted successfully to: ${extractPath}`); } catch (error) { utils_1.Logger.error(`Failed to extract component: ${error instanceof Error ? error.message : String(error)}`); throw error; } } async extractZip(archivePath, extractPath) { return new Promise((resolve, reject) => { fs_extra_1.default.createReadStream(archivePath) .pipe(unzipper_1.default.Extract({ path: extractPath })) .on('close', resolve) .on('error', reject); }); } async extractTar(archivePath, extractPath) { // Import tar dynamically to avoid bundling if not needed const tar = await Promise.resolve().then(() => __importStar(require('tar'))); await tar.extract({ file: archivePath, cwd: extractPath, strip: 1 // Remove top-level directory }); } extractFilenameFromUrl(url) { try { const urlObj = new URL(url); const pathname = urlObj.pathname; const filename = path_1.default.basename(pathname); return filename || 'component.zip'; } catch { return 'component.zip'; } } getCacheKey(url) { return crypto_1.default.createHash('sha256').update(url).digest('hex'); } async getCachedFile(url) { if (!this.cacheConfig.enabled) { return null; } const cacheKey = this.getCacheKey(url); const cachedFilePath = path_1.default.join(this.cacheDir, cacheKey); const metadataPath = `${cachedFilePath}.meta`; try { // Check if cached file exists if (!(await utils_1.FileUtils.exists(cachedFilePath)) || !(await utils_1.FileUtils.exists(metadataPath))) { return null; } // Check cache age const metadata = await fs_extra_1.default.readJson(metadataPath); const age = Date.now() - metadata.timestamp; if (age > this.cacheConfig.maxAge) { utils_1.Logger.debug('Cache entry expired, removing'); await this.removeCacheEntry(cacheKey); return null; } utils_1.Logger.debug('Cache hit for URL'); return cachedFilePath; } catch (error) { utils_1.Logger.debug(`Cache check failed: ${error instanceof Error ? error.message : String(error)}`); return null; } } async cacheFile(url, filePath) { if (!this.cacheConfig.enabled) { return; } try { await utils_1.FileUtils.ensureDir(this.cacheDir); const cacheKey = this.getCacheKey(url); const cachedFilePath = path_1.default.join(this.cacheDir, cacheKey); const metadataPath = `${cachedFilePath}.meta`; // Copy file to cache await fs_extra_1.default.copy(filePath, cachedFilePath); // Create metadata const metadata = { url, timestamp: Date.now(), size: (await fs_extra_1.default.stat(filePath)).size }; await fs_extra_1.default.writeJson(metadataPath, metadata); utils_1.Logger.debug('File cached successfully'); // Clean up old cache entries if needed await this.cleanupCache(); } catch (error) { utils_1.Logger.warning(`Failed to cache file: ${error instanceof Error ? error.message : String(error)}`); } } async removeCacheEntry(cacheKey) { const cachedFilePath = path_1.default.join(this.cacheDir, cacheKey); const metadataPath = `${cachedFilePath}.meta`; try { if (await utils_1.FileUtils.exists(cachedFilePath)) { await fs_extra_1.default.unlink(cachedFilePath); } if (await utils_1.FileUtils.exists(metadataPath)) { await fs_extra_1.default.unlink(metadataPath); } } catch (error) { utils_1.Logger.debug(`Failed to remove cache entry: ${error instanceof Error ? error.message : String(error)}`); } } async cleanupCache() { try { if (!(await utils_1.FileUtils.exists(this.cacheDir))) { return; } const files = await fs_extra_1.default.readdir(this.cacheDir); const cacheEntries = []; // Collect cache entries with metadata for (const file of files) { if (file.endsWith('.meta')) { continue; } const metadataPath = path_1.default.join(this.cacheDir, `${file}.meta`); if (await utils_1.FileUtils.exists(metadataPath)) { try { const metadata = await fs_extra_1.default.readJson(metadataPath); const filePath = path_1.default.join(this.cacheDir, file); const stats = await fs_extra_1.default.stat(filePath); cacheEntries.push({ key: file, path: filePath, size: stats.size, timestamp: metadata.timestamp }); } catch { // Skip invalid cache entries } } } // Calculate total cache size const totalSize = cacheEntries.reduce((sum, entry) => sum + entry.size, 0); if (totalSize > this.cacheConfig.maxSize) { utils_1.Logger.debug('Cache size exceeded, cleaning up old entries'); // Sort by timestamp (oldest first) cacheEntries.sort((a, b) => a.timestamp - b.timestamp); let sizeToFree = totalSize - (this.cacheConfig.maxSize * 0.8); // Free to 80% of max size for (const entry of cacheEntries) { if (sizeToFree <= 0) { break; } await this.removeCacheEntry(entry.key); sizeToFree -= entry.size; } } } catch (error) { utils_1.Logger.debug(`Cache cleanup failed: ${error instanceof Error ? error.message : String(error)}`); } } async clearCache() { try { if (await utils_1.FileUtils.exists(this.cacheDir)) { await fs_extra_1.default.remove(this.cacheDir); utils_1.Logger.info('Cache cleared successfully'); } } catch (error) { utils_1.Logger.error(`Failed to clear cache: ${error instanceof Error ? error.message : String(error)}`); throw error; } } static fromEnvironment() { // Read configuration from environment variables const proxyConfig = process.env.HTTP_PROXY || process.env.HTTPS_PROXY ? { host: process.env.PROXY_HOST || 'localhost', port: parseInt(process.env.PROXY_PORT || '8080', 10), protocol: process.env.PROXY_PROTOCOL || 'http', auth: process.env.PROXY_USERNAME ? { username: process.env.PROXY_USERNAME, password: process.env.PROXY_PASSWORD || '' } : undefined } : undefined; const sslConfig = { rejectUnauthorized: process.env.NODE_TLS_REJECT_UNAUTHORIZED !== '0', ca: process.env.SSL_CA ? fs_extra_1.default.readFileSync(process.env.SSL_CA) : undefined, cert: process.env.SSL_CERT ? fs_extra_1.default.readFileSync(process.env.SSL_CERT) : undefined, key: process.env.SSL_KEY ? fs_extra_1.default.readFileSync(process.env.SSL_KEY) : undefined, }; const cacheConfig = { enabled: process.env.IOCD_CACHE_ENABLED !== 'false', directory: process.env.IOCD_CACHE_DIR, maxAge: process.env.IOCD_CACHE_MAX_AGE ? parseInt(process.env.IOCD_CACHE_MAX_AGE, 10) : undefined, maxSize: process.env.IOCD_CACHE_MAX_SIZE ? parseInt(process.env.IOCD_CACHE_MAX_SIZE, 10) : undefined, }; return new ComponentDownloader(proxyConfig, sslConfig, cacheConfig); } } exports.ComponentDownloader = ComponentDownloader; //# sourceMappingURL=component-downloader.js.map