""" Project Discovery Service Servicio para descubrir proyectos S7 usando Everything API """ import uuid from datetime import datetime, timezone from pathlib import Path from typing import List, Dict, Any, Optional import logging from ..models.project_model import Project, ProjectManager from ..models.config_model import Config from ..utils.everything_wrapper import EverythingSearcher, create_everything_searcher class ProjectDiscoveryService: """Servicio para descubrir y gestionar proyectos""" def __init__(self, config: Config, project_manager: ProjectManager): self.config = config self.project_manager = project_manager self.logger = logging.getLogger(__name__) # Inicializar Everything searcher dll_path = config.get_dll_path() self.everything_searcher = create_everything_searcher(dll_path) if not self.everything_searcher: self.logger.warning("Everything API no disponible, funcionalidad limitada") def discover_all_projects(self) -> List[Project]: """ Descubrir todos los proyectos en los directorios de observación """ self.logger.info("Iniciando descubrimiento de proyectos...") discovered_projects = [] # Obtener directorios de observación habilitados observation_dirs = [ obs_dir for obs_dir in self.config.observation_directories if obs_dir.get("enabled", True) ] for obs_dir in observation_dirs: dir_path = obs_dir["path"] dir_type = obs_dir["type"] self.logger.info(f"Escaneando directorio: {dir_path} (tipo: {dir_type})") if dir_type == "siemens_s7": projects = self._discover_s7_projects(obs_dir) else: # manual directories projects = self._discover_manual_projects(obs_dir) discovered_projects.extend(projects) self.logger.info(f"Descubrimiento completado: {len(discovered_projects)} proyectos encontrados") # Actualizar project manager con proyectos descubiertos self._update_project_manager(discovered_projects) return discovered_projects def _discover_s7_projects(self, obs_dir: Dict[str, Any]) -> List[Project]: """Descubrir proyectos S7 en un directorio de observación""" projects = [] dir_path = obs_dir["path"] try: if self.everything_searcher and self.everything_searcher.is_everything_available(): # Usar Everything API para búsqueda rápida s7p_files = self.everything_searcher.search_s7p_files([dir_path]) else: # Fallback a búsqueda manual s7p_files = self._manual_search_s7p_files(dir_path) self.logger.debug(f"Encontrados {len(s7p_files)} archivos .s7p en {dir_path}") for s7p_file in s7p_files: project = self._create_project_from_s7p(s7p_file, obs_dir) if project: projects.append(project) self.logger.debug(f"Proyecto creado: {project.name}") except Exception as e: self.logger.error(f"Error descubriendo proyectos S7 en {dir_path}: {e}") return projects def _discover_manual_projects(self, obs_dir: Dict[str, Any]) -> List[Project]: """Descubrir proyectos de directorio manual""" projects = [] dir_path = obs_dir["path"] try: # Para directorios manuales, crear un proyecto por directorio if Path(dir_path).exists() and Path(dir_path).is_dir(): project = self._create_manual_project(obs_dir) if project: projects.append(project) self.logger.debug(f"Proyecto manual creado: {project.name}") except Exception as e: self.logger.error(f"Error creando proyecto manual para {dir_path}: {e}") return projects def _manual_search_s7p_files(self, directory: str) -> List[str]: """Búsqueda manual de archivos .s7p como fallback""" s7p_files = [] try: path = Path(directory) if not path.exists(): return s7p_files # Búsqueda recursiva con optimización (evitar último nivel) skip_last_level = self.config.everything_api.get("skip_last_level_for_s7p", True) for file_path in path.rglob("*.s7p"): if skip_last_level: # Verificar que no esté en el último nivel (debe tener subdirectorios) parent_dir = file_path.parent has_subdirs = any(item.is_dir() for item in parent_dir.iterdir()) if not has_subdirs: continue # Saltar archivos en directorios sin subdirectorios s7p_files.append(str(file_path)) except Exception as e: self.logger.error(f"Error en búsqueda manual de S7P en {directory}: {e}") return s7p_files def _create_project_from_s7p(self, s7p_file_path: str, obs_dir: Dict[str, Any]) -> Optional[Project]: """Crear un objeto Project desde un archivo .s7p""" try: s7p_path = Path(s7p_file_path) project_dir = s7p_path.parent obs_path = Path(obs_dir["path"]) # Generar ID único para el proyecto project_id = self._generate_project_id(s7p_file_path) # Determinar nombre del proyecto project_name = s7p_path.stem # Calcular ruta relativa try: relative_path = project_dir.relative_to(obs_path) backup_path = str(relative_path) except ValueError: # Si no se puede calcular relativa, usar nombre del directorio backup_path = project_dir.name relative_path = Path(project_dir.name) # Crear datos del proyecto project_data = { "id": project_id, "name": project_name, "path": str(project_dir), "type": "siemens_s7", "s7p_file": s7p_file_path, "observation_directory": obs_dir["path"], "relative_path": str(relative_path), "backup_path": backup_path, "schedule_config": { "schedule": self.config.global_settings.get("default_schedule", "daily"), "schedule_time": self.config.global_settings.get("default_schedule_time", "02:00"), "enabled": True, "next_scheduled_backup": "" }, "backup_history": { "last_backup_date": "", "last_backup_file": "", "backup_count": 0, "last_successful_backup": "" }, "hash_info": { "last_s7p_hash": "", "last_full_hash": "", "last_s7p_timestamp": "", "last_s7p_size": 0, "last_scan_timestamp": "", "file_count": 0, "total_size_bytes": 0 }, "status": { "current_status": "ready", "last_error": None, "retry_count": 0, "next_retry": None, "files_in_use": False, "exclusivity_check_passed": True, "last_status_update": datetime.now(timezone.utc).isoformat() }, "discovery_info": { "discovered_date": datetime.now(timezone.utc).isoformat(), "discovery_method": "everything_api" if self.everything_searcher else "manual_search", "auto_discovered": True } } return Project(project_data) except Exception as e: self.logger.error(f"Error creando proyecto desde {s7p_file_path}: {e}") return None def _create_manual_project(self, obs_dir: Dict[str, Any]) -> Optional[Project]: """Crear un proyecto para directorio manual""" try: dir_path = Path(obs_dir["path"]) # Generar ID único project_id = self._generate_project_id(str(dir_path)) # Usar nombre del directorio como nombre del proyecto project_name = dir_path.name project_data = { "id": project_id, "name": project_name, "path": str(dir_path), "type": "manual", "s7p_file": "", "observation_directory": str(dir_path.parent), "relative_path": dir_path.name, "backup_path": dir_path.name, "schedule_config": { "schedule": self.config.global_settings.get("default_schedule", "daily"), "schedule_time": self.config.global_settings.get("default_schedule_time", "02:00"), "enabled": True, "next_scheduled_backup": "" }, "backup_history": { "last_backup_date": "", "last_backup_file": "", "backup_count": 0, "last_successful_backup": "" }, "hash_info": { "last_s7p_hash": "", "last_full_hash": "", "last_s7p_timestamp": "", "last_s7p_size": 0, "last_scan_timestamp": "", "file_count": 0, "total_size_bytes": 0 }, "status": { "current_status": "ready", "last_error": None, "retry_count": 0, "next_retry": None, "files_in_use": False, "exclusivity_check_passed": True, "last_status_update": datetime.now(timezone.utc).isoformat() }, "discovery_info": { "discovered_date": datetime.now(timezone.utc).isoformat(), "discovery_method": "manual_directory", "auto_discovered": True } } return Project(project_data) except Exception as e: self.logger.error(f"Error creando proyecto manual para {obs_dir['path']}: {e}") return None def _generate_project_id(self, path: str) -> str: """Generar ID único para un proyecto basado en su ruta""" # Usar hash de la ruta + timestamp para garantizar unicidad import hashlib path_hash = hashlib.md5(path.encode()).hexdigest()[:8] timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") return f"project_{path_hash}_{timestamp}" def _update_project_manager(self, discovered_projects: List[Project]) -> None: """Actualizar el project manager con proyectos descubiertos""" try: # Obtener proyectos existentes existing_projects = {p.path: p for p in self.project_manager.get_all_projects()} # Agregar nuevos proyectos new_projects_count = 0 for project in discovered_projects: if project.path not in existing_projects: self.project_manager.add_project(project.to_dict()) new_projects_count += 1 self.logger.debug(f"Proyecto agregado: {project.name}") if new_projects_count > 0: self.logger.info(f"Se agregaron {new_projects_count} nuevos proyectos") else: self.logger.info("No se encontraron nuevos proyectos") # Actualizar estadísticas self.project_manager.update_statistics() except Exception as e: self.logger.error(f"Error actualizando project manager: {e}") def rescan_observation_directories(self) -> int: """ Re-escanear directorios de observación para nuevos proyectos Retorna el número de nuevos proyectos encontrados """ self.logger.info("Iniciando re-escaneo de directorios de observación...") initial_count = len(self.project_manager.get_all_projects()) # Re-descubrir proyectos self.discover_all_projects() final_count = len(self.project_manager.get_all_projects()) new_projects = final_count - initial_count self.logger.info(f"Re-escaneo completado: {new_projects} nuevos proyectos encontrados") return new_projects