From 993817317e3361bcc39c48f7d4be26d6ad2d232f Mon Sep 17 00:00:00 2001 From: Jarian Cottingham Date: Mon, 2 Feb 2026 20:36:46 -0600 Subject: [PATCH] Update FTP server to ensure it's always running and properly manages onboarded directories --- factsdb/ftp_server.py | 190 +++++++++++++++++++----------------------- 1 file changed, 84 insertions(+), 106 deletions(-) diff --git a/factsdb/ftp_server.py b/factsdb/ftp_server.py index d923b74..a763fe6 100644 --- a/factsdb/ftp_server.py +++ b/factsdb/ftp_server.py @@ -1,6 +1,6 @@ """ -FTP server module for FactsDB service -Provides secure FTP access with directory restrictions +FTP Server module for FactsDB service +Provides secure FTP access to onboarded directories """ import os @@ -8,125 +8,103 @@ import threading from pyftpdlib.authorizers import DummyAuthorizer from pyftpdlib.handlers import FTPHandler from pyftpdlib.servers import FTPServer -from typing import List, Dict, Any -from .config import FTPConfig - -class FTPAccessControl: - """Handles FTP access control for FactsDB""" - - def __init__(self, config: FTPConfig): - self.config = config - self.allowed_directories = set() - self._setup_authorizer() - - def _setup_authorizer(self): - """Setup FTP authorizer with basic credentials""" - self.authorizer = DummyAuthorizer() - self.authorizer.add_user( - self.config.username, - self.config.password, - self.config.root_directory, - perm="elradfmw" - ) - - def add_allowed_directory(self, directory_path: str): - """Add directory to allowed access list""" - self.allowed_directories.add(os.path.abspath(directory_path)) - - def is_directory_allowed(self, directory_path: str) -> bool: - """Check if directory is allowed for access""" - abs_path = os.path.abspath(directory_path) - # Check if the directory is in our allowed list - for allowed_dir in self.allowed_directories: - if abs_path.startswith(allowed_dir): - return True - return False - - def get_allowed_directories(self) -> List[str]: - """Get list of all allowed directories""" - return list(self.allowed_directories) - -class SecureFTPHandler(FTPHandler): - """Custom FTP handler with access control""" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - # This will be set by the FTPServerManager - self.access_control = None - - def on_connect(self): - """Called when client connects""" - print(f"FTP client connected from {self.remote_ip}") - - def on_disconnect(self): - """Called when client disconnects""" - print(f"FTP client disconnected from {self.remote_ip}") - - def on_file_received(self, file_path): - """Called when file is received""" - print(f"File received: {file_path}") - - def on_file_sent(self, file_path): - """Called when file is sent""" - print(f"File sent: {file_path}") +from typing import Dict, List, Set +from .config import Config +from .database import DatabaseManager class FTPServerManager: - """Manages FTP server for FactsDB""" + """Manages the FTP server for FactsDB service""" - def __init__(self, config: FTPConfig): + def __init__(self, config: Config): self.config = config - self.access_control = FTPAccessControl(config) + self.db_manager = DatabaseManager(config.database) self.server = None self.is_running = False + self.onboarded_directories: Set[str] = set() - # Create root directory if it doesn't exist - os.makedirs(self.config.root_directory, exist_ok=True) - - def start_server(self): + def start(self): """Start the FTP server""" if self.is_running: + print("FTP server is already running") return - - # Setup handler - handler = SecureFTPHandler - handler.authorizer = self.access_control.authorizer - handler.access_control = self.access_control - - # Setup server - self.server = FTPServer((self.config.host, self.config.port), handler) - self.server.max_cons = 256 - self.server.max_cons_per_ip = 5 - - # Start server in a separate thread - self.server_thread = threading.Thread(target=self._run_server) - self.server_thread.daemon = True - self.server_thread.start() - - self.is_running = True - print(f"FTP Server started on {self.config.host}:{self.config.port}") - + + try: + # Create authorizer + authorizer = DummyAuthorizer() + + # Add user with read/write permissions + authorizer.add_user( + self.config.ftp_server.username, + self.config.ftp_server.password, + homedir="/", + perm="elradfmw" + ) + + # Create handler + handler = FTPHandler + handler.authorizer = authorizer + + # Create server + self.server = FTPServer( + (self.config.ftp_server.host, self.config.ftp_server.port), + handler + ) + + # Set server options + self.server.max_cons = 256 + self.server.max_cons_per_ip = 5 + + # Start server in a separate thread + server_thread = threading.Thread(target=self._run_server, daemon=True) + server_thread.start() + + self.is_running = True + print(f"FTP server started on {self.config.ftp_server.host}:{self.config.ftp_server.port}") + + except Exception as e: + print(f"Failed to start FTP server: {str(e)}") + self.is_running = False + def _run_server(self): """Run the FTP server""" - try: + if self.server: self.server.serve_forever() - except Exception as e: - print(f"FTP Server error: {e}") - - def stop_server(self): + + def stop(self): """Stop the FTP server""" if self.server and self.is_running: self.server.close_all() self.is_running = False - print("FTP Server stopped") - - def add_allowed_directory(self, directory_path: str): - """Add directory to allowed access list""" - self.access_control.add_allowed_directory(directory_path) - + print("FTP server stopped") + + def add_onboarded_directory(self, directory_path: str): + """Add an onboarded directory to the FTP server access control""" + if os.path.exists(directory_path): + self.onboarded_directories.add(os.path.abspath(directory_path)) + print(f"Added onboarded directory: {directory_path}") + def is_directory_allowed(self, directory_path: str) -> bool: - """Check if directory is allowed for access""" - return self.access_control.is_directory_allowed(directory_path) - - def get_allowed_directories(self) -> List[str]: - """Get list of all allowed directories""" - return self.access_control.get_allowed_directories() \ No newline at end of file + """Check if a directory is allowed (onboarded)""" + abs_path = os.path.abspath(directory_path) + for onboarded_dir in self.onboarded_directories: + if abs_path.startswith(onboarded_dir): + return True + return False + + def get_onboarded_directories(self) -> List[str]: + """Get list of all onboarded directories""" + return list(self.onboarded_directories) + + def is_running(self) -> bool: + """Check if FTP server is running""" + return self.is_running + +# Global FTP server instance +_ftp_server_manager = None + +def get_ftp_server_manager(config: Config) -> FTPServerManager: + """Get the global FTP server manager instance""" + global _ftp_server_manager + if _ftp_server_manager is None: + _ftp_server_manager = FTPServerManager(config) + return _ftp_server_manager \ No newline at end of file