from __future__ import annotations import os import re import secrets import shutil import sys import warnings from dataclasses import dataclass from pathlib import Path from typing import Any, Dict, Optional import psutil def _calculate_auto_threads() -> int: cpu_count = psutil.cpu_count(logical=True) or 4 return max(1, min(cpu_count * 2, 64)) def _calculate_auto_connection_limit() -> int: available_mb = psutil.virtual_memory().available / (1024 * 1024) calculated = int(available_mb / 5) return max(20, min(calculated, 1000)) def _calculate_auto_backlog(connection_limit: int) -> int: return max(64, min(connection_limit * 2, 4096)) def _validate_rate_limit(value: str) -> str: pattern = r"^\d+\s+per\s+(second|minute|hour|day)$" if not re.match(pattern, value): raise ValueError(f"Invalid rate limit format: {value}. Expected format: '200 per minute'") return value if getattr(sys, "frozen", False): # Running in a PyInstaller bundle PROJECT_ROOT = Path(sys._MEIPASS) else: # Running in a normal Python environment PROJECT_ROOT = Path(__file__).resolve().parent.parent def _prepare_config_file(active_path: Path, legacy_path: Optional[Path] = None) -> Path: """Ensure config directories exist and migrate legacy files when possible.""" active_path = Path(active_path) active_path.parent.mkdir(parents=True, exist_ok=True) if legacy_path: legacy_path = Path(legacy_path) if not active_path.exists() and legacy_path.exists(): legacy_path.parent.mkdir(parents=True, exist_ok=True) try: shutil.move(str(legacy_path), str(active_path)) except OSError: shutil.copy2(legacy_path, active_path) try: legacy_path.unlink(missing_ok=True) except OSError: pass return active_path @dataclass class AppConfig: storage_root: Path max_upload_size: int ui_page_size: int secret_key: str iam_config_path: Path bucket_policy_path: Path api_base_url: Optional[str] aws_region: str aws_service: str ui_enforce_bucket_policies: bool log_level: str log_to_file: bool log_path: Path log_max_bytes: int log_backup_count: int ratelimit_default: str ratelimit_storage_uri: str ratelimit_list_buckets: str ratelimit_bucket_ops: str ratelimit_object_ops: str ratelimit_head_ops: str cors_origins: list[str] cors_methods: list[str] cors_allow_headers: list[str] cors_expose_headers: list[str] session_lifetime_days: int auth_max_attempts: int auth_lockout_minutes: int bulk_delete_max_keys: int secret_ttl_seconds: int stream_chunk_size: int multipart_min_part_size: int bucket_stats_cache_ttl: int object_cache_ttl: int encryption_enabled: bool encryption_master_key_path: Path kms_enabled: bool kms_keys_path: Path default_encryption_algorithm: str display_timezone: str lifecycle_enabled: bool lifecycle_interval_seconds: int metrics_history_enabled: bool metrics_history_retention_hours: int metrics_history_interval_minutes: int operation_metrics_enabled: bool operation_metrics_interval_minutes: int operation_metrics_retention_hours: int server_threads: int server_connection_limit: int server_backlog: int server_channel_timeout: int server_threads_auto: bool server_connection_limit_auto: bool server_backlog_auto: bool site_sync_enabled: bool site_sync_interval_seconds: int site_sync_batch_size: int sigv4_timestamp_tolerance_seconds: int presigned_url_min_expiry_seconds: int presigned_url_max_expiry_seconds: int replication_connect_timeout_seconds: int replication_read_timeout_seconds: int replication_max_retries: int replication_streaming_threshold_bytes: int replication_max_failures_per_bucket: int site_sync_connect_timeout_seconds: int site_sync_read_timeout_seconds: int site_sync_max_retries: int site_sync_clock_skew_tolerance_seconds: float object_key_max_length_bytes: int object_cache_max_size: int bucket_config_cache_ttl_seconds: float object_tag_limit: int encryption_chunk_size_bytes: int kms_generate_data_key_min_bytes: int kms_generate_data_key_max_bytes: int lifecycle_max_history_per_bucket: int site_id: Optional[str] site_endpoint: Optional[str] site_region: str site_priority: int ratelimit_admin: str num_trusted_proxies: int allowed_redirect_hosts: list[str] @classmethod def from_env(cls, overrides: Optional[Dict[str, Any]] = None) -> "AppConfig": overrides = overrides or {} def _get(name: str, default: Any) -> Any: return overrides.get(name, os.getenv(name, default)) storage_root = Path(_get("STORAGE_ROOT", PROJECT_ROOT / "data")).resolve() max_upload_size = int(_get("MAX_UPLOAD_SIZE", 1024 * 1024 * 1024)) ui_page_size = int(_get("UI_PAGE_SIZE", 100)) auth_max_attempts = int(_get("AUTH_MAX_ATTEMPTS", 5)) auth_lockout_minutes = int(_get("AUTH_LOCKOUT_MINUTES", 15)) bulk_delete_max_keys = int(_get("BULK_DELETE_MAX_KEYS", 500)) secret_ttl_seconds = int(_get("SECRET_TTL_SECONDS", 300)) stream_chunk_size = int(_get("STREAM_CHUNK_SIZE", 64 * 1024)) multipart_min_part_size = int(_get("MULTIPART_MIN_PART_SIZE", 5 * 1024 * 1024)) lifecycle_enabled = _get("LIFECYCLE_ENABLED", "false").lower() in ("true", "1", "yes") lifecycle_interval_seconds = int(_get("LIFECYCLE_INTERVAL_SECONDS", 3600)) default_secret = "dev-secret-key" secret_key = str(_get("SECRET_KEY", default_secret)) if not secret_key or secret_key == default_secret: secret_file = storage_root / ".myfsio.sys" / "config" / ".secret" if secret_file.exists(): secret_key = secret_file.read_text().strip() else: generated = secrets.token_urlsafe(32) if secret_key == default_secret: warnings.warn("Using insecure default SECRET_KEY. A random value has been generated and persisted; set SECRET_KEY for production", RuntimeWarning) try: secret_file.parent.mkdir(parents=True, exist_ok=True) secret_file.write_text(generated) try: os.chmod(secret_file, 0o600) except OSError: pass secret_key = generated except OSError: secret_key = generated iam_env_override = "IAM_CONFIG" in overrides or "IAM_CONFIG" in os.environ bucket_policy_override = "BUCKET_POLICY_PATH" in overrides or "BUCKET_POLICY_PATH" in os.environ default_iam_path = storage_root / ".myfsio.sys" / "config" / "iam.json" default_bucket_policy_path = storage_root / ".myfsio.sys" / "config" / "bucket_policies.json" iam_config_path = Path(_get("IAM_CONFIG", default_iam_path)).resolve() bucket_policy_path = Path(_get("BUCKET_POLICY_PATH", default_bucket_policy_path)).resolve() iam_config_path = _prepare_config_file( iam_config_path, legacy_path=None if iam_env_override else storage_root / "iam.json", ) bucket_policy_path = _prepare_config_file( bucket_policy_path, legacy_path=None if bucket_policy_override else storage_root / "bucket_policies.json", ) api_base_url = _get("API_BASE_URL", None) if api_base_url: api_base_url = str(api_base_url) aws_region = str(_get("AWS_REGION", "us-east-1")) aws_service = str(_get("AWS_SERVICE", "s3")) enforce_ui_policies = str(_get("UI_ENFORCE_BUCKET_POLICIES", "0")).lower() in {"1", "true", "yes", "on"} log_level = str(_get("LOG_LEVEL", "INFO")).upper() log_to_file = str(_get("LOG_TO_FILE", "1")).lower() in {"1", "true", "yes", "on"} log_dir = Path(_get("LOG_DIR", storage_root.parent / "logs")).resolve() log_dir.mkdir(parents=True, exist_ok=True) log_path = log_dir / str(_get("LOG_FILE", "app.log")) log_max_bytes = int(_get("LOG_MAX_BYTES", 5 * 1024 * 1024)) log_backup_count = int(_get("LOG_BACKUP_COUNT", 3)) ratelimit_default = _validate_rate_limit(str(_get("RATE_LIMIT_DEFAULT", "200 per minute"))) ratelimit_storage_uri = str(_get("RATE_LIMIT_STORAGE_URI", "memory://")) ratelimit_list_buckets = _validate_rate_limit(str(_get("RATE_LIMIT_LIST_BUCKETS", "60 per minute"))) ratelimit_bucket_ops = _validate_rate_limit(str(_get("RATE_LIMIT_BUCKET_OPS", "120 per minute"))) ratelimit_object_ops = _validate_rate_limit(str(_get("RATE_LIMIT_OBJECT_OPS", "240 per minute"))) ratelimit_head_ops = _validate_rate_limit(str(_get("RATE_LIMIT_HEAD_OPS", "100 per minute"))) def _csv(value: str, default: list[str]) -> list[str]: if not value: return default parts = [segment.strip() for segment in value.split(",") if segment.strip()] return parts or default cors_origins = _csv(str(_get("CORS_ORIGINS", "*")), ["*"]) cors_methods = _csv(str(_get("CORS_METHODS", "GET,PUT,POST,DELETE,OPTIONS,HEAD")), ["GET", "PUT", "POST", "DELETE", "OPTIONS", "HEAD"]) cors_allow_headers = _csv(str(_get("CORS_ALLOW_HEADERS", "*")), ["*"]) cors_expose_headers = _csv(str(_get("CORS_EXPOSE_HEADERS", "*")), ["*"]) session_lifetime_days = int(_get("SESSION_LIFETIME_DAYS", 30)) bucket_stats_cache_ttl = int(_get("BUCKET_STATS_CACHE_TTL", 60)) object_cache_ttl = int(_get("OBJECT_CACHE_TTL", 5)) encryption_enabled = str(_get("ENCRYPTION_ENABLED", "0")).lower() in {"1", "true", "yes", "on"} encryption_keys_dir = storage_root / ".myfsio.sys" / "keys" encryption_master_key_path = Path(_get("ENCRYPTION_MASTER_KEY_PATH", encryption_keys_dir / "master.key")).resolve() kms_enabled = str(_get("KMS_ENABLED", "0")).lower() in {"1", "true", "yes", "on"} kms_keys_path = Path(_get("KMS_KEYS_PATH", encryption_keys_dir / "kms_keys.json")).resolve() default_encryption_algorithm = str(_get("DEFAULT_ENCRYPTION_ALGORITHM", "AES256")) display_timezone = str(_get("DISPLAY_TIMEZONE", "UTC")) metrics_history_enabled = str(_get("METRICS_HISTORY_ENABLED", "0")).lower() in {"1", "true", "yes", "on"} metrics_history_retention_hours = int(_get("METRICS_HISTORY_RETENTION_HOURS", 24)) metrics_history_interval_minutes = int(_get("METRICS_HISTORY_INTERVAL_MINUTES", 5)) operation_metrics_enabled = str(_get("OPERATION_METRICS_ENABLED", "0")).lower() in {"1", "true", "yes", "on"} operation_metrics_interval_minutes = int(_get("OPERATION_METRICS_INTERVAL_MINUTES", 5)) operation_metrics_retention_hours = int(_get("OPERATION_METRICS_RETENTION_HOURS", 24)) _raw_threads = int(_get("SERVER_THREADS", 0)) if _raw_threads == 0: server_threads = _calculate_auto_threads() server_threads_auto = True else: server_threads = _raw_threads server_threads_auto = False _raw_conn_limit = int(_get("SERVER_CONNECTION_LIMIT", 0)) if _raw_conn_limit == 0: server_connection_limit = _calculate_auto_connection_limit() server_connection_limit_auto = True else: server_connection_limit = _raw_conn_limit server_connection_limit_auto = False _raw_backlog = int(_get("SERVER_BACKLOG", 0)) if _raw_backlog == 0: server_backlog = _calculate_auto_backlog(server_connection_limit) server_backlog_auto = True else: server_backlog = _raw_backlog server_backlog_auto = False server_channel_timeout = int(_get("SERVER_CHANNEL_TIMEOUT", 120)) site_sync_enabled = str(_get("SITE_SYNC_ENABLED", "0")).lower() in {"1", "true", "yes", "on"} site_sync_interval_seconds = int(_get("SITE_SYNC_INTERVAL_SECONDS", 60)) site_sync_batch_size = int(_get("SITE_SYNC_BATCH_SIZE", 100)) sigv4_timestamp_tolerance_seconds = int(_get("SIGV4_TIMESTAMP_TOLERANCE_SECONDS", 900)) presigned_url_min_expiry_seconds = int(_get("PRESIGNED_URL_MIN_EXPIRY_SECONDS", 1)) presigned_url_max_expiry_seconds = int(_get("PRESIGNED_URL_MAX_EXPIRY_SECONDS", 604800)) replication_connect_timeout_seconds = int(_get("REPLICATION_CONNECT_TIMEOUT_SECONDS", 5)) replication_read_timeout_seconds = int(_get("REPLICATION_READ_TIMEOUT_SECONDS", 30)) replication_max_retries = int(_get("REPLICATION_MAX_RETRIES", 2)) replication_streaming_threshold_bytes = int(_get("REPLICATION_STREAMING_THRESHOLD_BYTES", 10 * 1024 * 1024)) replication_max_failures_per_bucket = int(_get("REPLICATION_MAX_FAILURES_PER_BUCKET", 50)) site_sync_connect_timeout_seconds = int(_get("SITE_SYNC_CONNECT_TIMEOUT_SECONDS", 10)) site_sync_read_timeout_seconds = int(_get("SITE_SYNC_READ_TIMEOUT_SECONDS", 120)) site_sync_max_retries = int(_get("SITE_SYNC_MAX_RETRIES", 2)) site_sync_clock_skew_tolerance_seconds = float(_get("SITE_SYNC_CLOCK_SKEW_TOLERANCE_SECONDS", 1.0)) object_key_max_length_bytes = int(_get("OBJECT_KEY_MAX_LENGTH_BYTES", 1024)) object_cache_max_size = int(_get("OBJECT_CACHE_MAX_SIZE", 100)) bucket_config_cache_ttl_seconds = float(_get("BUCKET_CONFIG_CACHE_TTL_SECONDS", 30.0)) object_tag_limit = int(_get("OBJECT_TAG_LIMIT", 50)) encryption_chunk_size_bytes = int(_get("ENCRYPTION_CHUNK_SIZE_BYTES", 64 * 1024)) kms_generate_data_key_min_bytes = int(_get("KMS_GENERATE_DATA_KEY_MIN_BYTES", 1)) kms_generate_data_key_max_bytes = int(_get("KMS_GENERATE_DATA_KEY_MAX_BYTES", 1024)) lifecycle_max_history_per_bucket = int(_get("LIFECYCLE_MAX_HISTORY_PER_BUCKET", 50)) site_id_raw = _get("SITE_ID", None) site_id = str(site_id_raw).strip() if site_id_raw else None site_endpoint_raw = _get("SITE_ENDPOINT", None) site_endpoint = str(site_endpoint_raw).strip() if site_endpoint_raw else None site_region = str(_get("SITE_REGION", "us-east-1")) site_priority = int(_get("SITE_PRIORITY", 100)) ratelimit_admin = _validate_rate_limit(str(_get("RATE_LIMIT_ADMIN", "60 per minute"))) num_trusted_proxies = int(_get("NUM_TRUSTED_PROXIES", 0)) allowed_redirect_hosts_raw = _get("ALLOWED_REDIRECT_HOSTS", "") allowed_redirect_hosts = [h.strip() for h in str(allowed_redirect_hosts_raw).split(",") if h.strip()] return cls(storage_root=storage_root, max_upload_size=max_upload_size, ui_page_size=ui_page_size, secret_key=secret_key, iam_config_path=iam_config_path, bucket_policy_path=bucket_policy_path, api_base_url=api_base_url, aws_region=aws_region, aws_service=aws_service, ui_enforce_bucket_policies=enforce_ui_policies, log_level=log_level, log_to_file=log_to_file, log_path=log_path, log_max_bytes=log_max_bytes, log_backup_count=log_backup_count, ratelimit_default=ratelimit_default, ratelimit_storage_uri=ratelimit_storage_uri, ratelimit_list_buckets=ratelimit_list_buckets, ratelimit_bucket_ops=ratelimit_bucket_ops, ratelimit_object_ops=ratelimit_object_ops, ratelimit_head_ops=ratelimit_head_ops, cors_origins=cors_origins, cors_methods=cors_methods, cors_allow_headers=cors_allow_headers, cors_expose_headers=cors_expose_headers, session_lifetime_days=session_lifetime_days, auth_max_attempts=auth_max_attempts, auth_lockout_minutes=auth_lockout_minutes, bulk_delete_max_keys=bulk_delete_max_keys, secret_ttl_seconds=secret_ttl_seconds, stream_chunk_size=stream_chunk_size, multipart_min_part_size=multipart_min_part_size, bucket_stats_cache_ttl=bucket_stats_cache_ttl, object_cache_ttl=object_cache_ttl, encryption_enabled=encryption_enabled, encryption_master_key_path=encryption_master_key_path, kms_enabled=kms_enabled, kms_keys_path=kms_keys_path, default_encryption_algorithm=default_encryption_algorithm, display_timezone=display_timezone, lifecycle_enabled=lifecycle_enabled, lifecycle_interval_seconds=lifecycle_interval_seconds, metrics_history_enabled=metrics_history_enabled, metrics_history_retention_hours=metrics_history_retention_hours, metrics_history_interval_minutes=metrics_history_interval_minutes, operation_metrics_enabled=operation_metrics_enabled, operation_metrics_interval_minutes=operation_metrics_interval_minutes, operation_metrics_retention_hours=operation_metrics_retention_hours, server_threads=server_threads, server_connection_limit=server_connection_limit, server_backlog=server_backlog, server_channel_timeout=server_channel_timeout, server_threads_auto=server_threads_auto, server_connection_limit_auto=server_connection_limit_auto, server_backlog_auto=server_backlog_auto, site_sync_enabled=site_sync_enabled, site_sync_interval_seconds=site_sync_interval_seconds, site_sync_batch_size=site_sync_batch_size, sigv4_timestamp_tolerance_seconds=sigv4_timestamp_tolerance_seconds, presigned_url_min_expiry_seconds=presigned_url_min_expiry_seconds, presigned_url_max_expiry_seconds=presigned_url_max_expiry_seconds, replication_connect_timeout_seconds=replication_connect_timeout_seconds, replication_read_timeout_seconds=replication_read_timeout_seconds, replication_max_retries=replication_max_retries, replication_streaming_threshold_bytes=replication_streaming_threshold_bytes, replication_max_failures_per_bucket=replication_max_failures_per_bucket, site_sync_connect_timeout_seconds=site_sync_connect_timeout_seconds, site_sync_read_timeout_seconds=site_sync_read_timeout_seconds, site_sync_max_retries=site_sync_max_retries, site_sync_clock_skew_tolerance_seconds=site_sync_clock_skew_tolerance_seconds, object_key_max_length_bytes=object_key_max_length_bytes, object_cache_max_size=object_cache_max_size, bucket_config_cache_ttl_seconds=bucket_config_cache_ttl_seconds, object_tag_limit=object_tag_limit, encryption_chunk_size_bytes=encryption_chunk_size_bytes, kms_generate_data_key_min_bytes=kms_generate_data_key_min_bytes, kms_generate_data_key_max_bytes=kms_generate_data_key_max_bytes, lifecycle_max_history_per_bucket=lifecycle_max_history_per_bucket, site_id=site_id, site_endpoint=site_endpoint, site_region=site_region, site_priority=site_priority, ratelimit_admin=ratelimit_admin, num_trusted_proxies=num_trusted_proxies, allowed_redirect_hosts=allowed_redirect_hosts) def validate_and_report(self) -> list[str]: """Validate configuration and return a list of warnings/issues. Call this at startup to detect potential misconfigurations before the application fully commits to running. """ issues = [] try: test_file = self.storage_root / ".write_test" test_file.touch() test_file.unlink() except (OSError, PermissionError) as e: issues.append(f"CRITICAL: STORAGE_ROOT '{self.storage_root}' is not writable: {e}") storage_str = str(self.storage_root).lower() if "/tmp" in storage_str or "\\temp" in storage_str or "appdata\\local\\temp" in storage_str: issues.append(f"WARNING: STORAGE_ROOT '{self.storage_root}' appears to be a temporary directory. Data may be lost on reboot!") try: self.iam_config_path.relative_to(self.storage_root) except ValueError: issues.append(f"WARNING: IAM_CONFIG '{self.iam_config_path}' is outside STORAGE_ROOT '{self.storage_root}'. Consider setting IAM_CONFIG explicitly or ensuring paths are aligned.") try: self.bucket_policy_path.relative_to(self.storage_root) except ValueError: issues.append(f"WARNING: BUCKET_POLICY_PATH '{self.bucket_policy_path}' is outside STORAGE_ROOT '{self.storage_root}'. Consider setting BUCKET_POLICY_PATH explicitly.") try: self.log_path.parent.mkdir(parents=True, exist_ok=True) test_log = self.log_path.parent / ".write_test" test_log.touch() test_log.unlink() except (OSError, PermissionError) as e: issues.append(f"WARNING: Log directory '{self.log_path.parent}' is not writable: {e}") log_str = str(self.log_path).lower() if "/tmp" in log_str or "\\temp" in log_str or "appdata\\local\\temp" in log_str: issues.append(f"WARNING: LOG_DIR '{self.log_path.parent}' appears to be a temporary directory. Logs may be lost on reboot!") if self.encryption_enabled: try: self.encryption_master_key_path.relative_to(self.storage_root) except ValueError: issues.append(f"WARNING: ENCRYPTION_MASTER_KEY_PATH '{self.encryption_master_key_path}' is outside STORAGE_ROOT. Ensure proper backup procedures.") if self.kms_enabled: try: self.kms_keys_path.relative_to(self.storage_root) except ValueError: issues.append(f"WARNING: KMS_KEYS_PATH '{self.kms_keys_path}' is outside STORAGE_ROOT. Ensure proper backup procedures.") if self.secret_key == "dev-secret-key": issues.append("WARNING: Using default SECRET_KEY. Set SECRET_KEY environment variable for production.") if "*" in self.cors_origins: issues.append("INFO: CORS_ORIGINS is set to '*'. Consider restricting to specific domains in production.") if not (1 <= self.server_threads <= 64): issues.append(f"CRITICAL: SERVER_THREADS={self.server_threads} is outside valid range (1-64). Server cannot start.") if not (10 <= self.server_connection_limit <= 1000): issues.append(f"CRITICAL: SERVER_CONNECTION_LIMIT={self.server_connection_limit} is outside valid range (10-1000). Server cannot start.") if not (64 <= self.server_backlog <= 4096): issues.append(f"CRITICAL: SERVER_BACKLOG={self.server_backlog} is outside valid range (64-4096). Server cannot start.") if not (10 <= self.server_channel_timeout <= 300): issues.append(f"CRITICAL: SERVER_CHANNEL_TIMEOUT={self.server_channel_timeout} is outside valid range (10-300). Server cannot start.") if sys.platform != "win32": try: import resource soft_limit, _ = resource.getrlimit(resource.RLIMIT_NOFILE) threshold = int(soft_limit * 0.8) if self.server_connection_limit > threshold: issues.append(f"WARNING: SERVER_CONNECTION_LIMIT={self.server_connection_limit} exceeds 80% of system file descriptor limit (soft={soft_limit}). Consider running 'ulimit -n {self.server_connection_limit + 100}'.") except (ImportError, OSError): pass try: import psutil available_mb = psutil.virtual_memory().available / (1024 * 1024) estimated_mb = self.server_threads * 50 if estimated_mb > available_mb * 0.5: issues.append(f"WARNING: SERVER_THREADS={self.server_threads} may require ~{estimated_mb}MB memory, exceeding 50% of available RAM ({int(available_mb)}MB).") except ImportError: pass return issues def print_startup_summary(self) -> None: """Print a summary of the configuration at startup.""" print("\n" + "=" * 60) print("MyFSIO Configuration Summary") print("=" * 60) print(f" STORAGE_ROOT: {self.storage_root}") print(f" IAM_CONFIG: {self.iam_config_path}") print(f" BUCKET_POLICY: {self.bucket_policy_path}") print(f" LOG_PATH: {self.log_path}") if self.api_base_url: print(f" API_BASE_URL: {self.api_base_url}") if self.encryption_enabled: print(f" ENCRYPTION: Enabled (Master key: {self.encryption_master_key_path})") if self.kms_enabled: print(f" KMS: Enabled (Keys: {self.kms_keys_path})") def _auto(flag: bool) -> str: return " (auto)" if flag else "" print(f" SERVER_THREADS: {self.server_threads}{_auto(self.server_threads_auto)}") print(f" CONNECTION_LIMIT: {self.server_connection_limit}{_auto(self.server_connection_limit_auto)}") print(f" BACKLOG: {self.server_backlog}{_auto(self.server_backlog_auto)}") print(f" CHANNEL_TIMEOUT: {self.server_channel_timeout}s") print("=" * 60) issues = self.validate_and_report() if issues: print("\nConfiguration Issues Detected:") for issue in issues: print(f" • {issue}") print() else: print(" ✓ Configuration validated successfully\n") def to_flask_config(self) -> Dict[str, Any]: return { "STORAGE_ROOT": str(self.storage_root), "MAX_CONTENT_LENGTH": self.max_upload_size, "UI_PAGE_SIZE": self.ui_page_size, "SECRET_KEY": self.secret_key, "IAM_CONFIG": str(self.iam_config_path), "BUCKET_POLICY_PATH": str(self.bucket_policy_path), "API_BASE_URL": self.api_base_url, "AWS_REGION": self.aws_region, "AWS_SERVICE": self.aws_service, "UI_ENFORCE_BUCKET_POLICIES": self.ui_enforce_bucket_policies, "AUTH_MAX_ATTEMPTS": self.auth_max_attempts, "AUTH_LOCKOUT_MINUTES": self.auth_lockout_minutes, "BULK_DELETE_MAX_KEYS": self.bulk_delete_max_keys, "SECRET_TTL_SECONDS": self.secret_ttl_seconds, "STREAM_CHUNK_SIZE": self.stream_chunk_size, "MULTIPART_MIN_PART_SIZE": self.multipart_min_part_size, "BUCKET_STATS_CACHE_TTL": self.bucket_stats_cache_ttl, "OBJECT_CACHE_TTL": self.object_cache_ttl, "LOG_LEVEL": self.log_level, "LOG_TO_FILE": self.log_to_file, "LOG_FILE": str(self.log_path), "LOG_MAX_BYTES": self.log_max_bytes, "LOG_BACKUP_COUNT": self.log_backup_count, "RATELIMIT_DEFAULT": self.ratelimit_default, "RATELIMIT_STORAGE_URI": self.ratelimit_storage_uri, "RATELIMIT_LIST_BUCKETS": self.ratelimit_list_buckets, "RATELIMIT_BUCKET_OPS": self.ratelimit_bucket_ops, "RATELIMIT_OBJECT_OPS": self.ratelimit_object_ops, "RATELIMIT_HEAD_OPS": self.ratelimit_head_ops, "CORS_ORIGINS": self.cors_origins, "CORS_METHODS": self.cors_methods, "CORS_ALLOW_HEADERS": self.cors_allow_headers, "CORS_EXPOSE_HEADERS": self.cors_expose_headers, "SESSION_LIFETIME_DAYS": self.session_lifetime_days, "ENCRYPTION_ENABLED": self.encryption_enabled, "ENCRYPTION_MASTER_KEY_PATH": str(self.encryption_master_key_path), "KMS_ENABLED": self.kms_enabled, "KMS_KEYS_PATH": str(self.kms_keys_path), "DEFAULT_ENCRYPTION_ALGORITHM": self.default_encryption_algorithm, "DISPLAY_TIMEZONE": self.display_timezone, "LIFECYCLE_ENABLED": self.lifecycle_enabled, "LIFECYCLE_INTERVAL_SECONDS": self.lifecycle_interval_seconds, "METRICS_HISTORY_ENABLED": self.metrics_history_enabled, "METRICS_HISTORY_RETENTION_HOURS": self.metrics_history_retention_hours, "METRICS_HISTORY_INTERVAL_MINUTES": self.metrics_history_interval_minutes, "OPERATION_METRICS_ENABLED": self.operation_metrics_enabled, "OPERATION_METRICS_INTERVAL_MINUTES": self.operation_metrics_interval_minutes, "OPERATION_METRICS_RETENTION_HOURS": self.operation_metrics_retention_hours, "SERVER_THREADS": self.server_threads, "SERVER_CONNECTION_LIMIT": self.server_connection_limit, "SERVER_BACKLOG": self.server_backlog, "SERVER_CHANNEL_TIMEOUT": self.server_channel_timeout, "SITE_SYNC_ENABLED": self.site_sync_enabled, "SITE_SYNC_INTERVAL_SECONDS": self.site_sync_interval_seconds, "SITE_SYNC_BATCH_SIZE": self.site_sync_batch_size, "SIGV4_TIMESTAMP_TOLERANCE_SECONDS": self.sigv4_timestamp_tolerance_seconds, "PRESIGNED_URL_MIN_EXPIRY_SECONDS": self.presigned_url_min_expiry_seconds, "PRESIGNED_URL_MAX_EXPIRY_SECONDS": self.presigned_url_max_expiry_seconds, "REPLICATION_CONNECT_TIMEOUT_SECONDS": self.replication_connect_timeout_seconds, "REPLICATION_READ_TIMEOUT_SECONDS": self.replication_read_timeout_seconds, "REPLICATION_MAX_RETRIES": self.replication_max_retries, "REPLICATION_STREAMING_THRESHOLD_BYTES": self.replication_streaming_threshold_bytes, "REPLICATION_MAX_FAILURES_PER_BUCKET": self.replication_max_failures_per_bucket, "SITE_SYNC_CONNECT_TIMEOUT_SECONDS": self.site_sync_connect_timeout_seconds, "SITE_SYNC_READ_TIMEOUT_SECONDS": self.site_sync_read_timeout_seconds, "SITE_SYNC_MAX_RETRIES": self.site_sync_max_retries, "SITE_SYNC_CLOCK_SKEW_TOLERANCE_SECONDS": self.site_sync_clock_skew_tolerance_seconds, "OBJECT_KEY_MAX_LENGTH_BYTES": self.object_key_max_length_bytes, "OBJECT_CACHE_MAX_SIZE": self.object_cache_max_size, "BUCKET_CONFIG_CACHE_TTL_SECONDS": self.bucket_config_cache_ttl_seconds, "OBJECT_TAG_LIMIT": self.object_tag_limit, "ENCRYPTION_CHUNK_SIZE_BYTES": self.encryption_chunk_size_bytes, "KMS_GENERATE_DATA_KEY_MIN_BYTES": self.kms_generate_data_key_min_bytes, "KMS_GENERATE_DATA_KEY_MAX_BYTES": self.kms_generate_data_key_max_bytes, "LIFECYCLE_MAX_HISTORY_PER_BUCKET": self.lifecycle_max_history_per_bucket, "SITE_ID": self.site_id, "SITE_ENDPOINT": self.site_endpoint, "SITE_REGION": self.site_region, "SITE_PRIORITY": self.site_priority, "RATE_LIMIT_ADMIN": self.ratelimit_admin, "NUM_TRUSTED_PROXIES": self.num_trusted_proxies, "ALLOWED_REDIRECT_HOSTS": self.allowed_redirect_hosts, }