Files
ASE/src/ftp_csv_receiver.py
2025-11-01 21:31:20 +01:00

186 lines
6.2 KiB
Python
Executable File

#!.venv/bin/python
"""
This module implements an FTP server with custom commands for
managing virtual users and handling CSV file uploads.
"""
import logging
import os
from hashlib import sha256
from logging.handlers import RotatingFileHandler
from pathlib import Path
from pyftpdlib.handlers import FTPHandler
from pyftpdlib.servers import FTPServer
from utils.authorizers.database_authorizer import DatabaseAuthorizer
from utils.config import loader_ftp_csv as setting
from utils.connect import file_management, user_admin
# Configure logging (moved inside main function)
logger = logging.getLogger(__name__)
# Legacy authorizer kept for reference (not used anymore)
# The DatabaseAuthorizer is now used for real-time database synchronization
class ASEHandler(FTPHandler):
"""Custom FTP handler that extends FTPHandler with custom commands and file handling."""
def __init__(self: object, conn: object, server: object, ioloop: object = None) -> None:
"""Initializes the handler, adds custom commands, and sets up command permissions.
Args:
conn (object): The connection object.
server (object): The FTP server object.
ioloop (object): The I/O loop object.
"""
super().__init__(conn, server, ioloop)
self.proto_cmds = FTPHandler.proto_cmds.copy()
# Add custom FTP commands for managing virtual users - command in lowercase
self.proto_cmds.update(
{
"SITE ADDU": {
"perm": "M",
"auth": True,
"arg": True,
"help": "Syntax: SITE <SP> ADDU USERNAME PASSWORD (add virtual user).",
}
}
)
self.proto_cmds.update(
{
"SITE DISU": {
"perm": "M",
"auth": True,
"arg": True,
"help": "Syntax: SITE <SP> DISU USERNAME (disable virtual user).",
}
}
)
self.proto_cmds.update(
{
"SITE ENAU": {
"perm": "M",
"auth": True,
"arg": True,
"help": "Syntax: SITE <SP> ENAU USERNAME (enable virtual user).",
}
}
)
self.proto_cmds.update(
{
"SITE LSTU": {
"perm": "M",
"auth": True,
"arg": None,
"help": "Syntax: SITE <SP> LSTU (list virtual users).",
}
}
)
def on_file_received(self: object, file: str) -> None:
return file_management.on_file_received(self, file)
def on_incomplete_file_received(self: object, file: str) -> None:
"""Removes partially uploaded files.
Args:
file: The path to the incomplete file.
"""
os.remove(file)
def ftp_SITE_ADDU(self: object, line: str) -> None:
return user_admin.ftp_SITE_ADDU(self, line)
def ftp_SITE_DISU(self: object, line: str) -> None:
return user_admin.ftp_SITE_DISU(self, line)
def ftp_SITE_ENAU(self: object, line: str) -> None:
return user_admin.ftp_SITE_ENAU(self, line)
def ftp_SITE_LSTU(self: object, line: str) -> None:
return user_admin.ftp_SITE_LSTU(self, line)
def setup_logging(log_filename: str):
"""
Configura il logging per il server FTP con rotation e output su console.
Args:
log_filename (str): Percorso del file di log.
"""
root_logger = logging.getLogger()
formatter = logging.Formatter("%(asctime)s - PID: %(process)d.%(name)s.%(levelname)s: %(message)s")
# Rimuovi eventuali handler esistenti
if root_logger.hasHandlers():
root_logger.handlers.clear()
# Handler per file con rotation (max 10MB per file, mantiene 5 backup)
file_handler = RotatingFileHandler(
log_filename,
maxBytes=10 * 1024 * 1024, # 10 MB
backupCount=5, # Mantiene 5 file di backup
encoding="utf-8"
)
file_handler.setFormatter(formatter)
root_logger.addHandler(file_handler)
# Handler per console (utile per Docker)
console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
root_logger.addHandler(console_handler)
root_logger.setLevel(logging.INFO)
root_logger.info("Logging FTP configurato con rotation (10MB, 5 backup) e console output")
def main():
"""Main function to start the FTP server."""
# Load the configuration settings
cfg = setting.Config()
try:
# Configure logging first
setup_logging(cfg.logfilename)
# Initialize the authorizer with database support
# This authorizer checks the database on every login, ensuring
# all FTP server instances stay synchronized without restarts
authorizer = DatabaseAuthorizer(cfg)
# Initialize handler
handler = ASEHandler
handler.cfg = cfg
handler.authorizer = authorizer
# Set masquerade address only if configured (importante per HA con VIP)
# Questo è l'IP che il server FTP pubblicherà ai client per le connessioni passive
if cfg.proxyaddr and cfg.proxyaddr.strip():
handler.masquerade_address = cfg.proxyaddr
logger.info(f"FTP masquerade address configured: {cfg.proxyaddr}")
else:
logger.info("FTP masquerade address not configured - using server's default IP")
# Set the range of passive ports for the FTP server
_range = list(range(cfg.firstport, cfg.firstport + cfg.portrangewidth))
handler.passive_ports = _range
# Log configuration
logger.info(f"Starting FTP server on port {cfg.service_port} with DatabaseAuthorizer")
logger.info(f"FTP passive ports range: {cfg.firstport}-{cfg.firstport + cfg.portrangewidth - 1}")
logger.info(f"Database connection: {cfg.dbuser}@{cfg.dbhost}:{cfg.dbport}/{cfg.dbname}")
# Create and start the FTP server
server = FTPServer(("0.0.0.0", cfg.service_port), handler)
server.serve_forever()
except Exception as e:
logger.error("Exit with error: %s.", e)
if __name__ == "__main__":
main()