UNPKG

@kadi.build/local-remote-file-manager-ability

Version:

Local & Remote File Management System with S3-compatible container registry, HTTP server provider, file streaming, comprehensive tunnel services (ngrok, serveo, localtunnel, localhost.run, pinggy), and comprehensive testing suite

485 lines (411 loc) 16.7 kB
import { promises as fs } from 'fs'; import path from 'path'; import os from 'os'; class ConfigManager { constructor() { this.config = {}; this.defaults = { // Local File System Configuration DEFAULT_LOCAL_ROOT: os.homedir(), DEFAULT_UPLOAD_DIRECTORY: './uploads', DEFAULT_DOWNLOAD_DIRECTORY: './downloads', DEFAULT_TEMP_DIRECTORY: './temp', // File Operation Configuration MAX_FILE_SIZE: '1073741824', // 1GB in bytes CHUNK_SIZE: '8388608', // 8MB chunks for large operations MAX_RETRY_ATTEMPTS: '3', TIMEOUT_MS: '30000', // 30 seconds // File Watching Configuration (Bucket 2) WATCH_ENABLED: 'true', WATCH_RECURSIVE: 'true', WATCH_IGNORE_DOTFILES: 'true', WATCH_DEBOUNCE_MS: '100', // Compression Configuration (Bucket 3) COMPRESSION_ENABLED: 'true', COMPRESSION_LEVEL: '6', COMPRESSION_FORMAT: 'zip', COMPRESSION_MAX_FILE_SIZE: '1073741824', // 1GB COMPRESSION_CHUNK_SIZE: '8388608', // 8MB COMPRESSION_ENABLE_PROGRESS_TRACKING: 'true', COMPRESSION_ENABLE_CHECKSUM_VERIFICATION: 'true', // Tunneling Configuration (Bucket 4) TUNNEL_SERVICE: 'ngrok', TUNNEL_FALLBACK_SERVICES: 'serveo,localtunnel', // Fallback services (comma-separated) TUNNEL_AUTH_TOKEN: '', // General auth token (fallback) TUNNEL_SUBDOMAIN: '', // Custom subdomain (when supported) TUNNEL_REGION: 'us', // Region preference TUNNEL_AUTO_FALLBACK: 'true', // Automatically try fallback services // Ngrok-specific Configuration NGROK_AUTH_TOKEN: '', // Ngrok auth token (preferred for ngrok) NGROK_REGION: 'us', // Ngrok region (us, eu, ap, au, sa, jp, in) NGROK_PROTOCOL: 'http', // Ngrok protocol (http, https, tcp) // Performance Configuration MAX_CONCURRENT_OPERATIONS: '5', ENABLE_PROGRESS_TRACKING: 'true', ENABLE_CHECKSUM_VERIFICATION: 'true', // Logging and Debug Configuration LOG_LEVEL: 'info', // error, warn, info, debug ENABLE_FILE_LOGGING: 'false', LOG_FILE_PATH: './logs/application.log', // Security Configuration ALLOW_SYMLINKS: 'false', RESTRICT_TO_BASE_PATH: 'true', MAX_PATH_LENGTH: '255' }; } async load() { // Load from .env file if it exists try { const envPath = path.join(process.cwd(), '.env'); const envContent = await fs.readFile(envPath, 'utf8'); this.parseEnvContent(envContent); } catch (error) { console.warn('⚠️ .env file not found, using environment variables and defaults'); } // Override with actual environment variables this.loadFromEnvironment(); // Apply defaults for missing values this.applyDefaults(); // Ensure required directories exist await this.ensureDirectories(); // Validate configuration const validation = this.validate(); if (!validation.isValid) { console.warn('⚠️ Configuration validation warnings:'); validation.errors.forEach(error => console.warn(` - ${error}`)); } } parseEnvContent(content) { const lines = content.split('\n'); for (const line of lines) { const trimmed = line.trim(); if (trimmed && !trimmed.startsWith('#')) { const [key, ...valueParts] = trimmed.split('='); if (key && valueParts.length > 0) { const value = valueParts.join('=').replace(/^["']|["']$/g, ''); this.config[key.trim()] = value; } } } } loadFromEnvironment() { for (const key of Object.keys(this.defaults)) { if (process.env[key]) { this.config[key] = process.env[key]; } } } applyDefaults() { for (const [key, defaultValue] of Object.entries(this.defaults)) { if (!this.config[key]) { this.config[key] = defaultValue; } } } async ensureDirectories() { const dirs = [ this.get('DEFAULT_UPLOAD_DIRECTORY'), this.get('DEFAULT_DOWNLOAD_DIRECTORY'), this.get('DEFAULT_TEMP_DIRECTORY') ]; for (const dir of dirs) { try { await fs.mkdir(dir, { recursive: true }); } catch (error) { if (error.code !== 'EEXIST') { console.warn(`⚠️ Could not create directory ${dir}: ${error.message}`); } } } } get(key) { return this.config[key]; } getNumber(key) { const value = this.get(key); const number = value ? parseInt(value, 10) : 0; return isNaN(number) ? 0 : number; } getBoolean(key) { const value = this.get(key); return value && ['true', '1', 'yes', 'on'].includes(value.toLowerCase()); } set(key, value) { if (value === undefined || value === null || value === 'undefined' || value === 'null') { console.warn(`⚠️ Attempted to set ${key} to invalid value: ${value}. Skipping.`); return false; } const stringValue = String(value).trim(); if (!stringValue && this.isRequiredKey(key)) { console.warn(`⚠️ Attempted to set required key ${key} to empty value. Skipping.`); return false; } this.config[key] = stringValue; return true; } isRequiredKey(key) { const requiredKeys = [ 'DEFAULT_LOCAL_ROOT', 'DEFAULT_UPLOAD_DIRECTORY', 'DEFAULT_DOWNLOAD_DIRECTORY' ]; return requiredKeys.includes(key); } has(key) { return key in this.config && this.config[key] !== ''; } validate() { const errors = []; const warnings = []; // Validate directory paths this.validateDirectoryPaths(errors, warnings); // Validate numeric configurations this.validateNumericConfig(errors, warnings); // Validate file size limits this.validateFileSizeLimits(errors, warnings); // Validate security settings this.validateSecuritySettings(errors, warnings); // Validate compression settings this.validateCompressionSettings(errors, warnings); return { isValid: errors.length === 0, errors: errors, warnings: warnings }; } validateDirectoryPaths(errors, warnings) { const directoryKeys = [ 'DEFAULT_LOCAL_ROOT', 'DEFAULT_UPLOAD_DIRECTORY', 'DEFAULT_DOWNLOAD_DIRECTORY', 'DEFAULT_TEMP_DIRECTORY' ]; for (const key of directoryKeys) { const dir = this.get(key); if (!dir) { errors.push(`${key} is required and cannot be empty`); continue; } // Check for invalid characters (basic validation) if (/[<>:"|?*]/.test(dir)) { errors.push(`${key} contains invalid characters: ${dir}`); } // Warn about absolute vs relative paths if (key === 'DEFAULT_LOCAL_ROOT' && !path.isAbsolute(dir)) { warnings.push(`${key} should be an absolute path for consistency`); } } } validateNumericConfig(errors, warnings) { const numericConfigs = [ 'MAX_FILE_SIZE', 'CHUNK_SIZE', 'MAX_RETRY_ATTEMPTS', 'TIMEOUT_MS', 'MAX_CONCURRENT_OPERATIONS', 'WATCH_DEBOUNCE_MS', 'COMPRESSION_LEVEL', 'COMPRESSION_MAX_FILE_SIZE', 'COMPRESSION_CHUNK_SIZE', 'MAX_PATH_LENGTH' ]; for (const config of numericConfigs) { const value = this.getNumber(config); if (value <= 0) { warnings.push(`${config} should be a positive number, got: ${this.get(config)}`); } } // Specific validations const chunkSize = this.getNumber('CHUNK_SIZE'); if (chunkSize < 1024) { // Less than 1KB warnings.push('CHUNK_SIZE should be at least 1KB for efficiency'); } if (chunkSize > 100 * 1024 * 1024) { // More than 100MB warnings.push('CHUNK_SIZE should not exceed 100MB to avoid memory issues'); } const compressionLevel = this.getNumber('COMPRESSION_LEVEL'); if (compressionLevel < 0 || compressionLevel > 9) { warnings.push('COMPRESSION_LEVEL should be between 0 and 9'); } } validateFileSizeLimits(errors, warnings) { const maxFileSize = this.getNumber('MAX_FILE_SIZE'); const chunkSize = this.getNumber('CHUNK_SIZE'); if (maxFileSize < chunkSize) { warnings.push('MAX_FILE_SIZE should be larger than CHUNK_SIZE'); } // Warn about very large file sizes if (maxFileSize > 10 * 1024 * 1024 * 1024) { // 10GB warnings.push('MAX_FILE_SIZE is very large (>10GB), ensure adequate disk space'); } // Validate compression file size limits const compressionMaxFileSize = this.getNumber('COMPRESSION_MAX_FILE_SIZE'); if (compressionMaxFileSize > maxFileSize) { warnings.push('COMPRESSION_MAX_FILE_SIZE should not exceed MAX_FILE_SIZE'); } } validateSecuritySettings(errors, warnings) { const maxPathLength = this.getNumber('MAX_PATH_LENGTH'); if (maxPathLength > 4096) { warnings.push('MAX_PATH_LENGTH is very large, may cause security issues'); } if (this.getBoolean('ALLOW_SYMLINKS')) { warnings.push('ALLOW_SYMLINKS is enabled, which may pose security risks'); } } validateCompressionSettings(errors, warnings) { const compressionFormat = this.get('COMPRESSION_FORMAT'); if (!['zip', 'tar.gz'].includes(compressionFormat)) { errors.push('COMPRESSION_FORMAT must be either "zip" or "tar.gz"'); } if (!this.getBoolean('COMPRESSION_ENABLED')) { warnings.push('Compression functionality is disabled'); } const compressionLevel = this.getNumber('COMPRESSION_LEVEL'); if (compressionLevel > 6) { warnings.push('High compression level (>6) may significantly impact performance'); } } getLocalConfig() { return { localRoot: this.get('DEFAULT_LOCAL_ROOT'), uploadDirectory: this.get('DEFAULT_UPLOAD_DIRECTORY'), downloadDirectory: this.get('DEFAULT_DOWNLOAD_DIRECTORY'), tempDirectory: this.get('DEFAULT_TEMP_DIRECTORY'), maxFileSize: this.getNumber('MAX_FILE_SIZE'), chunkSize: this.getNumber('CHUNK_SIZE'), allowSymlinks: this.getBoolean('ALLOW_SYMLINKS'), restrictToBasePath: this.getBoolean('RESTRICT_TO_BASE_PATH'), maxPathLength: this.getNumber('MAX_PATH_LENGTH') }; } getPerformanceConfig() { return { maxRetryAttempts: this.getNumber('MAX_RETRY_ATTEMPTS'), timeoutMs: this.getNumber('TIMEOUT_MS'), maxConcurrentOperations: this.getNumber('MAX_CONCURRENT_OPERATIONS'), chunkSize: this.getNumber('CHUNK_SIZE'), enableProgressTracking: this.getBoolean('ENABLE_PROGRESS_TRACKING'), enableChecksumVerification: this.getBoolean('ENABLE_CHECKSUM_VERIFICATION') }; } getWatchConfig() { return { enabled: this.getBoolean('WATCH_ENABLED'), recursive: this.getBoolean('WATCH_RECURSIVE'), ignoreDotfiles: this.getBoolean('WATCH_IGNORE_DOTFILES'), debounceMs: this.getNumber('WATCH_DEBOUNCE_MS'), localRoot: this.get('DEFAULT_LOCAL_ROOT') }; } getCompressionConfig() { return { enabled: this.getBoolean('COMPRESSION_ENABLED'), level: this.getNumber('COMPRESSION_LEVEL'), format: this.get('COMPRESSION_FORMAT'), maxFileSize: this.getNumber('COMPRESSION_MAX_FILE_SIZE'), chunkSize: this.getNumber('COMPRESSION_CHUNK_SIZE'), enableProgressTracking: this.getBoolean('COMPRESSION_ENABLE_PROGRESS_TRACKING'), enableChecksumVerification: this.getBoolean('COMPRESSION_ENABLE_CHECKSUM_VERIFICATION'), localRoot: this.get('DEFAULT_LOCAL_ROOT') }; } getTunnelConfig() { return { service: this.get('TUNNEL_SERVICE'), fallbackServices: this.get('TUNNEL_FALLBACK_SERVICES'), authToken: this.get('TUNNEL_AUTH_TOKEN'), subdomain: this.get('TUNNEL_SUBDOMAIN'), region: this.get('TUNNEL_REGION'), autoFallback: this.getBoolean('TUNNEL_AUTO_FALLBACK'), localRoot: this.get('DEFAULT_LOCAL_ROOT'), // Ngrok-specific configuration ngrokAuthToken: this.get('NGROK_AUTH_TOKEN'), ngrokRegion: this.get('NGROK_REGION'), ngrokProtocol: this.get('NGROK_PROTOCOL') }; } async save(filePath = '.env') { const envPath = path.isAbsolute(filePath) ? filePath : path.join(process.cwd(), filePath); try { const lines = []; lines.push('# Local & Remote File Manager Configuration'); lines.push('# Generated on ' + new Date().toISOString()); lines.push(''); lines.push('# ============================================================================='); lines.push('# LOCAL FILE SYSTEM CONFIGURATION'); lines.push('# ============================================================================='); lines.push(''); lines.push('# Directory Paths'); lines.push(`DEFAULT_LOCAL_ROOT=${this.get('DEFAULT_LOCAL_ROOT')}`); lines.push(`DEFAULT_UPLOAD_DIRECTORY=${this.get('DEFAULT_UPLOAD_DIRECTORY')}`); lines.push(`DEFAULT_DOWNLOAD_DIRECTORY=${this.get('DEFAULT_DOWNLOAD_DIRECTORY')}`); lines.push(`DEFAULT_TEMP_DIRECTORY=${this.get('DEFAULT_TEMP_DIRECTORY')}`); lines.push(''); lines.push('# File Operation Limits'); lines.push(`MAX_FILE_SIZE=${this.get('MAX_FILE_SIZE')}`); lines.push(`CHUNK_SIZE=${this.get('CHUNK_SIZE')}`); lines.push(`MAX_RETRY_ATTEMPTS=${this.get('MAX_RETRY_ATTEMPTS')}`); lines.push(`TIMEOUT_MS=${this.get('TIMEOUT_MS')}`); lines.push(''); lines.push('# Performance Settings'); lines.push(`MAX_CONCURRENT_OPERATIONS=${this.get('MAX_CONCURRENT_OPERATIONS')}`); lines.push(`ENABLE_PROGRESS_TRACKING=${this.get('ENABLE_PROGRESS_TRACKING')}`); lines.push(`ENABLE_CHECKSUM_VERIFICATION=${this.get('ENABLE_CHECKSUM_VERIFICATION')}`); lines.push(''); lines.push('# Security Settings'); lines.push(`ALLOW_SYMLINKS=${this.get('ALLOW_SYMLINKS')}`); lines.push(`RESTRICT_TO_BASE_PATH=${this.get('RESTRICT_TO_BASE_PATH')}`); lines.push(`MAX_PATH_LENGTH=${this.get('MAX_PATH_LENGTH')}`); lines.push(''); lines.push('# ============================================================================='); lines.push('# FEATURE CONFIGURATION'); lines.push('# ============================================================================='); lines.push(''); lines.push('# File Watching (Bucket 2)'); lines.push(`WATCH_ENABLED=${this.get('WATCH_ENABLED')}`); lines.push(`WATCH_RECURSIVE=${this.get('WATCH_RECURSIVE')}`); lines.push(`WATCH_IGNORE_DOTFILES=${this.get('WATCH_IGNORE_DOTFILES')}`); lines.push(`WATCH_DEBOUNCE_MS=${this.get('WATCH_DEBOUNCE_MS')}`); lines.push(''); lines.push('# Compression (Bucket 3)'); lines.push(`COMPRESSION_ENABLED=${this.get('COMPRESSION_ENABLED')}`); lines.push(`COMPRESSION_LEVEL=${this.get('COMPRESSION_LEVEL')}`); lines.push(`COMPRESSION_FORMAT=${this.get('COMPRESSION_FORMAT')}`); lines.push(`COMPRESSION_MAX_FILE_SIZE=${this.get('COMPRESSION_MAX_FILE_SIZE')}`); lines.push(`COMPRESSION_CHUNK_SIZE=${this.get('COMPRESSION_CHUNK_SIZE')}`); lines.push(`COMPRESSION_ENABLE_PROGRESS_TRACKING=${this.get('COMPRESSION_ENABLE_PROGRESS_TRACKING')}`); lines.push(`COMPRESSION_ENABLE_CHECKSUM_VERIFICATION=${this.get('COMPRESSION_ENABLE_CHECKSUM_VERIFICATION')}`); lines.push(''); lines.push('# Tunneling (Bucket 4)'); lines.push(`TUNNEL_SERVICE=${this.get('TUNNEL_SERVICE')}`); lines.push(`TUNNEL_AUTH_TOKEN=${this.get('TUNNEL_AUTH_TOKEN')}`); lines.push(`TUNNEL_SUBDOMAIN=${this.get('TUNNEL_SUBDOMAIN')}`); lines.push(`TUNNEL_REGION=${this.get('TUNNEL_REGION')}`); lines.push(''); lines.push('# Logging'); lines.push(`LOG_LEVEL=${this.get('LOG_LEVEL')}`); lines.push(`ENABLE_FILE_LOGGING=${this.get('ENABLE_FILE_LOGGING')}`); lines.push(`LOG_FILE_PATH=${this.get('LOG_FILE_PATH')}`); await fs.writeFile(envPath, lines.join('\n')); console.log('✅ Configuration saved to .env file'); } catch (error) { console.error(`❌ Failed to save configuration: ${error.message}`); throw error; } } getSummary() { return { local: this.getLocalConfig(), performance: this.getPerformanceConfig(), watch: this.getWatchConfig(), compression: this.getCompressionConfig(), tunnel: this.getTunnelConfig() }; } reset() { this.config = {}; this.applyDefaults(); } } export { ConfigManager };