@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
JavaScript
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 };