diff --git a/README.md b/README.md index 8c5eb36..324a481 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # AutoBackups Project ## Overview -AutoBackups is a Python application designed to automate the backup of Simatic S7 projects and other user-defined directories. The application leverages the Everything API to efficiently locate files and manage backups based on user-defined configurations. +AutoBackups is a Python application designed to automate the backup of Simatic S7 projects and other user-defined directories. The application leverages the Everything API to efficiently locate files and manage backups based on user-defined configurations. Features a system tray integration for easy access and background operation. ## Features - Monitors specified directories for Simatic S7 files (*.s7p) and other user-defined directories. @@ -10,6 +10,8 @@ AutoBackups is a Python application designed to automate the backup of Simatic S - Skips projects with files in use and retries after one hour. - Maintains a two-stage hash system: first checking *.s7p files, then all files to avoid unnecessary backups. - User-friendly web interface built with Flask for configuration and status monitoring. +- **System tray integration** with options to open browser and close application. +- **Custom favicon** for both frontend and system tray. - Comprehensive logging system with timestamped log files. ## Project Structure @@ -24,14 +26,19 @@ AutoBackups is a Python application designed to automate the backup of Simatic S │ ├── services │ │ └── __init__.py # Business logic for backups │ └── utils -│ └── __init__.py # Utility functions for hashing and file access +│ ├── __init__.py # Utility functions for hashing and file access +│ └── system_tray.py # System tray functionality ├── static │ ├── css # CSS files for styling +│ ├── icons +│ │ └── favicon.png # Application icon (used in web and system tray) │ └── js # JavaScript files for client-side functionality ├── templates │ └── index.html # Main HTML template for the web interface ├── config.json # Global configuration settings for the application ├── projects.json # Per-project configuration, schedules, and hash storage +├── launch_tray.py # System tray launcher +├── launch_with_tray.bat # Windows batch file for easy launching ├── requirements.txt # Python dependencies └── README.md # Project documentation ``` @@ -57,13 +64,39 @@ AutoBackups is a Python application designed to automate the backup of Simatic S 4. Configure the application by editing `config.json` to specify directories to monitor and backup settings. ## Usage + +### Regular Mode - Start the Flask application: ``` python src/app.py ``` - - Access the web interface at `http://localhost:5120` to configure settings and monitor backup statuses. +### System Tray Mode (Recommended) +- Start the application with system tray integration: + ``` + python launch_tray.py + ``` +- Or use the Windows batch file: + ``` + launch_with_tray.bat + ``` + +#### System Tray Features: +- **Auto-start**: Application starts in the background with system tray icon +- **Quick Access**: Double-click the tray icon to open the web interface +- **Right-click Menu**: + - "Open AutoBackups" - Opens the web interface in your default browser + - "Status" - Shows application status (future feature) + - "Exit" - Safely closes the application +- **Custom Icon**: Uses `static/icons/favicon.png` for both web interface and system tray +- **Silent Operation**: Application runs silently in the background + +#### System Tray Requirements: +- Windows 10/11 (tested) +- Python packages: `pystray`, `pillow` (automatically installed) +- Valid favicon.png file in `static/icons/` + ## Contributing Contributions are welcome! Please submit a pull request or open an issue for any enhancements or bug fixes. diff --git a/config.json b/config.json index f28f4e4..a785a2e 100644 --- a/config.json +++ b/config.json @@ -5,6 +5,12 @@ "type": "siemens_s7", "enabled": true, "description": "Directorio principal de proyectos Siemens" + }, + { + "path": "C:/Trabajo/SIDEL/05 - E5.007161 - Modifica O&U - SAE346", + "type": "siemens_s7", + "enabled": true, + "description": "SAE346" } ], "backup_destination": "D:\\Backups\\AutoBackups", diff --git a/launch_tray.py b/launch_tray.py new file mode 100644 index 0000000..898067f --- /dev/null +++ b/launch_tray.py @@ -0,0 +1,94 @@ +""" +AutoBackups System Tray Launcher +Lanza la aplicación AutoBackups con icono en el system tray +""" + +import sys +from pathlib import Path +import threading +import time +import logging + +# Agregar el directorio src al path para imports +current_dir = Path(__file__).parent +src_dir = current_dir / "src" +sys.path.insert(0, str(src_dir)) + +# Imports de la aplicación +from src.app import AutoBackupsFlaskApp, app + + +def run_flask_app(autobackups_app, host, port, debug=False): + """Ejecuta la aplicación Flask en un hilo separado""" + try: + app.run(host=host, port=port, debug=debug, use_reloader=False) + except Exception as e: + logging.error(f"Error ejecutando Flask app: {e}") + + +def main(): + """Función principal para el launcher del system tray""" + try: + print("AutoBackups - System Tray Mode") + print("=" * 30) + + # Crear y configurar aplicación Flask + autobackups_app = AutoBackupsFlaskApp() + + # Verificar requerimientos del sistema + if not autobackups_app.check_system_requirements(): + print("Error en la verificación de requerimientos del sistema.") + sys.exit(1) + + # Ejecutar descubrimiento inicial de proyectos + projects_found = autobackups_app.discover_projects() + msg = f"Descubrimiento inicial: {projects_found} proyectos encontrados" + print(msg) + + # Configurar Flask + host = autobackups_app.config.web_interface.get("host", "127.0.0.1") + port = autobackups_app.config.web_interface.get("port", 5000) + debug = autobackups_app.config.web_interface.get("debug", False) + + print(f"Servidor web iniciando en http://{host}:{port}") + + # Iniciar system tray + if autobackups_app.start_system_tray(): + print("✓ System tray iniciado correctamente") + else: + print("⚠ System tray no disponible, continuando...") + + # Ejecutar Flask en un hilo separado + flask_thread = threading.Thread( + target=run_flask_app, args=(autobackups_app, host, port, debug), daemon=True + ) + flask_thread.start() + + print("✓ Aplicación iniciada") + print("• Accede desde el icono del system tray") + print("• Usa Ctrl+C para salir desde la consola") + print("• O usa 'Exit' desde el menú del system tray") + + # Mantener el programa corriendo + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + print("\nCerrando aplicación...") + + except KeyboardInterrupt: + print("\nAplicación interrumpida por el usuario") + except Exception as e: + print(f"Error inesperado: {e}") + sys.exit(1) + finally: + # Limpieza + if "autobackups_app" in locals(): + if autobackups_app.scheduler: + autobackups_app.scheduler.shutdown() + if autobackups_app.system_tray: + autobackups_app.system_tray.stop_tray() + + +if __name__ == "__main__": + main() diff --git a/launch_with_tray.bat b/launch_with_tray.bat new file mode 100644 index 0000000..c77512e --- /dev/null +++ b/launch_with_tray.bat @@ -0,0 +1,24 @@ +@echo off +echo AutoBackups - Launching with System Tray... +echo. + +REM Activate conda environment +call conda activate autobackups + +REM Check if activation was successful +if errorlevel 1 ( + echo Error: Could not activate conda environment 'autobackups' + echo Please make sure the environment exists and conda is installed. + pause + exit /b 1 +) + +REM Launch the application with system tray +python launch_tray.py + +REM Pause to see any error messages +if errorlevel 1 ( + echo. + echo Application exited with an error. + pause +) diff --git a/projects.json b/projects.json index 1d43bdb..dd8eaee 100644 --- a/projects.json +++ b/projects.json @@ -1,8 +1,8 @@ { "metadata": { "version": "1.0", - "last_updated": "2025-09-02T07:15:16.655495+00:00", - "total_projects": 3 + "last_updated": "2025-09-02T10:51:48.579987+00:00", + "total_projects": 9 }, "projects": [ { @@ -21,10 +21,10 @@ "next_scheduled_backup": "" }, "backup_history": { - "last_backup_date": "2025-09-02T08:48:07.555782+00:00", - "last_backup_file": "D:\\Backups\\AutoBackups\\Ssae0452 Last Version Walter\\2025-09-02\\10-48-04_projects.zip", - "backup_count": 1, - "last_successful_backup": "2025-09-02T08:48:07.555782+00:00" + "last_backup_date": "2025-09-02T10:52:24.182464+00:00", + "last_backup_file": "D:\\Backups\\AutoBackups\\Ssae0452 Last Version Walter\\2025-09-02\\12-52-22_projects.zip", + "backup_count": 2, + "last_successful_backup": "2025-09-02T10:52:24.182464+00:00" }, "hash_info": { "last_s7p_hash": "", @@ -42,7 +42,7 @@ "next_retry": null, "files_in_use": false, "exclusivity_check_passed": true, - "last_status_update": "2025-09-02T08:48:07.555782+00:00" + "last_status_update": "2025-09-02T10:52:24.182464+00:00" }, "discovery_info": { "discovered_date": "", @@ -66,10 +66,10 @@ "next_scheduled_backup": "" }, "backup_history": { - "last_backup_date": "2025-09-02T08:48:07.539194+00:00", - "last_backup_file": "D:\\Backups\\AutoBackups\\LineaB\\Ssae04_11 - compiled\\2025-09-02\\10-48-04_projects.zip", - "backup_count": 2, - "last_successful_backup": "2025-09-02T08:48:07.539194+00:00" + "last_backup_date": "2025-09-02T10:52:24.159989+00:00", + "last_backup_file": "D:\\Backups\\AutoBackups\\LineaB\\Ssae04_11 - compiled\\2025-09-02\\12-52-22_projects.zip", + "backup_count": 3, + "last_successful_backup": "2025-09-02T10:52:24.159989+00:00" }, "hash_info": { "last_s7p_hash": "", @@ -87,7 +87,7 @@ "next_retry": null, "files_in_use": false, "exclusivity_check_passed": true, - "last_status_update": "2025-09-02T08:48:07.539194+00:00" + "last_status_update": "2025-09-02T10:52:24.159989+00:00" }, "discovery_info": { "discovered_date": "", @@ -111,10 +111,10 @@ "next_scheduled_backup": "" }, "backup_history": { - "last_backup_date": "2025-09-02T08:48:07.592782+00:00", - "last_backup_file": "D:\\Backups\\AutoBackups\\LineaB\\Ssae04_14 - TIA\\2025-09-02\\10-48-04_projects.zip", - "backup_count": 1, - "last_successful_backup": "2025-09-02T08:48:07.592782+00:00" + "last_backup_date": "2025-09-02T10:52:24.172317+00:00", + "last_backup_file": "D:\\Backups\\AutoBackups\\LineaB\\Ssae04_14 - TIA\\2025-09-02\\12-52-22_projects.zip", + "backup_count": 2, + "last_successful_backup": "2025-09-02T10:52:24.172317+00:00" }, "hash_info": { "last_s7p_hash": "", @@ -132,7 +132,277 @@ "next_retry": null, "files_in_use": false, "exclusivity_check_passed": true, - "last_status_update": "2025-09-02T08:48:07.592782+00:00" + "last_status_update": "2025-09-02T10:52:24.172317+00:00" + }, + "discovery_info": { + "discovered_date": "", + "discovery_method": "", + "auto_discovered": true + } + }, + { + "id": "project_c437a7bb_20250902_125148", + "name": "Inverter", + "path": "C:\\Trabajo\\SIDEL\\05 - E5.007161 - Modifica O&U - SAE346\\InLavoro\\PLC\\Varios\\Inverter", + "type": "siemens_s7", + "s7p_file": "C:\\Trabajo\\SIDEL\\05 - E5.007161 - Modifica O&U - SAE346\\InLavoro\\PLC\\Varios\\Inverter\\InverterGSD.s7p", + "observation_directory": "C:/Trabajo/SIDEL/05 - E5.007161 - Modifica O&U - SAE346", + "relative_path": "InLavoro\\PLC\\Varios\\Inverter", + "backup_path": "InLavoro\\PLC\\Varios\\Inverter", + "schedule_config": { + "schedule": "daily", + "schedule_time": "02:00", + "enabled": true, + "next_scheduled_backup": "" + }, + "backup_history": { + "last_backup_date": "2025-09-02T10:52:22.919831+00:00", + "last_backup_file": "D:\\Backups\\AutoBackups\\InLavoro\\PLC\\Varios\\Inverter\\2025-09-02\\12-52-22_projects.zip", + "backup_count": 1, + "last_successful_backup": "2025-09-02T10:52:22.919831+00:00" + }, + "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": null, + "retry_count": 0, + "next_retry": null, + "files_in_use": false, + "exclusivity_check_passed": true, + "last_status_update": "2025-09-02T10:52:22.919831+00:00" + }, + "discovery_info": { + "discovered_date": "", + "discovery_method": "", + "auto_discovered": true + } + }, + { + "id": "project_315c5dfb_20250902_125148", + "name": "Ssae03_2", + "path": "C:\\Trabajo\\SIDEL\\05 - E5.007161 - Modifica O&U - SAE346\\InLavoro\\PLC\\Varios\\daSAP\\PLC SAE346\\Ssae03_2", + "type": "siemens_s7", + "s7p_file": "C:\\Trabajo\\SIDEL\\05 - E5.007161 - Modifica O&U - SAE346\\InLavoro\\PLC\\Varios\\daSAP\\PLC SAE346\\Ssae03_2\\Ssae0346.s7p", + "observation_directory": "C:/Trabajo/SIDEL/05 - E5.007161 - Modifica O&U - SAE346", + "relative_path": "InLavoro\\PLC\\Varios\\daSAP\\PLC SAE346\\Ssae03_2", + "backup_path": "InLavoro\\PLC\\Varios\\daSAP\\PLC SAE346\\Ssae03_2", + "schedule_config": { + "schedule": "daily", + "schedule_time": "02:00", + "enabled": true, + "next_scheduled_backup": "" + }, + "backup_history": { + "last_backup_date": "2025-09-02T10:52:23.703364+00:00", + "last_backup_file": "D:\\Backups\\AutoBackups\\InLavoro\\PLC\\Varios\\daSAP\\PLC SAE346\\Ssae03_2\\2025-09-02\\12-52-22_projects.zip", + "backup_count": 1, + "last_successful_backup": "2025-09-02T10:52:23.703364+00:00" + }, + "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": null, + "retry_count": 0, + "next_retry": null, + "files_in_use": false, + "exclusivity_check_passed": true, + "last_status_update": "2025-09-02T10:52:23.703364+00:00" + }, + "discovery_info": { + "discovered_date": "", + "discovery_method": "", + "auto_discovered": true + } + }, + { + "id": "project_8d1cfcd7_20250902_125148", + "name": "Rve116_PEPSI_OCTOBER L7", + "path": "C:\\Trabajo\\SIDEL\\05 - E5.007161 - Modifica O&U - SAE346\\InLavoro\\PLC\\Varios\\Filler\\Rve116_PEPSI_OCTOBER L7", + "type": "siemens_s7", + "s7p_file": "C:\\Trabajo\\SIDEL\\05 - E5.007161 - Modifica O&U - SAE346\\InLavoro\\PLC\\Varios\\Filler\\Rve116_PEPSI_OCTOBER L7\\Rve116.s7p", + "observation_directory": "C:/Trabajo/SIDEL/05 - E5.007161 - Modifica O&U - SAE346", + "relative_path": "InLavoro\\PLC\\Varios\\Filler\\Rve116_PEPSI_OCTOBER L7", + "backup_path": "InLavoro\\PLC\\Varios\\Filler\\Rve116_PEPSI_OCTOBER L7", + "schedule_config": { + "schedule": "daily", + "schedule_time": "02:00", + "enabled": true, + "next_scheduled_backup": "" + }, + "backup_history": { + "last_backup_date": "2025-09-02T10:52:23.563211+00:00", + "last_backup_file": "D:\\Backups\\AutoBackups\\InLavoro\\PLC\\Varios\\Filler\\Rve116_PEPSI_OCTOBER L7\\2025-09-02\\12-52-22_projects.zip", + "backup_count": 1, + "last_successful_backup": "2025-09-02T10:52:23.563211+00:00" + }, + "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": null, + "retry_count": 0, + "next_retry": null, + "files_in_use": false, + "exclusivity_check_passed": true, + "last_status_update": "2025-09-02T10:52:23.563211+00:00" + }, + "discovery_info": { + "discovered_date": "", + "discovery_method": "", + "auto_discovered": true + } + }, + { + "id": "project_42543170_20250902_125148", + "name": "TestCOM", + "path": "C:\\Trabajo\\SIDEL\\05 - E5.007161 - Modifica O&U - SAE346\\InLavoro\\PLC\\Varios\\Filler\\TestCOM", + "type": "siemens_s7", + "s7p_file": "C:\\Trabajo\\SIDEL\\05 - E5.007161 - Modifica O&U - SAE346\\InLavoro\\PLC\\Varios\\Filler\\TestCOM\\TestCOM.s7p", + "observation_directory": "C:/Trabajo/SIDEL/05 - E5.007161 - Modifica O&U - SAE346", + "relative_path": "InLavoro\\PLC\\Varios\\Filler\\TestCOM", + "backup_path": "InLavoro\\PLC\\Varios\\Filler\\TestCOM", + "schedule_config": { + "schedule": "daily", + "schedule_time": "02:00", + "enabled": true, + "next_scheduled_backup": "" + }, + "backup_history": { + "last_backup_date": "2025-09-02T10:52:23.522388+00:00", + "last_backup_file": "D:\\Backups\\AutoBackups\\InLavoro\\PLC\\Varios\\Filler\\TestCOM\\2025-09-02\\12-52-23_projects.zip", + "backup_count": 1, + "last_successful_backup": "2025-09-02T10:52:23.522388+00:00" + }, + "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": null, + "retry_count": 0, + "next_retry": null, + "files_in_use": false, + "exclusivity_check_passed": true, + "last_status_update": "2025-09-02T10:52:23.522388+00:00" + }, + "discovery_info": { + "discovered_date": "", + "discovery_method": "", + "auto_discovered": true + } + }, + { + "id": "project_86a0b02d_20250902_125148", + "name": "SAE446Mi", + "path": "C:\\Trabajo\\SIDEL\\05 - E5.007161 - Modifica O&U - SAE346\\InLavoro\\PLC\\Varios\\Last Backup\\SAE446Mi", + "type": "siemens_s7", + "s7p_file": "C:\\Trabajo\\SIDEL\\05 - E5.007161 - Modifica O&U - SAE346\\InLavoro\\PLC\\Varios\\Last Backup\\SAE446Mi\\SAE446Mi.s7p", + "observation_directory": "C:/Trabajo/SIDEL/05 - E5.007161 - Modifica O&U - SAE346", + "relative_path": "InLavoro\\PLC\\Varios\\Last Backup\\SAE446Mi", + "backup_path": "InLavoro\\PLC\\Varios\\Last Backup\\SAE446Mi", + "schedule_config": { + "schedule": "daily", + "schedule_time": "02:00", + "enabled": true, + "next_scheduled_backup": "" + }, + "backup_history": { + "last_backup_date": "2025-09-02T10:52:24.414156+00:00", + "last_backup_file": "D:\\Backups\\AutoBackups\\InLavoro\\PLC\\Varios\\Last Backup\\SAE446Mi\\2025-09-02\\12-52-23_projects.zip", + "backup_count": 1, + "last_successful_backup": "2025-09-02T10:52:24.414156+00:00" + }, + "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": null, + "retry_count": 0, + "next_retry": null, + "files_in_use": false, + "exclusivity_check_passed": true, + "last_status_update": "2025-09-02T10:52:24.414156+00:00" + }, + "discovery_info": { + "discovered_date": "", + "discovery_method": "", + "auto_discovered": true + } + }, + { + "id": "project_06372986_20250902_125148", + "name": "SAE446Mi", + "path": "C:\\Trabajo\\SIDEL\\05 - E5.007161 - Modifica O&U - SAE346\\Reporte\\LastWork\\PLC_Sae446_Mixer_20241217\\SAE446Mi", + "type": "siemens_s7", + "s7p_file": "C:\\Trabajo\\SIDEL\\05 - E5.007161 - Modifica O&U - SAE346\\Reporte\\LastWork\\PLC_Sae446_Mixer_20241217\\SAE446Mi\\SAE446Mi.s7p", + "observation_directory": "C:/Trabajo/SIDEL/05 - E5.007161 - Modifica O&U - SAE346", + "relative_path": "Reporte\\LastWork\\PLC_Sae446_Mixer_20241217\\SAE446Mi", + "backup_path": "Reporte\\LastWork\\PLC_Sae446_Mixer_20241217\\SAE446Mi", + "schedule_config": { + "schedule": "daily", + "schedule_time": "02:00", + "enabled": true, + "next_scheduled_backup": "" + }, + "backup_history": { + "last_backup_date": "2025-09-02T10:52:24.343664+00:00", + "last_backup_file": "D:\\Backups\\AutoBackups\\Reporte\\LastWork\\PLC_Sae446_Mixer_20241217\\SAE446Mi\\2025-09-02\\12-52-23_projects.zip", + "backup_count": 1, + "last_successful_backup": "2025-09-02T10:52:24.343664+00:00" + }, + "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": null, + "retry_count": 0, + "next_retry": null, + "files_in_use": false, + "exclusivity_check_passed": true, + "last_status_update": "2025-09-02T10:52:24.343664+00:00" }, "discovery_info": { "discovered_date": "", diff --git a/requirements.txt b/requirements.txt index e5a5749..60ea6e8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,6 +14,10 @@ psutil==5.9.5 # System utilities (disk space monitoring) filelock==3.12.4 # File locking for concurrent access # tkinter is included with Python standard library +# System Tray Integration +pystray==0.19.5 # System tray icon support for Windows +Pillow==11.3.0 # Image processing for icons + # Web Interface Jinja2==3.1.2 # Template engine for Flask diff --git a/setup.bat b/setup.bat new file mode 100644 index 0000000..db24dc1 --- /dev/null +++ b/setup.bat @@ -0,0 +1,51 @@ +@echo off +echo AutoBackups - Setup Script +echo ============================ +echo. + +echo [1/4] Activating conda environment... +call conda activate autobackups +if errorlevel 1 ( + echo Error: Could not activate conda environment 'autobackups' + echo. + echo Creating new environment... + call conda create --name autobackups python=3.12 -y + if errorlevel 1 ( + echo Error: Could not create conda environment + pause + exit /b 1 + ) + call conda activate autobackups +) + +echo [2/4] Installing Python dependencies... +pip install -r requirements.txt +if errorlevel 1 ( + echo Error: Could not install dependencies + pause + exit /b 1 +) + +echo [3/4] Checking favicon file... +if not exist "static\icons\favicon.png" ( + echo Warning: favicon.png not found in static\icons\ + echo System tray icon may not display correctly +) + +echo [4/4] Testing system tray functionality... +python -c "import pystray, PIL; print('✓ System tray dependencies OK')" +if errorlevel 1 ( + echo Error: System tray dependencies not working + pause + exit /b 1 +) + +echo. +echo ✓ Setup completed successfully! +echo. +echo Usage: +echo Regular mode: python src\app.py +echo System tray mode: python launch_tray.py +echo Or use: launch_with_tray.bat +echo. +pause diff --git a/src/app.py b/src/app.py index 9677b78..e1d426c 100644 --- a/src/app.py +++ b/src/app.py @@ -22,6 +22,7 @@ sys.path.insert(0, str(src_dir)) from models.config_model import Config from models.project_model import ProjectManager from utils.file_utils import DiskSpaceChecker +from utils.system_tray import SystemTrayManager from services.basic_backup_service import BasicBackupService from routes import register_api_routes, register_web_routes @@ -40,6 +41,7 @@ class AutoBackupsFlaskApp: self.backup_service = None self.scheduler = None self.logger = None + self.system_tray = None # Inicializar aplicación self._setup_logging() @@ -47,6 +49,7 @@ class AutoBackupsFlaskApp: self._initialize_services() self._setup_scheduler() self._register_routes() + self._setup_system_tray() def _setup_logging(self): """Configurar sistema de logging""" @@ -145,6 +148,31 @@ class AutoBackupsFlaskApp: register_api_routes(app, self) self.logger.info("Rutas Flask registradas desde módulos") + def _setup_system_tray(self): + """Inicializa el icono de system tray""" + try: + host = self.config.web_interface.get("host", "127.0.0.1") + port = self.config.web_interface.get("port", 5000) + + self.system_tray = SystemTrayManager(self, host, port) + self.logger.info("System tray configurado correctamente") + + except Exception as e: + self.logger.error(f"Error configurando system tray: {e}") + self.system_tray = None + + def start_system_tray(self): + """Inicia el icono de system tray""" + if self.system_tray: + success = self.system_tray.start_tray() + if success: + self.logger.info("System tray iniciado correctamente") + return True + else: + self.logger.warning("No se pudo iniciar el system tray") + return False + return False + def scheduled_project_scan(self): """Escaneo programado de proyectos""" try: @@ -250,19 +278,30 @@ def main(): debug = autobackups_app.config.web_interface.get("debug", False) print(f"\nServidor web iniciando en http://{host}:{port}") + + # Iniciar system tray + autobackups_app.start_system_tray() + print("Presiona Ctrl+C para detener el servidor") + print("Aplicación disponible en el system tray") # Iniciar servidor Flask app.run(host=host, port=port, debug=debug, use_reloader=False) except KeyboardInterrupt: print("\nAplicación interrumpida por el usuario") - if autobackups_app and autobackups_app.scheduler: - autobackups_app.scheduler.shutdown() + if autobackups_app: + if autobackups_app.scheduler: + autobackups_app.scheduler.shutdown() + if autobackups_app.system_tray: + autobackups_app.system_tray.stop_tray() except Exception as e: print(f"Error inesperado: {e}") - if autobackups_app and autobackups_app.scheduler: - autobackups_app.scheduler.shutdown() + if autobackups_app: + if autobackups_app.scheduler: + autobackups_app.scheduler.shutdown() + if autobackups_app.system_tray: + autobackups_app.system_tray.stop_tray() sys.exit(1) diff --git a/src/utils/system_tray.py b/src/utils/system_tray.py new file mode 100644 index 0000000..f9a02cb --- /dev/null +++ b/src/utils/system_tray.py @@ -0,0 +1,154 @@ +""" +System Tray functionality for AutoBackups application +Provides system tray icon with options to open browser and close application +""" + +import threading +import webbrowser +import os +from pathlib import Path +import pystray +from PIL import Image +import logging + + +class SystemTrayManager: + """Manages system tray icon and menu for AutoBackups""" + + def __init__(self, app_instance, host="127.0.0.1", port=5000): + """ + Initialize system tray manager + + Args: + app_instance: The main AutoBackups Flask application instance + host: Web server host (default: 127.0.0.1) + port: Web server port (default: 5000) + """ + self.app_instance = app_instance + self.host = host + self.port = port + self.icon = None + self.logger = logging.getLogger(__name__) + + # Get icon path + self.icon_path = self._get_icon_path() + + def _get_icon_path(self): + """Get the path to the favicon.png icon""" + current_dir = Path(__file__).parent.parent.parent + icon_path = current_dir / "static" / "icons" / "favicon.png" + + if not icon_path.exists(): + self.logger.warning(f"Icon file not found at {icon_path}") + return None + + return str(icon_path) + + def _load_icon_image(self): + """Load the icon image from file""" + if not self.icon_path: + # Create a simple default icon if file not found + return Image.new("RGB", (64, 64), color="blue") + + try: + return Image.open(self.icon_path) + except Exception as e: + self.logger.error(f"Error loading icon image: {e}") + # Return a simple default icon + return Image.new("RGB", (64, 64), color="blue") + + def _open_browser(self, icon, item): + """Open web browser to the application URL""" + url = f"http://{self.host}:{self.port}" + try: + webbrowser.open(url) + self.logger.info(f"Opened browser to {url}") + except Exception as e: + self.logger.error(f"Error opening browser: {e}") + + def _quit_application(self, icon, item): + """Quit the application""" + try: + self.logger.info("Shutting down application from system tray") + + # Stop the scheduler if it exists + if ( + self.app_instance + and hasattr(self.app_instance, "scheduler") + and self.app_instance.scheduler + ): + self.app_instance.scheduler.shutdown(wait=False) + + # Stop the system tray icon + if self.icon: + self.icon.stop() + + # Exit the application + os._exit(0) + + except Exception as e: + self.logger.error(f"Error during application shutdown: {e}") + os._exit(1) + + def _show_status(self, icon, item): + """Show application status (placeholder for future implementation)""" + # This could show a status window or notification in the future + self.logger.info("Status requested from system tray") + + def create_tray_icon(self): + """Create and configure the system tray icon""" + try: + # Load the icon image + image = self._load_icon_image() + + # Create menu items + menu = pystray.Menu( + pystray.MenuItem( + "Open AutoBackups", + self._open_browser, + # This will be the action when double-clicking the icon + default=True, + ), + pystray.MenuItem("Status", self._show_status), + pystray.Menu.SEPARATOR, + pystray.MenuItem("Exit", self._quit_application), + ) + + # Create the icon + self.icon = pystray.Icon( + "AutoBackups", image, "AutoBackups - Automatic Backup System", menu + ) + + self.logger.info("System tray icon created successfully") + return self.icon + + except Exception as e: + self.logger.error(f"Error creating system tray icon: {e}") + return None + + def start_tray(self): + """Start the system tray icon in a separate thread""" + if not self.icon: + self.icon = self.create_tray_icon() + + if self.icon: + try: + # Run the tray icon in a separate thread + tray_thread = threading.Thread(target=self.icon.run, daemon=True) + tray_thread.start() + self.logger.info("System tray started successfully") + return True + except Exception as e: + self.logger.error(f"Error starting system tray: {e}") + return False + + return False + + def stop_tray(self): + """Stop the system tray icon""" + if self.icon: + try: + self.icon.stop() + self.logger.info("System tray stopped") + except Exception as e: + self.logger.error(f"Error stopping system tray: {e}") diff --git a/templates/base.html b/templates/base.html index 1ca9d71..5fd1a4c 100644 --- a/templates/base.html +++ b/templates/base.html @@ -5,6 +5,9 @@