Path: blob/master/ftp_and_sftp_processor.py
640 views
#!/usr/bin/env python1import argparse2import ftplib3import json4import logging5import os6import sys7import tempfile8import time9from abc import ABC, abstractmethod10from collections.abc import Callable11from datetime import datetime, timedelta12from ftplib import FTP, error_perm, error_reply13from typing import Any1415import paramiko1617from plate_recognition import recognition_api, save_results1819LOG_LEVEL = os.environ.get("LOGGING", "INFO").upper()2021logging.basicConfig(22stream=sys.stdout,23level=LOG_LEVEL,24style="{",25format="{asctime} {levelname} {name} {threadName} : {message}",26)2728lgr = logging.getLogger(__name__)293031def get_files_and_dirs(32func: Callable[[Any, list, list, list], None]33) -> Callable[[Any], tuple[list, list, list]]:34def wrapper(self):35file_list = self.list_files()36dirs = []37nondirs = []38func(self, file_list, dirs, nondirs)39return file_list, dirs, nondirs4041return wrapper424344class FileTransferProcessor(ABC):45def __init__(self, **kwargs):46for key, value in kwargs.items():47setattr(self, key, value)4849self.processed = []5051@abstractmethod52def connect(self):53pass5455@abstractmethod56def delete_file(self, file):57pass5859@abstractmethod60def list_files(self):61pass6263@abstractmethod64def set_ftp_binary_file(self):65pass6667@abstractmethod68def set_working_directory(self, path):69pass7071@abstractmethod72def get_working_directory(self):73pass7475@abstractmethod76def retrieve_files(self):77pass7879def processing_single_camera(self, args):80self.set_working_directory(args.folder)8182file_list, dirs, nondirs = self.retrieve_files()8384logging.info(85"Found %s file(s) in %s.", len(nondirs), self.get_working_directory()86)8788# processing files89self.process_files(nondirs)9091for folder in dirs:92folder_path = f"./{folder}"93self.camera_id = folder94self.set_working_directory(folder_path)95nondirs = []96file_list = self.list_files()9798for info in file_list:99name = info[-1]100ls_type = info[0] if self.os_linux else info[-2]101if ls_type.startswith("d") or ls_type == "<DIR>":102# Don't process files any deeper103pass104else:105if self.os_linux:106nondirs.append(107[name, self.parse_date(info[-4], info[-3], info[-2])]108)109else:110file_date = info[0].split("-")111file_time = info[1]112113if "AM" in file_time or "PM" in file_time:114parsed_time = datetime.strptime(file_time, "%I:%M%p")115file_time = parsed_time.strftime(116"%H:%M"117) # from AM/PM to 24-hour format118119nondirs.append(120[121name,122self.parse_date(123file_date[0], file_date[1], file_time, linux=False124),125]126)127128logging.info(129"Found %s file(s) in %s.", len(nondirs), self.get_working_directory()130)131self.process_files(nondirs)132133def track_processed(self):134"""135Track processed is an interval specified136137:param self: FileTransferProcessor properties context138:return: Boolean139"""140return self.interval and self.interval > 0 and not self.delete141142def manage_processed_file(self, file, last_modified):143"""144Process a file path:1451. Deletes old file in ftp_files from ftp_client146147:param file: file data path148:param last_modified: last modified datetime149"""150151rm_older_than_date = datetime.now() - timedelta(seconds=self.delete)152if rm_older_than_date > last_modified:153result = self.delete_file(file)154if "error" in result.lower():155print(f"file couldn't be deleted: {result}")156else:157self.processed.remove(file)158159def process_files(self, ftp_files):160results = []161for file_last_modified in ftp_files:162ftp_file = file_last_modified[0]163last_modified = file_last_modified[1]164165if self.delete is not None:166self.manage_processed_file(ftp_file, last_modified)167continue168169logging.info(ftp_file)170171with tempfile.NamedTemporaryFile(172suffix="_" + ftp_file, mode="rb+"173) as image:174175self.set_ftp_binary_file(ftp_file, image)176api_res = recognition_api(177image,178self.regions,179self.api_key,180self.sdk_url,181camera_id=self.camera_id,182timestamp=self.timestamp,183mmc=self.mmc,184exit_on_error=False,185)186results.append(api_res)187188if self.track_processed():189self.processed.append(ftp_file)190191if self.output_file:192save_results(results, self)193else:194print(json.dumps(results, indent=2))195196def get_month_literal(self, month_number):197198month_mapping = {199"01": "jan",200"02": "feb",201"03": "mar",202"04": "apr",203"05": "may",204"06": "jun",205"07": "jul",206"08": "aug",207"09": "sep",208"10": "oct",209"11": "nov",210"12": "dec",211}212213month_literal = month_mapping.get(month_number.lower(), "unknown")214return month_literal215216def parse_date(self, x, y, z, linux=True):217"""218M D T|Y219Jan 3 1994220Jan 17 1993221Sep 13 19:07222"""223224if not linux:225x = self.get_month_literal(x)226227date_string = f"{x} {int(y):02} {z}"228229if ":" in z:230modify_year = True231parse_string = "%b %d %H:%M"232else:233modify_year = False234parse_string = "%b %d %Y"235236logging.debug(f"Input Date String: {date_string}")237date_time_obj = datetime.strptime(date_string, parse_string)238if modify_year:239date_time_obj = date_time_obj.replace(year=datetime.now().year)240241logging.debug(f"Parsed date: {date_time_obj}")242return date_time_obj243244245class FTPProcessor(FileTransferProcessor):246def __init__(self, **kwargs):247super().__init__(**kwargs)248self.ftp = None249self.os_linux = None250251def connect(self):252self.ftp = FTP(timeout=120)253self.ftp.connect(self.hostname, self.port)254self.ftp.login(self.ftp_user, self.ftp_password)255logging.info(f"Connected to FTP server at {self.hostname}")256return self.ftp257258def delete_file(self, file):259try:260response = self.ftp.delete(file)261return f"File {file} deleted. Server response: {response}"262except error_perm as e:263return f"Permission error: {e}"264except error_reply as e:265return f"Other FTP error: {e}"266except Exception as e:267return f"An unexpected error occurred: {e}"268269def get_working_directory(self):270return self.ftp.pwd()271272def set_working_directory(self, path):273try:274self.ftp.cwd(path)275except ftplib.error_perm as e:276print(f"Error 550: {e}")277278def is_linux_os(self, file_list):279# check if OS is Linux or Windows280for info in file_list:281info_first_possition = info[0]282if info_first_possition.startswith("d") or info_first_possition.startswith(283"-"284):285return True286return False287288def set_ftp_binary_file(self, file, image):289"""Retrieve a file in binary transfer mode290291Args:292file (String): remote file path293image (TemporaryFile): image file destination294"""295self.ftp.retrbinary("RETR " + file, image.write)296297def list_files(self):298file_list = []299self.ftp.retrlines("LIST", lambda x: file_list.append(x.split(maxsplit=8)))300return file_list301302@get_files_and_dirs303def retrieve_files(self, file_list, dirs, nondirs):304self.os_linux = self.is_linux_os(file_list)305for info in file_list:306name = info[-1]307ls_type = info[0] if self.os_linux else info[-2]308if ls_type.startswith("d") or ls_type == "<DIR>":309dirs.append(name)310else:311if self.os_linux:312nondirs.append(313[name, self.parse_date(info[-4], info[-3], info[-2])]314)315else:316file_date = info[0].split("-")317file_time = info[1]318319if "AM" in file_time or "PM" in file_time:320parsed_time = datetime.strptime(file_time, "%I:%M%p")321file_time = parsed_time.strftime(322"%H:%M"323) # from AM/PM to 24-hour format324325nondirs.append(326[327name,328self.parse_date(329file_date[0], file_date[1], file_time, linux=False330),331]332)333334335class SFTPProcessor(FileTransferProcessor):336def __init__(self, **kwargs):337super().__init__(**kwargs)338self.sftp = None339self.os_linux = True340341def connect(self):342try:343ssh = paramiko.SSHClient()344ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())345346if self.ftp_password and not self.pkey:347print(self.hostname, self.port, self.ftp_user, self.ftp_password)348ssh.connect(349self.hostname,350port=self.port,351username=self.ftp_user,352password=self.ftp_password,353look_for_keys=False,354allow_agent=False,355)356357else:358try:359key = paramiko.RSAKey.from_private_key_file(self.pkey)360ssh.connect(self.hostname, self.port, self.ftp_user, pkey=key)361except paramiko.AuthenticationException as e:362logging.error(f"Authentication failed: {e}")363raise364except Exception as e:365logging.error(f"An unexpected error occurred: {e}")366raise367368self.sftp = ssh.open_sftp()369logging.info(f"Connected to SFTP server at {self.hostname}")370371except paramiko.AuthenticationException:372logging.error("Authentication failed. Please check your credentials.")373except paramiko.SSHException as e:374logging.error(f"SSH connection error: {e}")375except Exception as e:376logging.error(f"An unexpected error occurred: {e}")377378return self.sftp379380def delete_file(self, file):381try:382self.sftp.remove(file)383return f"File {file} deleted."384except FileNotFoundError as e:385return f"File not found: {e}"386except OSError as e:387return f"An IOError occurred: {e}"388except Exception as e:389return f"An unexpected error occurred: {e}"390391def get_working_directory(self):392return self.sftp.getcwd()393394def set_working_directory(self, path):395try:396self.sftp.chdir(path)397except Exception as e:398print(f"Error changing working directory: {e}")399400def set_ftp_binary_file(self, file, image):401"""Copy a remote file (remotepath) from the SFTP server and write to an open file or file-like object402403Args:404file (String): remote file path405image (TemporaryFile): image file destination406"""407wd = self.get_working_directory()408self.sftp.getfo(wd + "/" + file, image)409410def list_files(self):411412file_list = []413414try:415file_list_attr = self.sftp.listdir_attr()416417for attr in file_list_attr:418file_list.append(str(attr).split())419420for info in file_list:421# info format: ['-rw-------', '1', '0', '0', '175289', '16', 'Nov', '18:10', 'demo.jpg']422# adapting to linux standard: ['-rw-------', '1', '0', '0', '175289', 'Nov', '16', '18:10', 'demo.jpg']423tmp_month = info[-3]424info[-3] = info[-4]425info[-4] = tmp_month426427except Exception as e:428print(f"Error listing files: {e}")429return430431return file_list432433@get_files_and_dirs434def retrieve_files(self, file_list, dirs, nondirs):435436for info in file_list:437name = info[-1]438if info[0].startswith("d"):439dirs.append(name)440else:441nondirs.append([name, self.parse_date(info[-4], info[-3], info[-2])])442443444def parse_arguments(args_hook=lambda _: _):445parser = argparse.ArgumentParser(446description="Read license plates from the images on an FTP server and output the result as JSON or CSV",447epilog="""448Examples:\n449450FTP:451---452Process images on an FTP server:453ftp_and_sftp_processor.py -a MY_API_KEY -H host -U user1 -P pass454Specify Camera ID and/or two Regions:455ftp_and_sftp_processor.py -a MY_API_KEY -H host -U user1 -P pass -f /home/user1 --camera-id Camera1 -r us-ca -r th-37456Use the Snapshot SDK instead of the Cloud Api:457ftp_and_sftp_processor.py -H host -U user1 -P pass -s http://localhost:8080458459""",460formatter_class=argparse.RawTextHelpFormatter,461)462parser.add_argument("-a", "--api-key", help="Your API key.", required=False)463parser.add_argument(464"-r",465"--regions",466help="Match the license plate pattern fo specific region",467required=False,468action="append",469)470parser.add_argument(471"-s",472"--sdk-url",473help="Url to self hosted sdk For example, http://localhost:8080",474required=False,475)476parser.add_argument(477"--camera-id", help="Name of the source camera.", required=False478)479480parser.add_argument(481"-c",482"--protocol",483help="protocol tu use, available choices 'ftp' or 'sftp'",484choices="ftp sftp".split(),485default="ftp",486required=False,487)488489args_hook(parser)490args = parser.parse_args()491492if not args.api_key:493raise Exception("api-key parameter is required")494495return args496497498def custom_args(parser):499parser.epilog += """500501Specify a folder on the FTP server:502ftp_and_sftp_processor.py -a MY_API_KEY -H 192.168.0.59 -U user1 -P pass -f /home/user1503Delete processed files from the FTP server after 10 seconds:504ftp_and_sftp_processor.py -a MY_API_KEY -H 192.168.0.59 -U user1 -P pass -f /home/user1 -d 10505Specify a folder containing dynamic cameras, Sub-folder names are Camera IDs:506ftp_and_sftp_processor.py -a MY_API_KEY -H 192.168.0.59 -U user1 -P pass --cameras-root /srv/cameras507Periodically check for new files every 10 seconds:508ftp_and_sftp_processor.py -a MY_API_KEY -H 192.168.0.59 -U user1 -P pass -f /home/user1 -i 10509Enable Make Model and Color prediction:510ftp_and_sftp_processor.py -a MY_API_KEY -H 192.168.0.59 -U user1 -P pass -f /home/user1 --mmc511Specify an output file and format for the results:512ftp_and_sftp_processor.py -a MY_API_KEY -H 192.168.0.59 -U user1 -P pass -f /home/user1 -o data.csv --format csv513514SFTP:515----516ftp_password login, Process images in /tmp/images:517ftp_and_sftp_processor.py -c sftp -U usr1 -P pass -H 192.168.0.59 -f /tmp/images -a 4805bee#########\n518Private Key login, Process images in /tmp/images:519ftp_and_sftp_processor.py -c sftp -U usr1 --pkey '/home/danleyb2/.ssh/id_rsa' -H 192.168.0.59 -f /tmp/images -a 4805bee#########\n520ftp_password login, Process images in /tmp/images using Snapshot SDK:521ftp_and_sftp_processor.py -c sftp -U usr1 -P pass -H 192.168.0.59 -f /tmp/images -s http://localhost:8080\n522Process images in /tmp/images Periodically every 5 seconds:523ftp_and_sftp_processor.py -c sftp -U usr1 -P pass -H 192.168.0.59 -f /tmp/images -a 4805bee######### -i 5524"""525parser.add_argument("-t", "--timestamp", help="Timestamp.", required=False)526parser.add_argument("-H", "--hostname", help="host", required=True)527parser.add_argument("-p", "--port", help="port", required=False)528parser.add_argument(529"-U", "--ftp-user", help="Transfer protocol server user", required=True530)531parser.add_argument(532"-P",533"--ftp-password",534help="Transfer protocol server user's password",535required=False,536)537parser.add_argument("--pkey", help="SFTP Private Key Path", required=False)538parser.add_argument(539"-d",540"--delete",541type=int,542help="Remove images from the FTP server after processing. Optionally specify a timeout in seconds.",543nargs="?",544const=0,545)546parser.add_argument(547"-f",548"--folder",549help="Specify folder with images on the FTP server.",550default="/",551)552parser.add_argument(553"--cameras-root", help="Root folder containing dynamic cameras", required=False554)555parser.add_argument("-o", "--output-file", help="Save result to file.")556parser.add_argument(557"--format",558help="Format of the result.",559default="json",560choices="json csv".split(),561)562parser.add_argument(563"--mmc",564action="store_true",565help="Predict vehicle make and model (SDK only). It has to be enabled.",566)567parser.add_argument(568"-i",569"--interval",570type=int,571help="Periodically fetch new images from the server every interval seconds.",572)573574def default_port():575return 21 if parser.parse_args().protocol == "ftp" else 22576577parser.set_defaults(port=default_port())578579580def ftp_process(args):581582args_dict = vars(args)583584if args.protocol == "ftp":585file_processor = FTPProcessor(**args_dict)586else:587if not args.ftp_password and not args.pkey:588raise Exception("ftp_password or pkey path are required")589file_processor = SFTPProcessor(**args_dict)590591"""592for attr, value in file_processor.__dict__.items():593print(f"{attr}: {value}")594"""595596file_processor.connect()597598if args.cameras_root:599file_processor.set_working_directory(args.cameras_root)600file_list, dirs, nondirs = file_processor.retrieve_files()601for folder in dirs:602logging.info(603f"Processing Dynamic Camera : {file_processor.get_working_directory()}"604)605args.folder = os.path.join(args.cameras_root, folder)606# The camera id is the folder name607args.camera_id = folder608609file_processor.processing_single_camera(args)610else:611file_processor.processing_single_camera(args)612613614def main():615args = parse_arguments(custom_args)616617if args.interval and args.interval > 0:618while True:619try:620ftp_process(args)621except Exception as e:622print(f"ERROR: {e}")623time.sleep(args.interval)624else:625ftp_process(args)626627628if __name__ == "__main__":629main()630631632