@interopio/desktop-cli
Version:
io.Connect Desktop Seed Repository CLI Tools
412 lines • 18.2 kB
JavaScript
;
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