"""Helper script to run the API server, UI server, or both.""" from __future__ import annotations import argparse import atexit import os import signal import subprocess import sys import warnings import multiprocessing from multiprocessing import Process from pathlib import Path from dotenv import load_dotenv for _env_file in [ Path("/opt/myfsio/myfsio.env"), Path.cwd() / ".env", Path.cwd() / "myfsio.env", ]: if _env_file.exists(): load_dotenv(_env_file, override=True) from typing import Optional from app import create_api_app, create_ui_app from app.config import AppConfig from app.iam import IamService, IamError, ALLOWED_ACTIONS, _derive_fernet_key from app.version import get_version def _server_host() -> str: """Return the bind host for API and UI servers.""" return os.getenv("APP_HOST", "0.0.0.0") def _is_debug_enabled() -> bool: return os.getenv("FLASK_DEBUG", "0").lower() in ("1", "true", "yes") def _is_frozen() -> bool: """Check if running as a compiled binary (PyInstaller/Nuitka).""" return getattr(sys, 'frozen', False) or '__compiled__' in globals() def _serve_granian(target: str, port: int, config: Optional[AppConfig] = None) -> None: from granian import Granian from granian.constants import Interfaces from granian.http import HTTP1Settings kwargs: dict = { "target": target, "address": _server_host(), "port": port, "interface": Interfaces.WSGI, "factory": True, "workers": 1, } if config: kwargs["blocking_threads"] = config.server_threads kwargs["backlog"] = config.server_backlog kwargs["backpressure"] = config.server_connection_limit kwargs["http1_settings"] = HTTP1Settings( header_read_timeout=config.server_channel_timeout * 1000, max_buffer_size=config.server_max_buffer_size, ) else: kwargs["http1_settings"] = HTTP1Settings( max_buffer_size=1024 * 1024 * 128, ) server = Granian(**kwargs) server.serve() def _find_rust_binary() -> Optional[Path]: candidates = [ Path(__file__).parent / "myfsio-engine" / "target" / "release" / "myfsio-server.exe", Path(__file__).parent / "myfsio-engine" / "target" / "release" / "myfsio-server", Path(__file__).parent / "myfsio-engine" / "target" / "debug" / "myfsio-server.exe", Path(__file__).parent / "myfsio-engine" / "target" / "debug" / "myfsio-server", ] for p in candidates: if p.exists(): return p return None def serve_rust_api(port: int, config: Optional[AppConfig] = None) -> None: binary = _find_rust_binary() if binary is None: print("ERROR: Rust engine binary not found. Build it first:") print(" cd myfsio-engine && cargo build --release") sys.exit(1) env = os.environ.copy() env["PORT"] = str(port) env["HOST"] = _server_host() if config: env["STORAGE_ROOT"] = str(config.storage_root) env["AWS_REGION"] = config.aws_region if config.secret_key: env["SECRET_KEY"] = config.secret_key env.setdefault("ENCRYPTION_ENABLED", str(config.encryption_enabled).lower()) env.setdefault("KMS_ENABLED", str(config.kms_enabled).lower()) env.setdefault("LIFECYCLE_ENABLED", str(config.lifecycle_enabled).lower()) env.setdefault("RUST_LOG", "info") print(f"Starting Rust S3 engine: {binary}") proc = subprocess.Popen([str(binary)], env=env) try: proc.wait() except KeyboardInterrupt: proc.terminate() proc.wait(timeout=5) def serve_api(port: int, prod: bool = False, config: Optional[AppConfig] = None) -> None: if prod: _serve_granian("app:create_api_app", port, config) else: app = create_api_app() debug = _is_debug_enabled() if debug: warnings.warn("DEBUG MODE ENABLED - DO NOT USE IN PRODUCTION", RuntimeWarning) app.run(host=_server_host(), port=port, debug=debug) def serve_ui(port: int, prod: bool = False, config: Optional[AppConfig] = None) -> None: if prod: _serve_granian("app:create_ui_app", port, config) else: app = create_ui_app() debug = _is_debug_enabled() if debug: warnings.warn("DEBUG MODE ENABLED - DO NOT USE IN PRODUCTION", RuntimeWarning) app.run(host=_server_host(), port=port, debug=debug) def reset_credentials() -> None: import json import secrets from cryptography.fernet import Fernet config = AppConfig.from_env() iam_path = config.iam_config_path encryption_key = config.secret_key access_key = os.environ.get("ADMIN_ACCESS_KEY", "").strip() or secrets.token_hex(12) secret_key = os.environ.get("ADMIN_SECRET_KEY", "").strip() or secrets.token_urlsafe(32) custom_keys = bool(os.environ.get("ADMIN_ACCESS_KEY", "").strip()) fernet = Fernet(_derive_fernet_key(encryption_key)) if encryption_key else None raw_config = None if iam_path.exists(): try: raw_bytes = iam_path.read_bytes() from app.iam import _IAM_ENCRYPTED_PREFIX if raw_bytes.startswith(_IAM_ENCRYPTED_PREFIX): if fernet: try: content = fernet.decrypt(raw_bytes[len(_IAM_ENCRYPTED_PREFIX):]).decode("utf-8") raw_config = json.loads(content) except Exception: print("WARNING: Could not decrypt existing IAM config. Creating fresh config.") else: print("WARNING: IAM config is encrypted but no SECRET_KEY available. Creating fresh config.") else: try: raw_config = json.loads(raw_bytes.decode("utf-8")) except json.JSONDecodeError: print("WARNING: Existing IAM config is corrupted. Creating fresh config.") except OSError: pass if raw_config and raw_config.get("users"): is_v2 = raw_config.get("version", 1) >= 2 admin_user = None for user in raw_config["users"]: policies = user.get("policies", []) for p in policies: actions = p.get("actions", []) if "iam:*" in actions or "*" in actions: admin_user = user break if admin_user: break if not admin_user: admin_user = raw_config["users"][0] if is_v2: admin_keys = admin_user.get("access_keys", []) if admin_keys: admin_keys[0]["access_key"] = access_key admin_keys[0]["secret_key"] = secret_key else: from datetime import datetime as _dt, timezone as _tz admin_user["access_keys"] = [{ "access_key": access_key, "secret_key": secret_key, "status": "active", "created_at": _dt.now(_tz.utc).isoformat(), }] else: admin_user["access_key"] = access_key admin_user["secret_key"] = secret_key else: from datetime import datetime as _dt, timezone as _tz raw_config = { "version": 2, "users": [ { "user_id": f"u-{secrets.token_hex(8)}", "display_name": "Local Admin", "enabled": True, "access_keys": [ { "access_key": access_key, "secret_key": secret_key, "status": "active", "created_at": _dt.now(_tz.utc).isoformat(), } ], "policies": [ {"bucket": "*", "actions": list(ALLOWED_ACTIONS)} ], } ] } json_text = json.dumps(raw_config, indent=2) iam_path.parent.mkdir(parents=True, exist_ok=True) temp_path = iam_path.with_suffix(".json.tmp") if fernet: from app.iam import _IAM_ENCRYPTED_PREFIX encrypted = fernet.encrypt(json_text.encode("utf-8")) temp_path.write_bytes(_IAM_ENCRYPTED_PREFIX + encrypted) else: temp_path.write_text(json_text, encoding="utf-8") temp_path.replace(iam_path) print(f"\n{'='*60}") print("MYFSIO - ADMIN CREDENTIALS RESET") print(f"{'='*60}") if custom_keys: print(f"Access Key: {access_key} (from ADMIN_ACCESS_KEY)") print(f"Secret Key: {'(from ADMIN_SECRET_KEY)' if os.environ.get('ADMIN_SECRET_KEY', '').strip() else secret_key}") else: print(f"Access Key: {access_key}") print(f"Secret Key: {secret_key}") print(f"{'='*60}") if fernet: print("IAM config saved (encrypted).") else: print(f"IAM config saved to: {iam_path}") print(f"{'='*60}\n") if __name__ == "__main__": multiprocessing.freeze_support() if _is_frozen(): multiprocessing.set_start_method("spawn", force=True) parser = argparse.ArgumentParser(description="Run the S3 clone services.") parser.add_argument("--mode", choices=["api", "ui", "both", "reset-cred"], default="both") parser.add_argument("--api-port", type=int, default=5000) parser.add_argument("--ui-port", type=int, default=5100) parser.add_argument("--prod", action="store_true", help="Run in production mode using Granian") parser.add_argument("--dev", action="store_true", help="Force development mode (Flask dev server)") parser.add_argument("--engine", choices=["python", "rust"], default="python", help="API engine: python (Flask) or rust (myfsio-engine)") parser.add_argument("--check-config", action="store_true", help="Validate configuration and exit") parser.add_argument("--show-config", action="store_true", help="Show configuration summary and exit") parser.add_argument("--reset-cred", action="store_true", help="Reset admin credentials and exit") parser.add_argument("--version", action="version", version=f"MyFSIO {get_version()}") args = parser.parse_args() if args.reset_cred or args.mode == "reset-cred": reset_credentials() sys.exit(0) if args.check_config or args.show_config: config = AppConfig.from_env() config.print_startup_summary() if args.check_config: issues = config.validate_and_report() critical = [i for i in issues if i.startswith("CRITICAL:")] sys.exit(1 if critical else 0) sys.exit(0) prod_mode = args.prod or (_is_frozen() and not args.dev) config = AppConfig.from_env() first_run_marker = config.storage_root / ".myfsio.sys" / ".initialized" is_first_run = not first_run_marker.exists() if is_first_run: config.print_startup_summary() issues = config.validate_and_report() critical_issues = [i for i in issues if i.startswith("CRITICAL:")] if critical_issues: print("ABORTING: Critical configuration issues detected. Please fix them before starting.") sys.exit(1) try: first_run_marker.parent.mkdir(parents=True, exist_ok=True) first_run_marker.write_text(f"Initialized on {__import__('datetime').datetime.now().isoformat()}\n") except OSError: pass if prod_mode: print("Running in production mode (Granian)") issues = config.validate_and_report() critical_issues = [i for i in issues if i.startswith("CRITICAL:")] if critical_issues: for issue in critical_issues: print(f" {issue}") print("ABORTING: Critical configuration issues detected. Please fix them before starting.") sys.exit(1) else: print("Running in development mode (Flask dev server)") use_rust = args.engine == "rust" if args.mode in {"api", "both"}: if use_rust: print(f"Starting Rust API engine on port {args.api_port}...") else: print(f"Starting API server on port {args.api_port}...") if use_rust: api_proc = Process(target=serve_rust_api, args=(args.api_port, config)) else: api_proc = Process(target=serve_api, args=(args.api_port, prod_mode, config)) api_proc.start() else: api_proc = None def _cleanup_api(): if api_proc and api_proc.is_alive(): api_proc.terminate() api_proc.join(timeout=5) if api_proc.is_alive(): api_proc.kill() if api_proc: atexit.register(_cleanup_api) signal.signal(signal.SIGTERM, lambda *_: sys.exit(0)) if args.mode in {"ui", "both"}: print(f"Starting UI server on port {args.ui_port}...") serve_ui(args.ui_port, prod_mode, config) elif api_proc: try: api_proc.join() except KeyboardInterrupt: pass