Update FTP server to ensure it's always running and properly manages onboarded directories

This commit is contained in:
Jarian Cottingham 2026-02-02 20:36:46 -06:00
parent 40cb5122bd
commit 993817317e

View File

@ -1,6 +1,6 @@
""" """
FTP server module for FactsDB service FTP Server module for FactsDB service
Provides secure FTP access with directory restrictions Provides secure FTP access to onboarded directories
""" """
import os import os
@ -8,125 +8,103 @@ import threading
from pyftpdlib.authorizers import DummyAuthorizer from pyftpdlib.authorizers import DummyAuthorizer
from pyftpdlib.handlers import FTPHandler from pyftpdlib.handlers import FTPHandler
from pyftpdlib.servers import FTPServer from pyftpdlib.servers import FTPServer
from typing import List, Dict, Any from typing import Dict, List, Set
from .config import FTPConfig from .config import Config
from .database import DatabaseManager
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}")
class FTPServerManager: 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.config = config
self.access_control = FTPAccessControl(config) self.db_manager = DatabaseManager(config.database)
self.server = None self.server = None
self.is_running = False self.is_running = False
self.onboarded_directories: Set[str] = set()
# Create root directory if it doesn't exist def start(self):
os.makedirs(self.config.root_directory, exist_ok=True)
def start_server(self):
"""Start the FTP server""" """Start the FTP server"""
if self.is_running: if self.is_running:
print("FTP server is already running")
return return
# Setup handler try:
handler = SecureFTPHandler # Create authorizer
handler.authorizer = self.access_control.authorizer authorizer = DummyAuthorizer()
handler.access_control = self.access_control
# Add user with read/write permissions
# Setup server authorizer.add_user(
self.server = FTPServer((self.config.host, self.config.port), handler) self.config.ftp_server.username,
self.server.max_cons = 256 self.config.ftp_server.password,
self.server.max_cons_per_ip = 5 homedir="/",
perm="elradfmw"
# Start server in a separate thread )
self.server_thread = threading.Thread(target=self._run_server)
self.server_thread.daemon = True # Create handler
self.server_thread.start() handler = FTPHandler
handler.authorizer = authorizer
self.is_running = True
print(f"FTP Server started on {self.config.host}:{self.config.port}") # 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): def _run_server(self):
"""Run the FTP server""" """Run the FTP server"""
try: if self.server:
self.server.serve_forever() self.server.serve_forever()
except Exception as e:
print(f"FTP Server error: {e}") def stop(self):
def stop_server(self):
"""Stop the FTP server""" """Stop the FTP server"""
if self.server and self.is_running: if self.server and self.is_running:
self.server.close_all() self.server.close_all()
self.is_running = False self.is_running = False
print("FTP Server stopped") print("FTP server stopped")
def add_allowed_directory(self, directory_path: str): def add_onboarded_directory(self, directory_path: str):
"""Add directory to allowed access list""" """Add an onboarded directory to the FTP server access control"""
self.access_control.add_allowed_directory(directory_path) 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: def is_directory_allowed(self, directory_path: str) -> bool:
"""Check if directory is allowed for access""" """Check if a directory is allowed (onboarded)"""
return self.access_control.is_directory_allowed(directory_path) abs_path = os.path.abspath(directory_path)
for onboarded_dir in self.onboarded_directories:
def get_allowed_directories(self) -> List[str]: if abs_path.startswith(onboarded_dir):
"""Get list of all allowed directories""" return True
return self.access_control.get_allowed_directories() 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