Problemas
This commit is contained in:
parent
be61956a54
commit
581aa551c4
|
@ -14,7 +14,7 @@ AutoBackups is a Python application designed to automate the backup of Simatic S
|
|||
|
||||
## Project Structure
|
||||
```
|
||||
|
||||
autobackups
|
||||
├── .logs/ # Application logs with startup timestamps
|
||||
├── src
|
||||
│ ├── app.py # Main entry point of the Flask application
|
|
@ -0,0 +1,258 @@
|
|||
# AutoBackups - Especificación Técnica Detallada
|
||||
|
||||
## Resumen Ejecutivo
|
||||
AutoBackups es una aplicación Python que automatiza el backup de proyectos Simatic S7 y directorios definidos por el usuario, utilizando la API de Everything para búsqueda eficiente de archivos y un sistema de hash inteligente para evitar backups innecesarios.
|
||||
|
||||
## Arquitectura del Sistema
|
||||
|
||||
### 1. Componentes Principales
|
||||
|
||||
#### 1.1 Motor de Búsqueda (Everything API)
|
||||
- **Librería**: PyEverything wrapper
|
||||
- **DLL Local**: `Everything-SDK\dll\Everything64.dll`
|
||||
- **Función**: Localizar archivos *.s7p en directorios de observación
|
||||
- **Optimización**: Evitar último nivel del árbol para archivos .s7p
|
||||
- **Ventaja**: Aprovecha el índice existente de Everything para búsquedas instantáneas
|
||||
|
||||
#### 1.2 Sistema de Hash en Dos Etapas
|
||||
**Etapa 1**: Hash rápido de archivos *.s7p
|
||||
- Solo verifica MD5 de (timestamp + tamaño) de archivos .s7p
|
||||
- Si no hay cambios → no se procede con backup
|
||||
- Si hay cambios → procede a Etapa 2
|
||||
|
||||
**Etapa 2**: Hash completo del proyecto
|
||||
- Calcula MD5 de (timestamp + tamaño) de todos los archivos del directorio
|
||||
- Compara con último hash almacenado
|
||||
- Solo hace backup si hay diferencias
|
||||
|
||||
#### 1.3 Scheduler de Background
|
||||
- **daily**: Una vez al día a hora configurada
|
||||
- **hourly**: Cada hora
|
||||
- **3-hour**: Cada 3 horas
|
||||
- **7-hour**: Cada 7 horas
|
||||
- **startup**: Al iniciar la aplicación
|
||||
- **manual**: Solo bajo demanda del usuario
|
||||
- **Escaneos**: Automáticos cada 1 hora
|
||||
- **Backups**: Mínimo cada 10 minutos
|
||||
- **Prioridad**: Baja prioridad de sistema
|
||||
|
||||
### 2. Estructura de Datos
|
||||
|
||||
#### 2.1 config.json (Configuración Global)
|
||||
```json
|
||||
{
|
||||
"observation_directories": [
|
||||
{
|
||||
"path": "C:\\Projects\\Siemens",
|
||||
"type": "siemens_s7",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"path": "D:\\Engineering\\Projects",
|
||||
"type": "siemens_s7",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"path": "C:\\Important\\Docs",
|
||||
"type": "manual",
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"backup_destination": "D:\\Backups\\AutoBackups",
|
||||
"default_schedule": "daily",
|
||||
"default_schedule_time": "02:00",
|
||||
"retry_delay_hours": 1,
|
||||
"web_interface": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 5000
|
||||
},
|
||||
"logging": {
|
||||
"level": "INFO",
|
||||
"max_log_files": 30
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.2 projects.json (Estado de Proyectos)
|
||||
```json
|
||||
{
|
||||
"projects": [
|
||||
{
|
||||
"id": "project_001",
|
||||
"name": "PLC_MainLine",
|
||||
"path": "C:\\Projects\\Siemens\\PLC_MainLine",
|
||||
"type": "siemens_s7",
|
||||
"s7p_file": "C:\\Projects\\Siemens\\PLC_MainLine\\PLC_MainLine.s7p",
|
||||
"schedule": "daily",
|
||||
"schedule_time": "02:00",
|
||||
"enabled": true,
|
||||
"last_backup": "2025-09-01T02:15:30",
|
||||
"last_s7p_hash": "abc123def456",
|
||||
"last_full_hash": "def789ghi012",
|
||||
"last_s7p_timestamp": "2025-08-31T14:30:00",
|
||||
"status": "ready",
|
||||
"retry_count": 0,
|
||||
"next_retry": null,
|
||||
"error_message": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Flujo de Operación
|
||||
|
||||
#### 3.1 Inicio de Aplicación
|
||||
1. Cargar configuración desde `config.json`
|
||||
2. Cargar estado de proyectos desde `projects.json`
|
||||
3. Inicializar logger con timestamp de inicio
|
||||
4. Escanear directorios de observación usando Everything API
|
||||
5. Actualizar lista de proyectos detectados
|
||||
6. Iniciar scheduler
|
||||
7. Iniciar interfaz web Flask
|
||||
|
||||
#### 4.2 Proceso de Backup
|
||||
1. **Verificación de Espacio**: Verificar mínimo 100MB libres
|
||||
2. **Verificación de Acceso .s7p**: Intentar comprimir solo archivo .s7p
|
||||
3. **Hash Etapa 1**: Verificar MD5 de (timestamp + tamaño) del archivo .s7p
|
||||
4. **Hash Etapa 2**: Si Etapa 1 detecta cambios, verificar hash completo
|
||||
5. **Compresión**: Crear archivo ZIP con estructura específica preservada
|
||||
6. **Almacenamiento**: Guardar en `backup_destination/ProjectPath/YYYY-MM-DD/HH-MM-SS_projects.zip`
|
||||
7. **Actualización**: Actualizar hashes y timestamps en `projects.json`
|
||||
|
||||
#### 3.3 Gestión de Errores
|
||||
- **Archivos en uso**: Marcar proyecto para reintento en 1 hora
|
||||
- **Errores de permisos**: Log de error y notificación en interfaz
|
||||
- **Errores de espacio**: Verificar espacio disponible antes de backup
|
||||
|
||||
### 4. Interfaz Web
|
||||
|
||||
#### 4.1 Dashboard Principal
|
||||
- **Lista de Proyectos**: Tabla con estado, último backup, próximo backup
|
||||
- **Controles Globales**: Backup manual global, pausar/reanudar scheduler
|
||||
- **Estadísticas**: Total de proyectos, backups exitosos, errores
|
||||
|
||||
#### 4.2 Configuración de Proyectos
|
||||
- **Habilitación/Deshabilitación**: Toggle por proyecto
|
||||
- **Configuración de Schedule**: Dropdown con opciones de frecuencia
|
||||
- **Backup Manual**: Botón de backup inmediato por proyecto
|
||||
|
||||
#### 4.3 Logs y Monitoreo
|
||||
- **Log Viewer**: Mostrar logs recientes con filtros por nivel
|
||||
- **Estado del Sistema**: Información de Everything API, espacio en disco
|
||||
|
||||
### 5. Estructura de Backup
|
||||
|
||||
#### 5.1 Nomenclatura de Archivos (Estructura Específica)
|
||||
```
|
||||
backup_destination/
|
||||
├── LineA/
|
||||
│ └── Project1/
|
||||
│ ├── 2025-09-01/
|
||||
│ │ ├── 02-15-30_projects.zip
|
||||
│ │ └── 14-30-15_projects.zip
|
||||
│ └── 2025-09-02/
|
||||
│ └── 02-15-45_projects.zip
|
||||
└── LineB/
|
||||
└── Project2/
|
||||
└── 2025-09-01/
|
||||
└── 08-20-10_projects.zip
|
||||
```
|
||||
|
||||
#### 5.2 Contenido del ZIP
|
||||
```
|
||||
02-15-30_projects.zip
|
||||
└── LineA/
|
||||
└── Project1/
|
||||
├── project.s7p
|
||||
├── subfolder1/
|
||||
└── subfolder2/
|
||||
```
|
||||
|
||||
### 6. Tecnologías y Librerías
|
||||
|
||||
#### 6.1 Python Core
|
||||
- **Python**: 3.12
|
||||
- **Framework Web**: Flask
|
||||
- **Scheduler**: APScheduler
|
||||
- **Compresión**: zipfile (biblioteca estándar)
|
||||
|
||||
#### 6.2 Librerías Específicas
|
||||
- **Everything API**: PyEverything wrapper con DLL local
|
||||
- **Hashing**: hashlib MD5 para (timestamp + tamaño)
|
||||
- **Config**: json (biblioteca estándar)
|
||||
- **Logging**: logging (biblioteca estándar)
|
||||
- **File Access Check**: Intento de compresión + verificación .s7p
|
||||
- **Process Priority**: pywin32 para baja prioridad
|
||||
- **Disk Space**: psutil para monitoreo de espacio libre
|
||||
|
||||
#### 6.3 Frontend
|
||||
- **HTML/CSS/JavaScript**: Vanilla (sin React por simplicidad)
|
||||
- **AJAX**: Para comunicación con Flask API
|
||||
- **Bootstrap**: Para estilizado responsivo
|
||||
|
||||
### 7. Consideraciones de Seguridad
|
||||
|
||||
#### 7.1 Acceso a Archivos
|
||||
- Verificación de permisos antes de backup
|
||||
- Manejo seguro de rutas para evitar path traversal
|
||||
- Logs de todos los accesos a archivos
|
||||
|
||||
#### 7.2 Interfaz Web
|
||||
- Solo acceso local (127.0.0.1)
|
||||
- No autenticación requerida (uso local únicamente)
|
||||
- Validación de inputs en formularios
|
||||
|
||||
### 8. Logging y Monitoreo
|
||||
|
||||
#### 8.1 Estructura de Logs
|
||||
```
|
||||
.logs/
|
||||
├── autobackups_2025-09-01_08-30-15.log
|
||||
├── autobackups_2025-09-02_08-30-22.log
|
||||
└── ...
|
||||
```
|
||||
|
||||
#### 8.2 Niveles de Log
|
||||
- **INFO**: Inicio/fin de backups, descubrimiento de proyectos
|
||||
- **WARNING**: Archivos en uso, reintentos
|
||||
- **ERROR**: Fallos de backup, errores de configuración
|
||||
- **DEBUG**: Detalles de hashes, operaciones de archivos
|
||||
|
||||
### 9. Instalación y Despliegue
|
||||
|
||||
#### 9.1 Entorno Miniconda
|
||||
```bash
|
||||
conda create --name autobackups python=3.12
|
||||
conda activate autobackups
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
#### 9.2 Ejecución
|
||||
- **Desarrollo**: `python src/app.py`
|
||||
- **Producción**: `pythonw.exe src/app.py` (sin consola)
|
||||
- **Como servicio**: Futuro enhancement con `python-windows-service`
|
||||
|
||||
### 10. Roadmap de Desarrollo
|
||||
|
||||
#### Fase 1: Core Backend
|
||||
- [ ] Integración con Everything API
|
||||
- [ ] Sistema de hash en dos etapas
|
||||
- [ ] Motor de backup básico
|
||||
- [ ] Scheduler con APScheduler
|
||||
|
||||
#### Fase 2: Interfaz Web
|
||||
- [ ] Dashboard básico con Flask
|
||||
- [ ] API REST para operaciones CRUD
|
||||
- [ ] Frontend con HTML/JS/Bootstrap
|
||||
|
||||
#### Fase 3: Características Avanzadas
|
||||
- [ ] Logs web viewer
|
||||
- [ ] Configuración avanzada de directorios
|
||||
- [ ] Estadísticas y reportes
|
||||
|
||||
#### Fase 4: Optimizaciones
|
||||
- [ ] Ejecución como servicio Windows
|
||||
- [ ] Notificaciones por email
|
||||
- [ ] Backup incremental
|
||||
|
||||
Este documento será la base para el desarrollo del proyecto AutoBackups.
|
|
@ -0,0 +1,124 @@
|
|||
# AutoBackups - Preguntas Técnicas Pendientes
|
||||
|
||||
## Preguntas Críticas para el Desarrollo
|
||||
|
||||
### 1. Everything API - Integración
|
||||
**Pregunta**: ¿Everything está instalado y funcionando en el sistema objetivo?
|
||||
**Impacto**: Si no está disponible, necesitaremos un método alternativo de búsqueda de archivos.
|
||||
**Opciones**:
|
||||
- Usar `python-everything` si Everything está disponible
|
||||
- Implementar búsqueda recursiva con `os.walk()` como fallback
|
||||
- Usar `pathlib` con `glob` patterns
|
||||
|
||||
**Decisión necesaria**: ¿Podemos asumir que Everything estará disponible o necesitamos fallback?
|
||||
|
||||
### 2. Estructura de Backup - Clarificación de Rutas
|
||||
**Pregunta**: Si tenemos un archivo en `C:\Projects\Siemens\LineA\Project1\project.s7p`, ¿el backup debe ser?
|
||||
**Opción A**:
|
||||
```
|
||||
backup_destination/2025-09-01_14-30-15/Projects_Siemens_LineA_Project1.zip
|
||||
└── Projects/Siemens/LineA/Project1/...
|
||||
```
|
||||
**Opción B**:
|
||||
```
|
||||
backup_destination/2025-09-01_14-30-15/LineA_Project1.zip
|
||||
└── LineA/Project1/...
|
||||
```
|
||||
|
||||
**Decisión necesaria**: ¿Qué estructura prefieres?
|
||||
|
||||
### 3. Configuración de Schedules - Granularidad
|
||||
**Pregunta**: ¿Los schedules deben ser configurables a nivel de:
|
||||
- Por directorio de observación (todos los proyectos de un directorio)
|
||||
- Por proyecto individual
|
||||
- Ambos (global con override por proyecto)
|
||||
|
||||
**Decisión necesaria**: ¿Qué nivel de granularidad necesitas?
|
||||
|
||||
### 4. Hash de Archivos - Algoritmo y Almacenamiento
|
||||
**Pregunta**: Para el sistema de hash en dos etapas:
|
||||
- ¿Usamos MD5 (rápido) o SHA256 (más seguro) para los hashes?
|
||||
- ¿El hash incluye solo timestamp o también tamaño de archivo?
|
||||
- ¿Cómo manejamos archivos que se mueven dentro del proyecto?
|
||||
|
||||
**Decisión necesaria**: ¿Qué balance entre velocidad y precisión prefieres?
|
||||
|
||||
### 5. Detección de Archivos en Uso - Método
|
||||
**Pregunta**: Para detectar archivos bloqueados, ¿prefieres:
|
||||
**Opción A**: Intentar abrir cada archivo en modo escritura exclusiva
|
||||
**Opción B**: Usar `lsof` (Linux) / `handle.exe` (Windows) para listar archivos abiertos
|
||||
**Opción C**: Solo verificar el archivo .s7p principal
|
||||
|
||||
**Decisión necesaria**: ¿Qué método consideras más apropiado?
|
||||
|
||||
### 6. Manejo de Proyectos Grandes - Performance
|
||||
**Pregunta**: ¿Hay límites que debamos considerar?
|
||||
- Tamaño máximo de proyecto para backup
|
||||
- Tiempo máximo de backup
|
||||
- Número máximo de archivos por proyecto
|
||||
|
||||
**Decisión necesaria**: ¿Necesitamos algún tipo de throttling o límites?
|
||||
|
||||
### 7. Configuración de Directorios - Recursividad
|
||||
**Pregunta**: Cuando especificamos un directorio de observación como `C:\Projects`, ¿debemos:
|
||||
- Buscar solo en subdirectorios inmediatos
|
||||
- Buscar recursivamente en toda la jerarquía
|
||||
- Permitir configurar la profundidad de búsqueda
|
||||
|
||||
**Decisión necesaria**: ¿Qué comportamiento prefieres por defecto?
|
||||
|
||||
### 8. Backup Incremental vs Completo
|
||||
**Pregunta**: ¿Todos los backups deben ser completos o consideras backup incremental?
|
||||
- Backup completo: Todo el proyecto cada vez
|
||||
- Backup incremental: Solo archivos modificados
|
||||
|
||||
**Decisión necesaria**: ¿Preferencia por simplicidad o eficiencia de espacio?
|
||||
|
||||
### 9. Web Interface - Características Específicas
|
||||
**Pregunta**: ¿Qué funcionalidades son prioritarias en la interfaz web?
|
||||
**Must-have**:
|
||||
- Lista de proyectos con estado
|
||||
- Trigger manual de backup
|
||||
- Ver logs básicos
|
||||
|
||||
**Nice-to-have**:
|
||||
- Configurar schedules por proyecto
|
||||
- Ver progreso de backup en tiempo real
|
||||
- Estadísticas históricas
|
||||
- Configurar nuevos directorios
|
||||
|
||||
**Decisión necesaria**: ¿Cuáles son must-have vs nice-to-have?
|
||||
|
||||
### 10. Error Handling - Estrategias de Recuperación
|
||||
**Pregunta**: ¿Cómo manejar errores específicos?
|
||||
- Espacio insuficiente en destino de backup
|
||||
- Pérdida de conexión con Everything
|
||||
- Corrupción de archivos de configuración
|
||||
- Falla durante compresión
|
||||
|
||||
**Decisión necesaria**: ¿Qué nivel de robustez necesitas?
|
||||
|
||||
### 11. Startup Behavior - Inicialización
|
||||
**Pregunta**: Al iniciar la aplicación:
|
||||
- ¿Debe hacer un escaneo completo inmediatamente?
|
||||
- ¿Debe esperar al primer schedule programado?
|
||||
- ¿Debe permitir configurar el comportamiento de startup?
|
||||
|
||||
**Decisión necesaria**: ¿Qué comportamiento prefieres al iniciar?
|
||||
|
||||
### 12. Multi-threading - Concurrencia
|
||||
**Pregunta**: ¿Los backups deben ejecutarse:
|
||||
- Secuencialmente (uno por vez)
|
||||
- En paralelo (múltiples simultáneos)
|
||||
- Con límite configurable de concurrencia
|
||||
|
||||
**Decisión necesaria**: ¿Qué balance entre velocidad y recursos del sistema?
|
||||
|
||||
## Próximos Pasos
|
||||
Una vez que tengas respuestas a estas preguntas, podremos:
|
||||
1. Finalizar el diseño técnico
|
||||
2. Crear el `requirements.txt` definitivo
|
||||
3. Comenzar con la implementación por fases
|
||||
4. Definir el plan de testing
|
||||
|
||||
¿Podrías revisar estas preguntas y darme tus preferencias para cada una?
|
|
@ -0,0 +1,60 @@
|
|||
{
|
||||
"observation_directories": [
|
||||
{
|
||||
"path": "C:\\Projects\\Siemens",
|
||||
"type": "siemens_s7",
|
||||
"enabled": true,
|
||||
"description": "Directorio principal de proyectos Siemens"
|
||||
},
|
||||
{
|
||||
"path": "D:\\Engineering\\Projects",
|
||||
"type": "siemens_s7",
|
||||
"enabled": true,
|
||||
"description": "Proyectos de ingeniería adicionales"
|
||||
},
|
||||
{
|
||||
"path": "C:\\Important\\Documentation",
|
||||
"type": "manual",
|
||||
"enabled": true,
|
||||
"description": "Documentación importante para backup manual"
|
||||
}
|
||||
],
|
||||
"backup_destination": "D:\\Backups\\AutoBackups",
|
||||
"global_settings": {
|
||||
"default_schedule": "daily",
|
||||
"default_schedule_time": "02:00",
|
||||
"retry_delay_hours": 1,
|
||||
"backup_timeout_minutes": 0,
|
||||
"min_free_space_mb": 100,
|
||||
"scan_interval_minutes": 60,
|
||||
"min_backup_interval_minutes": 10
|
||||
},
|
||||
"everything_api": {
|
||||
"dll_path": "Everything-SDK\\dll\\Everything64.dll",
|
||||
"enabled": true,
|
||||
"search_depth": -1,
|
||||
"skip_last_level_for_s7p": true
|
||||
},
|
||||
"web_interface": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 5000,
|
||||
"debug": false
|
||||
},
|
||||
"logging": {
|
||||
"level": "INFO",
|
||||
"max_log_files": 30,
|
||||
"log_rotation_days": 30
|
||||
},
|
||||
"backup_options": {
|
||||
"compression_level": 6,
|
||||
"include_subdirectories": true,
|
||||
"preserve_directory_structure": true,
|
||||
"hash_algorithm": "md5",
|
||||
"hash_includes": ["timestamp", "size"],
|
||||
"backup_type": "complete",
|
||||
"process_priority": "low",
|
||||
"sequential_execution": true,
|
||||
"filename_format": "HH-MM-SS_projects.zip",
|
||||
"date_format": "YYYY-MM-DD"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
{
|
||||
"metadata": {
|
||||
"version": "1.0",
|
||||
"last_updated": "2025-09-01T08:30:15Z",
|
||||
"total_projects": 0
|
||||
},
|
||||
"projects": [
|
||||
{
|
||||
"id": "example_project_001",
|
||||
"name": "PLC_MainLine_Example",
|
||||
"path": "C:\\Projects\\Siemens\\LineA\\Project1",
|
||||
"type": "siemens_s7",
|
||||
"s7p_file": "C:\\Projects\\Siemens\\LineA\\Project1\\project.s7p",
|
||||
"observation_directory": "C:\\Projects\\Siemens",
|
||||
"relative_path": "LineA\\Project1",
|
||||
"backup_path": "LineA\\Project1",
|
||||
"schedule_config": {
|
||||
"schedule": "daily",
|
||||
"schedule_time": "02:00",
|
||||
"enabled": true,
|
||||
"next_scheduled_backup": "2025-09-02T02:00:00Z"
|
||||
},
|
||||
"backup_history": {
|
||||
"last_backup_date": "2025-09-01T02:15:30Z",
|
||||
"last_backup_file": "D:\\Backups\\AutoBackups\\LineA\\Project1\\2025-09-01\\02-15-30_projects.zip",
|
||||
"backup_count": 5,
|
||||
"last_successful_backup": "2025-09-01T02:15:30Z"
|
||||
},
|
||||
"hash_info": {
|
||||
"last_s7p_hash": "abc123def456789",
|
||||
"last_full_hash": "def789ghi012345",
|
||||
"last_s7p_timestamp": "2025-08-31T14:30:00Z",
|
||||
"last_s7p_size": 2048576,
|
||||
"last_scan_timestamp": "2025-09-01T02:10:00Z",
|
||||
"file_count": 1247,
|
||||
"total_size_bytes": 125847296
|
||||
},
|
||||
"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-01T02:15:35Z"
|
||||
},
|
||||
"discovery_info": {
|
||||
"discovered_date": "2025-09-01T08:30:15Z",
|
||||
"discovery_method": "everything_api",
|
||||
"auto_discovered": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"statistics": {
|
||||
"total_backups_created": 15,
|
||||
"total_backup_size_mb": 2450.5,
|
||||
"average_backup_time_seconds": 45.2,
|
||||
"last_global_scan": "2025-09-01T08:30:15Z",
|
||||
"projects_with_errors": 0,
|
||||
"projects_pending_retry": 0
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
# Core Framework
|
||||
Flask==2.3.3
|
||||
Werkzeug==2.3.7
|
||||
|
||||
# Scheduling
|
||||
APScheduler==3.10.4
|
||||
|
||||
# Everything API Integration
|
||||
PyEverything==1.0.1 # Wrapper para Everything SDK
|
||||
|
||||
# File Operations and Utilities
|
||||
pathlib2==2.3.7.post1 # Enhanced pathlib for Python 3.12
|
||||
psutil==5.9.5 # System utilities (disk space monitoring)
|
||||
filelock==3.12.4 # File locking for concurrent access
|
||||
|
||||
# Web Interface
|
||||
Jinja2==3.1.2 # Template engine for Flask
|
||||
|
||||
# Configuration and Data
|
||||
jsonschema==4.19.1 # JSON schema validation
|
||||
|
||||
# Logging
|
||||
colorlog==6.7.0 # Colored logging output
|
||||
|
||||
# Windows-specific dependencies
|
||||
pywin32==306; sys_platform == "win32" # Windows API access for low priority
|
||||
|
||||
# Development and Testing (opcional, remove for production)
|
||||
pytest==7.4.2
|
||||
pytest-flask==1.2.0
|
||||
black==23.9.1 # Code formatting
|
||||
flake8==6.1.0 # Code linting
|
|
@ -0,0 +1,216 @@
|
|||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[codz]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# UV
|
||||
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
#uv.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
#poetry.toml
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
||||
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
||||
#pdm.lock
|
||||
#pdm.toml
|
||||
.pdm-python
|
||||
.pdm-build/
|
||||
|
||||
# pixi
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
||||
#pixi.lock
|
||||
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
||||
# in the .venv directory. It is recommended not to include this directory in version control.
|
||||
.pixi
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# Redis
|
||||
*.rdb
|
||||
*.aof
|
||||
*.pid
|
||||
|
||||
# RabbitMQ
|
||||
mnesia/
|
||||
rabbitmq/
|
||||
rabbitmq-data/
|
||||
|
||||
# ActiveMQ
|
||||
activemq-data/
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.envrc
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
# Abstra
|
||||
# Abstra is an AI-powered process automation framework.
|
||||
# Ignore directories containing user credentials, local state, and settings.
|
||||
# Learn more at https://abstra.io/docs
|
||||
.abstra/
|
||||
|
||||
# Visual Studio Code
|
||||
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
||||
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
||||
# you could uncomment the following to ignore the entire vscode folder
|
||||
# .vscode/
|
||||
|
||||
# Ruff stuff:
|
||||
.ruff_cache/
|
||||
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
# Marimo
|
||||
marimo/_static/
|
||||
marimo/_lsp/
|
||||
__marimo__/
|
||||
|
||||
# Streamlit
|
||||
.streamlit/secrets.toml
|
|
@ -0,0 +1,72 @@
|
|||
# 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.
|
||||
|
||||
## Features
|
||||
- Monitors specified directories for Simatic S7 files (*.s7p) and other user-defined directories.
|
||||
- Automatically compresses and backs up projects on a configurable schedule (daily, hourly, 3-hour, 7-hour intervals, startup, or manual).
|
||||
- Supports manual backup triggers.
|
||||
- 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.
|
||||
- Comprehensive logging system with timestamped log files.
|
||||
|
||||
## Project Structure
|
||||
```
|
||||
autobackups
|
||||
├── .logs/ # Application logs with startup timestamps
|
||||
├── src
|
||||
│ ├── app.py # Main entry point of the Flask application
|
||||
│ ├── models
|
||||
│ │ └── __init__.py # Data models for configuration and directories
|
||||
│ ├── routes
|
||||
│ │ └── __init__.py # API routes for backup operations
|
||||
│ ├── services
|
||||
│ │ └── __init__.py # Business logic for backups
|
||||
│ └── utils
|
||||
│ └── __init__.py # Utility functions for hashing and file access
|
||||
├── static
|
||||
│ ├── css # CSS files for styling
|
||||
│ └── 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
|
||||
├── requirements.txt # Python dependencies
|
||||
└── README.md # Project documentation
|
||||
```
|
||||
|
||||
## Installation
|
||||
1. Clone the repository:
|
||||
```
|
||||
git clone <repository-url>
|
||||
cd autobackups
|
||||
```
|
||||
|
||||
2. Create a Miniconda environment:
|
||||
```
|
||||
conda create --name autobackups python=3.12
|
||||
conda activate autobackups
|
||||
```
|
||||
|
||||
3. Install the required packages:
|
||||
```
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
4. Configure the application by editing `config.json` to specify directories to monitor and backup settings.
|
||||
|
||||
## Usage
|
||||
- Start the Flask application:
|
||||
```
|
||||
python src/app.py
|
||||
```
|
||||
|
||||
- Access the web interface at `http://localhost:5000` to configure settings and monitor backup statuses.
|
||||
|
||||
## Contributing
|
||||
Contributions are welcome! Please submit a pull request or open an issue for any enhancements or bug fixes.
|
||||
|
||||
## License
|
||||
This project is licensed under the MIT License. See the LICENSE file for details.
|
|
@ -0,0 +1,124 @@
|
|||
# AutoBackups - Preguntas Técnicas Pendientes
|
||||
|
||||
## Preguntas Críticas para el Desarrollo
|
||||
|
||||
### 1. Everything API - Integración
|
||||
**Pregunta**: ¿Everything está instalado y funcionando en el sistema objetivo?
|
||||
**Impacto**: Si no está disponible, necesitaremos un método alternativo de búsqueda de archivos.
|
||||
**Opciones**:
|
||||
- Usar `python-everything` si Everything está disponible
|
||||
- Implementar búsqueda recursiva con `os.walk()` como fallback
|
||||
- Usar `pathlib` con `glob` patterns
|
||||
|
||||
**Decisión necesaria**: ¿Podemos asumir que Everything estará disponible o necesitamos fallback?
|
||||
|
||||
### 2. Estructura de Backup - Clarificación de Rutas
|
||||
**Pregunta**: Si tenemos un archivo en `C:\Projects\Siemens\LineA\Project1\project.s7p`, ¿el backup debe ser?
|
||||
**Opción A**:
|
||||
```
|
||||
backup_destination/2025-09-01_14-30-15/Projects_Siemens_LineA_Project1.zip
|
||||
└── Projects/Siemens/LineA/Project1/...
|
||||
```
|
||||
**Opción B**:
|
||||
```
|
||||
backup_destination/2025-09-01_14-30-15/LineA_Project1.zip
|
||||
└── LineA/Project1/...
|
||||
```
|
||||
|
||||
**Decisión necesaria**: ¿Qué estructura prefieres?
|
||||
|
||||
### 3. Configuración de Schedules - Granularidad
|
||||
**Pregunta**: ¿Los schedules deben ser configurables a nivel de:
|
||||
- Por directorio de observación (todos los proyectos de un directorio)
|
||||
- Por proyecto individual
|
||||
- Ambos (global con override por proyecto)
|
||||
|
||||
**Decisión necesaria**: ¿Qué nivel de granularidad necesitas?
|
||||
|
||||
### 4. Hash de Archivos - Algoritmo y Almacenamiento
|
||||
**Pregunta**: Para el sistema de hash en dos etapas:
|
||||
- ¿Usamos MD5 (rápido) o SHA256 (más seguro) para los hashes?
|
||||
- ¿El hash incluye solo timestamp o también tamaño de archivo?
|
||||
- ¿Cómo manejamos archivos que se mueven dentro del proyecto?
|
||||
|
||||
**Decisión necesaria**: ¿Qué balance entre velocidad y precisión prefieres?
|
||||
|
||||
### 5. Detección de Archivos en Uso - Método
|
||||
**Pregunta**: Para detectar archivos bloqueados, ¿prefieres:
|
||||
**Opción A**: Intentar abrir cada archivo en modo escritura exclusiva
|
||||
**Opción B**: Usar `lsof` (Linux) / `handle.exe` (Windows) para listar archivos abiertos
|
||||
**Opción C**: Solo verificar el archivo .s7p principal
|
||||
|
||||
**Decisión necesaria**: ¿Qué método consideras más apropiado?
|
||||
|
||||
### 6. Manejo de Proyectos Grandes - Performance
|
||||
**Pregunta**: ¿Hay límites que debamos considerar?
|
||||
- Tamaño máximo de proyecto para backup
|
||||
- Tiempo máximo de backup
|
||||
- Número máximo de archivos por proyecto
|
||||
|
||||
**Decisión necesaria**: ¿Necesitamos algún tipo de throttling o límites?
|
||||
|
||||
### 7. Configuración de Directorios - Recursividad
|
||||
**Pregunta**: Cuando especificamos un directorio de observación como `C:\Projects`, ¿debemos:
|
||||
- Buscar solo en subdirectorios inmediatos
|
||||
- Buscar recursivamente en toda la jerarquía
|
||||
- Permitir configurar la profundidad de búsqueda
|
||||
|
||||
**Decisión necesaria**: ¿Qué comportamiento prefieres por defecto?
|
||||
|
||||
### 8. Backup Incremental vs Completo
|
||||
**Pregunta**: ¿Todos los backups deben ser completos o consideras backup incremental?
|
||||
- Backup completo: Todo el proyecto cada vez
|
||||
- Backup incremental: Solo archivos modificados
|
||||
|
||||
**Decisión necesaria**: ¿Preferencia por simplicidad o eficiencia de espacio?
|
||||
|
||||
### 9. Web Interface - Características Específicas
|
||||
**Pregunta**: ¿Qué funcionalidades son prioritarias en la interfaz web?
|
||||
**Must-have**:
|
||||
- Lista de proyectos con estado
|
||||
- Trigger manual de backup
|
||||
- Ver logs básicos
|
||||
|
||||
**Nice-to-have**:
|
||||
- Configurar schedules por proyecto
|
||||
- Ver progreso de backup en tiempo real
|
||||
- Estadísticas históricas
|
||||
- Configurar nuevos directorios
|
||||
|
||||
**Decisión necesaria**: ¿Cuáles son must-have vs nice-to-have?
|
||||
|
||||
### 10. Error Handling - Estrategias de Recuperación
|
||||
**Pregunta**: ¿Cómo manejar errores específicos?
|
||||
- Espacio insuficiente en destino de backup
|
||||
- Pérdida de conexión con Everything
|
||||
- Corrupción de archivos de configuración
|
||||
- Falla durante compresión
|
||||
|
||||
**Decisión necesaria**: ¿Qué nivel de robustez necesitas?
|
||||
|
||||
### 11. Startup Behavior - Inicialización
|
||||
**Pregunta**: Al iniciar la aplicación:
|
||||
- ¿Debe hacer un escaneo completo inmediatamente?
|
||||
- ¿Debe esperar al primer schedule programado?
|
||||
- ¿Debe permitir configurar el comportamiento de startup?
|
||||
|
||||
**Decisión necesaria**: ¿Qué comportamiento prefieres al iniciar?
|
||||
|
||||
### 12. Multi-threading - Concurrencia
|
||||
**Pregunta**: ¿Los backups deben ejecutarse:
|
||||
- Secuencialmente (uno por vez)
|
||||
- En paralelo (múltiples simultáneos)
|
||||
- Con límite configurable de concurrencia
|
||||
|
||||
**Decisión necesaria**: ¿Qué balance entre velocidad y recursos del sistema?
|
||||
|
||||
## Próximos Pasos
|
||||
Una vez que tengas respuestas a estas preguntas, podremos:
|
||||
1. Finalizar el diseño técnico
|
||||
2. Crear el `requirements.txt` definitivo
|
||||
3. Comenzar con la implementación por fases
|
||||
4. Definir el plan de testing
|
||||
|
||||
¿Podrías revisar estas preguntas y darme tus preferencias para cada una?
|
|
@ -0,0 +1,275 @@
|
|||
"""
|
||||
AutoBackups - Aplicación Principal
|
||||
Sistema automatizado de backup para proyectos Simatic S7
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
# Agregar el directorio src al path para imports
|
||||
current_dir = Path(__file__).parent
|
||||
src_dir = current_dir
|
||||
sys.path.insert(0, str(src_dir))
|
||||
|
||||
# Imports de módulos propios
|
||||
from models.config_model import Config
|
||||
from models.project_model import ProjectManager
|
||||
from utils.file_utils import DiskSpaceChecker
|
||||
from services.basic_backup_service import BasicBackupService
|
||||
|
||||
|
||||
class AutoBackupsApp:
|
||||
"""Aplicación principal de AutoBackups"""
|
||||
|
||||
def __init__(self):
|
||||
self.config = None
|
||||
self.project_manager = None
|
||||
self.discovery_service = None
|
||||
self.disk_checker = None
|
||||
self.logger = None
|
||||
|
||||
# Inicializar aplicación
|
||||
self._setup_logging()
|
||||
self._load_configuration()
|
||||
self._initialize_services()
|
||||
|
||||
def _setup_logging(self):
|
||||
"""Configurar sistema de logging"""
|
||||
try:
|
||||
# Crear directorio de logs si no existe
|
||||
logs_dir = Path(__file__).parent / ".logs"
|
||||
logs_dir.mkdir(exist_ok=True)
|
||||
|
||||
# Nombre del archivo de log con timestamp
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
||||
log_filename = logs_dir / f"autobackups_{timestamp}.log"
|
||||
|
||||
# Configurar logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler(log_filename, encoding='utf-8'),
|
||||
logging.StreamHandler(sys.stdout)
|
||||
]
|
||||
)
|
||||
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.logger.info(f"AutoBackups iniciado - Log: {log_filename}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error configurando logging: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
def _load_configuration(self):
|
||||
"""Cargar configuración del sistema"""
|
||||
try:
|
||||
self.logger.info("Cargando configuración...")
|
||||
self.config = Config()
|
||||
self.logger.info("Configuración cargada exitosamente")
|
||||
|
||||
# Mostrar información básica de configuración
|
||||
self.logger.info(f"Directorios de observación: {len(self.config.observation_directories)}")
|
||||
self.logger.info(f"Destino de backups: {self.config.backup_destination}")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error cargando configuración: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
def _initialize_services(self):
|
||||
"""Inicializar servicios principales"""
|
||||
try:
|
||||
self.logger.info("Inicializando servicios...")
|
||||
|
||||
# Project Manager
|
||||
self.project_manager = ProjectManager()
|
||||
self.logger.info("Project Manager inicializado")
|
||||
|
||||
# Disk Space Checker
|
||||
self.disk_checker = DiskSpaceChecker()
|
||||
self.logger.info("Disk Space Checker inicializado")
|
||||
|
||||
# Basic Backup Service (Fase 1)
|
||||
self.backup_service = BasicBackupService(self.config)
|
||||
self.logger.info("Basic Backup Service inicializado")
|
||||
|
||||
# Project Discovery Service (temporalmente comentado)
|
||||
# self.discovery_service = ProjectDiscoveryService(self.config, self.project_manager)
|
||||
# self.logger.info("Project Discovery Service inicializado")
|
||||
|
||||
self.logger.info("Todos los servicios inicializados correctamente")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error inicializando servicios: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
def check_system_requirements(self) -> bool:
|
||||
"""Verificar requerimientos del sistema"""
|
||||
try:
|
||||
self.logger.info("Verificando requerimientos del sistema...")
|
||||
|
||||
# Verificar espacio en disco
|
||||
backup_destination = self.config.backup_destination
|
||||
min_space_mb = self.config.get_min_free_space_mb()
|
||||
|
||||
free_space_mb = self.disk_checker.get_free_space_mb(backup_destination)
|
||||
|
||||
if free_space_mb < min_space_mb:
|
||||
self.logger.error(
|
||||
f"Espacio insuficiente en destino de backup. "
|
||||
f"Disponible: {free_space_mb:.1f}MB, "
|
||||
f"Requerido: {min_space_mb}MB"
|
||||
)
|
||||
return False
|
||||
|
||||
self.logger.info(f"Espacio en disco OK: {free_space_mb:.1f}MB disponibles")
|
||||
|
||||
# Verificar Everything API (opcional en Fase 1)
|
||||
if hasattr(self, 'backup_service'):
|
||||
if self.backup_service.check_system_requirements():
|
||||
self.logger.info("Requerimientos del sistema verificados")
|
||||
else:
|
||||
self.logger.warning("Algunos requerimientos fallaron")
|
||||
else:
|
||||
self.logger.warning("Backup service no disponible")
|
||||
|
||||
# Verificar directorios de observación
|
||||
missing_dirs = []
|
||||
for obs_dir in self.config.observation_directories:
|
||||
if not Path(obs_dir["path"]).exists():
|
||||
missing_dirs.append(obs_dir["path"])
|
||||
|
||||
if missing_dirs:
|
||||
self.logger.warning(f"Directorios de observación no encontrados: {missing_dirs}")
|
||||
|
||||
self.logger.info("Verificación de requerimientos completada")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error verificando requerimientos del sistema: {e}")
|
||||
return False
|
||||
|
||||
def discover_projects(self) -> int:
|
||||
"""Descubrir proyectos en directorios de observación"""
|
||||
try:
|
||||
self.logger.info("Iniciando descubrimiento de proyectos...")
|
||||
|
||||
# Usar el servicio básico de backup
|
||||
if hasattr(self, 'backup_service'):
|
||||
projects = self.backup_service.discover_projects_basic()
|
||||
else:
|
||||
projects = []
|
||||
|
||||
# Agregar proyectos al manager
|
||||
for project_info in projects:
|
||||
self.project_manager.add_or_update_project(project_info)
|
||||
|
||||
msg = f"Descubrimiento completado: {len(projects)} proyectos"
|
||||
self.logger.info(msg)
|
||||
|
||||
return len(projects)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error en descubrimiento de proyectos: {e}")
|
||||
return 0
|
||||
|
||||
def show_system_status(self):
|
||||
"""Mostrar estado actual del sistema"""
|
||||
try:
|
||||
self.logger.info("=== ESTADO DEL SISTEMA ===")
|
||||
|
||||
# Información de configuración
|
||||
self.logger.info(f"Directorios de observación configurados: {len(self.config.observation_directories)}")
|
||||
self.logger.info(f"Destino de backups: {self.config.backup_destination}")
|
||||
|
||||
# Información de proyectos
|
||||
all_projects = self.project_manager.get_all_projects()
|
||||
enabled_projects = self.project_manager.get_enabled_projects()
|
||||
|
||||
self.logger.info(f"Total de proyectos: {len(all_projects)}")
|
||||
self.logger.info(f"Proyectos habilitados: {len(enabled_projects)}")
|
||||
|
||||
# Información de espacio en disco
|
||||
backup_dest = self.config.backup_destination
|
||||
total_mb, used_mb, free_mb = self.disk_checker.get_disk_usage_info(backup_dest)
|
||||
|
||||
self.logger.info(f"Espacio en disco (destino backup):")
|
||||
self.logger.info(f" Total: {total_mb:.1f}MB")
|
||||
self.logger.info(f" Usado: {used_mb:.1f}MB")
|
||||
self.logger.info(f" Libre: {free_mb:.1f}MB")
|
||||
|
||||
# Información de Everything API
|
||||
if self.discovery_service.everything_searcher:
|
||||
self.logger.info("Everything API: Disponible")
|
||||
else:
|
||||
self.logger.info("Everything API: No disponible")
|
||||
|
||||
self.logger.info("=== FIN ESTADO DEL SISTEMA ===")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error mostrando estado del sistema: {e}")
|
||||
|
||||
def run_initial_setup(self):
|
||||
"""Ejecutar configuración inicial de la aplicación"""
|
||||
try:
|
||||
self.logger.info("=== CONFIGURACIÓN INICIAL DE AUTOBACKUPS ===")
|
||||
|
||||
# Verificar requerimientos del sistema
|
||||
if not self.check_system_requirements():
|
||||
self.logger.error("Los requerimientos del sistema no se cumplen")
|
||||
return False
|
||||
|
||||
# Descubrir proyectos
|
||||
projects_found = self.discover_projects()
|
||||
|
||||
if projects_found == 0:
|
||||
self.logger.warning("No se encontraron proyectos para backup")
|
||||
|
||||
# Mostrar estado del sistema
|
||||
self.show_system_status()
|
||||
|
||||
self.logger.info("=== CONFIGURACIÓN INICIAL COMPLETADA ===")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error en configuración inicial: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
"""Función principal"""
|
||||
try:
|
||||
print("AutoBackups - Sistema de Backup Automatizado")
|
||||
print("=" * 50)
|
||||
|
||||
# Crear y configurar aplicación
|
||||
app = AutoBackupsApp()
|
||||
|
||||
# Ejecutar configuración inicial
|
||||
if app.run_initial_setup():
|
||||
app.logger.info("AutoBackups configurado correctamente")
|
||||
|
||||
# TODO: En las siguientes fases:
|
||||
# - Iniciar scheduler de backups
|
||||
# - Iniciar interfaz web Flask
|
||||
# - Configurar tareas en background
|
||||
|
||||
print("\nConfiguración inicial completada.")
|
||||
print("Revisa el archivo de log para más detalles.")
|
||||
|
||||
else:
|
||||
print("Error en la configuración inicial. Revisa los logs.")
|
||||
sys.exit(1)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\nAplicación interrumpida por el usuario")
|
||||
except Exception as e:
|
||||
print(f"Error inesperado: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -0,0 +1,9 @@
|
|||
"""
|
||||
AutoBackups - Data Models
|
||||
Modelos de datos para configuración y manejo de proyectos
|
||||
"""
|
||||
|
||||
from .config_model import Config
|
||||
from .project_model import Project, ProjectStatus
|
||||
|
||||
__all__ = ['Config', 'Project', 'ProjectStatus']
|
|
@ -0,0 +1,167 @@
|
|||
"""
|
||||
Configuration Model
|
||||
Manejo de la configuración global del sistema
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import Dict, List, Any
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class Config:
|
||||
"""Clase para manejar la configuración global del sistema"""
|
||||
|
||||
def __init__(self, config_path: str = None):
|
||||
if config_path is None:
|
||||
# Buscar config.json en el directorio del proyecto
|
||||
current_dir = Path(__file__).parent.parent.parent
|
||||
config_path = current_dir / "config.json"
|
||||
|
||||
self.config_path = Path(config_path)
|
||||
self._config = {}
|
||||
self.load_config()
|
||||
|
||||
def load_config(self) -> None:
|
||||
"""Cargar configuración desde archivo JSON"""
|
||||
try:
|
||||
if self.config_path.exists():
|
||||
with open(self.config_path, 'r', encoding='utf-8') as f:
|
||||
self._config = json.load(f)
|
||||
else:
|
||||
# Crear configuración por defecto si no existe
|
||||
self._create_default_config()
|
||||
self.save_config()
|
||||
except Exception as e:
|
||||
raise Exception(f"Error cargando configuración: {e}")
|
||||
|
||||
def save_config(self) -> None:
|
||||
"""Guardar configuración actual al archivo JSON"""
|
||||
try:
|
||||
with open(self.config_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(self._config, f, indent=2, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
raise Exception(f"Error guardando configuración: {e}")
|
||||
|
||||
def _create_default_config(self) -> None:
|
||||
"""Crear configuración por defecto"""
|
||||
self._config = {
|
||||
"observation_directories": [],
|
||||
"backup_destination": "D:\\Backups\\AutoBackups",
|
||||
"global_settings": {
|
||||
"default_schedule": "daily",
|
||||
"default_schedule_time": "02:00",
|
||||
"retry_delay_hours": 1,
|
||||
"backup_timeout_minutes": 0,
|
||||
"min_free_space_mb": 100,
|
||||
"scan_interval_minutes": 60,
|
||||
"min_backup_interval_minutes": 10
|
||||
},
|
||||
"everything_api": {
|
||||
"dll_path": "Everything-SDK\\dll\\Everything64.dll",
|
||||
"enabled": True,
|
||||
"search_depth": -1,
|
||||
"skip_last_level_for_s7p": True
|
||||
},
|
||||
"web_interface": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 5000,
|
||||
"debug": False
|
||||
},
|
||||
"logging": {
|
||||
"level": "INFO",
|
||||
"max_log_files": 30,
|
||||
"log_rotation_days": 30
|
||||
},
|
||||
"backup_options": {
|
||||
"compression_level": 6,
|
||||
"include_subdirectories": True,
|
||||
"preserve_directory_structure": True,
|
||||
"hash_algorithm": "md5",
|
||||
"hash_includes": ["timestamp", "size"],
|
||||
"backup_type": "complete",
|
||||
"process_priority": "low",
|
||||
"sequential_execution": True,
|
||||
"filename_format": "HH-MM-SS_projects.zip",
|
||||
"date_format": "YYYY-MM-DD"
|
||||
}
|
||||
}
|
||||
|
||||
# Getters para las secciones principales
|
||||
@property
|
||||
def observation_directories(self) -> List[Dict[str, Any]]:
|
||||
return self._config.get("observation_directories", [])
|
||||
|
||||
@property
|
||||
def backup_destination(self) -> str:
|
||||
return self._config.get("backup_destination", "")
|
||||
|
||||
@property
|
||||
def global_settings(self) -> Dict[str, Any]:
|
||||
return self._config.get("global_settings", {})
|
||||
|
||||
@property
|
||||
def everything_api(self) -> Dict[str, Any]:
|
||||
return self._config.get("everything_api", {})
|
||||
|
||||
@property
|
||||
def web_interface(self) -> Dict[str, Any]:
|
||||
return self._config.get("web_interface", {})
|
||||
|
||||
@property
|
||||
def logging_config(self) -> Dict[str, Any]:
|
||||
return self._config.get("logging", {})
|
||||
|
||||
@property
|
||||
def backup_options(self) -> Dict[str, Any]:
|
||||
return self._config.get("backup_options", {})
|
||||
|
||||
# Métodos de utilidad
|
||||
def get_dll_path(self) -> str:
|
||||
"""Obtener la ruta completa de la DLL de Everything"""
|
||||
dll_relative_path = self.everything_api.get("dll_path", "")
|
||||
if dll_relative_path:
|
||||
# Convertir ruta relativa a absoluta
|
||||
current_dir = Path(__file__).parent.parent.parent.parent
|
||||
return str(current_dir / dll_relative_path)
|
||||
return ""
|
||||
|
||||
def get_min_free_space_mb(self) -> int:
|
||||
"""Obtener el espacio mínimo libre requerido en MB"""
|
||||
return self.global_settings.get("min_free_space_mb", 100)
|
||||
|
||||
def get_scan_interval_minutes(self) -> int:
|
||||
"""Obtener el intervalo de escaneo en minutos"""
|
||||
return self.global_settings.get("scan_interval_minutes", 60)
|
||||
|
||||
def get_min_backup_interval_minutes(self) -> int:
|
||||
"""Obtener el intervalo mínimo de backup en minutos"""
|
||||
return self.global_settings.get("min_backup_interval_minutes", 10)
|
||||
|
||||
def add_observation_directory(self, path: str, dir_type: str, description: str = "") -> None:
|
||||
"""Agregar un nuevo directorio de observación"""
|
||||
new_dir = {
|
||||
"path": path,
|
||||
"type": dir_type,
|
||||
"enabled": True,
|
||||
"description": description
|
||||
}
|
||||
if "observation_directories" not in self._config:
|
||||
self._config["observation_directories"] = []
|
||||
self._config["observation_directories"].append(new_dir)
|
||||
self.save_config()
|
||||
|
||||
def update_config_value(self, key_path: str, value: Any) -> None:
|
||||
"""Actualizar un valor específico en la configuración usando dot notation"""
|
||||
keys = key_path.split('.')
|
||||
current = self._config
|
||||
|
||||
# Navegar hasta el penúltimo nivel
|
||||
for key in keys[:-1]:
|
||||
if key not in current:
|
||||
current[key] = {}
|
||||
current = current[key]
|
||||
|
||||
# Establecer el valor final
|
||||
current[keys[-1]] = value
|
||||
self.save_config()
|
|
@ -0,0 +1,265 @@
|
|||
"""
|
||||
Project Model
|
||||
Modelo de datos para manejo de proyectos y su estado
|
||||
"""
|
||||
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Any, Optional
|
||||
|
||||
|
||||
class ProjectStatus(Enum):
|
||||
"""Estados posibles de un proyecto"""
|
||||
READY = "ready"
|
||||
BACKING_UP = "backing_up"
|
||||
ERROR = "error"
|
||||
FILES_IN_USE = "files_in_use"
|
||||
RETRY_PENDING = "retry_pending"
|
||||
DISABLED = "disabled"
|
||||
|
||||
|
||||
class Project:
|
||||
"""Clase para representar un proyecto individual"""
|
||||
|
||||
def __init__(self, project_data: Dict[str, Any]):
|
||||
self.id = project_data.get("id", "")
|
||||
self.name = project_data.get("name", "")
|
||||
self.path = project_data.get("path", "")
|
||||
self.type = project_data.get("type", "")
|
||||
self.s7p_file = project_data.get("s7p_file", "")
|
||||
self.observation_directory = project_data.get("observation_directory", "")
|
||||
self.relative_path = project_data.get("relative_path", "")
|
||||
self.backup_path = project_data.get("backup_path", "")
|
||||
|
||||
# Configuración de schedule
|
||||
schedule_config = project_data.get("schedule_config", {})
|
||||
self.schedule = schedule_config.get("schedule", "daily")
|
||||
self.schedule_time = schedule_config.get("schedule_time", "02:00")
|
||||
self.enabled = schedule_config.get("enabled", True)
|
||||
self.next_scheduled_backup = schedule_config.get("next_scheduled_backup", "")
|
||||
|
||||
# Historia de backups
|
||||
backup_history = project_data.get("backup_history", {})
|
||||
self.last_backup_date = backup_history.get("last_backup_date", "")
|
||||
self.last_backup_file = backup_history.get("last_backup_file", "")
|
||||
self.backup_count = backup_history.get("backup_count", 0)
|
||||
self.last_successful_backup = backup_history.get("last_successful_backup", "")
|
||||
|
||||
# Información de hash
|
||||
hash_info = project_data.get("hash_info", {})
|
||||
self.last_s7p_hash = hash_info.get("last_s7p_hash", "")
|
||||
self.last_full_hash = hash_info.get("last_full_hash", "")
|
||||
self.last_s7p_timestamp = hash_info.get("last_s7p_timestamp", "")
|
||||
self.last_s7p_size = hash_info.get("last_s7p_size", 0)
|
||||
self.last_scan_timestamp = hash_info.get("last_scan_timestamp", "")
|
||||
self.file_count = hash_info.get("file_count", 0)
|
||||
self.total_size_bytes = hash_info.get("total_size_bytes", 0)
|
||||
|
||||
# Estado actual
|
||||
status = project_data.get("status", {})
|
||||
self.current_status = ProjectStatus(status.get("current_status", "ready"))
|
||||
self.last_error = status.get("last_error", None)
|
||||
self.retry_count = status.get("retry_count", 0)
|
||||
self.next_retry = status.get("next_retry", None)
|
||||
self.files_in_use = status.get("files_in_use", False)
|
||||
self.exclusivity_check_passed = status.get("exclusivity_check_passed", True)
|
||||
self.last_status_update = status.get("last_status_update", "")
|
||||
|
||||
# Información de descubrimiento
|
||||
discovery_info = project_data.get("discovery_info", {})
|
||||
self.discovered_date = discovery_info.get("discovered_date", "")
|
||||
self.discovery_method = discovery_info.get("discovery_method", "")
|
||||
self.auto_discovered = discovery_info.get("auto_discovered", True)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convertir el proyecto a diccionario para serialización JSON"""
|
||||
return {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"path": self.path,
|
||||
"type": self.type,
|
||||
"s7p_file": self.s7p_file,
|
||||
"observation_directory": self.observation_directory,
|
||||
"relative_path": self.relative_path,
|
||||
"backup_path": self.backup_path,
|
||||
"schedule_config": {
|
||||
"schedule": self.schedule,
|
||||
"schedule_time": self.schedule_time,
|
||||
"enabled": self.enabled,
|
||||
"next_scheduled_backup": self.next_scheduled_backup
|
||||
},
|
||||
"backup_history": {
|
||||
"last_backup_date": self.last_backup_date,
|
||||
"last_backup_file": self.last_backup_file,
|
||||
"backup_count": self.backup_count,
|
||||
"last_successful_backup": self.last_successful_backup
|
||||
},
|
||||
"hash_info": {
|
||||
"last_s7p_hash": self.last_s7p_hash,
|
||||
"last_full_hash": self.last_full_hash,
|
||||
"last_s7p_timestamp": self.last_s7p_timestamp,
|
||||
"last_s7p_size": self.last_s7p_size,
|
||||
"last_scan_timestamp": self.last_scan_timestamp,
|
||||
"file_count": self.file_count,
|
||||
"total_size_bytes": self.total_size_bytes
|
||||
},
|
||||
"status": {
|
||||
"current_status": self.current_status.value,
|
||||
"last_error": self.last_error,
|
||||
"retry_count": self.retry_count,
|
||||
"next_retry": self.next_retry,
|
||||
"files_in_use": self.files_in_use,
|
||||
"exclusivity_check_passed": self.exclusivity_check_passed,
|
||||
"last_status_update": self.last_status_update
|
||||
},
|
||||
"discovery_info": {
|
||||
"discovered_date": self.discovered_date,
|
||||
"discovery_method": self.discovery_method,
|
||||
"auto_discovered": self.auto_discovered
|
||||
}
|
||||
}
|
||||
|
||||
def update_status(self, status: ProjectStatus, error_message: str = None) -> None:
|
||||
"""Actualizar el estado del proyecto"""
|
||||
self.current_status = status
|
||||
self.last_error = error_message
|
||||
self.last_status_update = datetime.now(timezone.utc).isoformat()
|
||||
|
||||
if status == ProjectStatus.ERROR:
|
||||
self.retry_count += 1
|
||||
elif status == ProjectStatus.READY:
|
||||
self.retry_count = 0
|
||||
self.last_error = None
|
||||
|
||||
def update_hash_info(self, s7p_hash: str = None, full_hash: str = None,
|
||||
s7p_timestamp: str = None, s7p_size: int = None,
|
||||
file_count: int = None, total_size: int = None) -> None:
|
||||
"""Actualizar información de hash"""
|
||||
if s7p_hash is not None:
|
||||
self.last_s7p_hash = s7p_hash
|
||||
if full_hash is not None:
|
||||
self.last_full_hash = full_hash
|
||||
if s7p_timestamp is not None:
|
||||
self.last_s7p_timestamp = s7p_timestamp
|
||||
if s7p_size is not None:
|
||||
self.last_s7p_size = s7p_size
|
||||
if file_count is not None:
|
||||
self.file_count = file_count
|
||||
if total_size is not None:
|
||||
self.total_size_bytes = total_size
|
||||
|
||||
self.last_scan_timestamp = datetime.now(timezone.utc).isoformat()
|
||||
|
||||
def update_backup_info(self, backup_file_path: str) -> None:
|
||||
"""Actualizar información después de un backup exitoso"""
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
self.last_backup_date = now
|
||||
self.last_successful_backup = now
|
||||
self.last_backup_file = backup_file_path
|
||||
self.backup_count += 1
|
||||
self.update_status(ProjectStatus.READY)
|
||||
|
||||
|
||||
class ProjectManager:
|
||||
"""Clase para manejar la colección de proyectos"""
|
||||
|
||||
def __init__(self, projects_file_path: str = None):
|
||||
if projects_file_path is None:
|
||||
# Buscar projects.json en el directorio del proyecto
|
||||
current_dir = Path(__file__).parent.parent.parent
|
||||
projects_file_path = current_dir / "projects.json"
|
||||
|
||||
self.projects_file_path = Path(projects_file_path)
|
||||
self.projects: Dict[str, Project] = {}
|
||||
self.metadata = {}
|
||||
self.statistics = {}
|
||||
self.load_projects()
|
||||
|
||||
def load_projects(self) -> None:
|
||||
"""Cargar proyectos desde archivo JSON"""
|
||||
try:
|
||||
if self.projects_file_path.exists():
|
||||
with open(self.projects_file_path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
self.metadata = data.get("metadata", {})
|
||||
self.statistics = data.get("statistics", {})
|
||||
|
||||
# Cargar proyectos
|
||||
for project_data in data.get("projects", []):
|
||||
project = Project(project_data)
|
||||
self.projects[project.id] = project
|
||||
else:
|
||||
# Crear archivo por defecto si no existe
|
||||
self._create_default_projects_file()
|
||||
self.save_projects()
|
||||
except Exception as e:
|
||||
raise Exception(f"Error cargando proyectos: {e}")
|
||||
|
||||
def save_projects(self) -> None:
|
||||
"""Guardar proyectos al archivo JSON"""
|
||||
try:
|
||||
data = {
|
||||
"metadata": self.metadata,
|
||||
"projects": [project.to_dict() for project in self.projects.values()],
|
||||
"statistics": self.statistics
|
||||
}
|
||||
|
||||
with open(self.projects_file_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
raise Exception(f"Error guardando proyectos: {e}")
|
||||
|
||||
def _create_default_projects_file(self) -> None:
|
||||
"""Crear archivo de proyectos por defecto"""
|
||||
self.metadata = {
|
||||
"version": "1.0",
|
||||
"last_updated": datetime.now(timezone.utc).isoformat(),
|
||||
"total_projects": 0
|
||||
}
|
||||
|
||||
self.statistics = {
|
||||
"total_backups_created": 0,
|
||||
"total_backup_size_mb": 0.0,
|
||||
"average_backup_time_seconds": 0.0,
|
||||
"last_global_scan": "",
|
||||
"projects_with_errors": 0,
|
||||
"projects_pending_retry": 0
|
||||
}
|
||||
|
||||
def add_project(self, project_data: Dict[str, Any]) -> Project:
|
||||
"""Agregar un nuevo proyecto"""
|
||||
project = Project(project_data)
|
||||
self.projects[project.id] = project
|
||||
self.metadata["total_projects"] = len(self.projects)
|
||||
self.metadata["last_updated"] = datetime.now(timezone.utc).isoformat()
|
||||
self.save_projects()
|
||||
return project
|
||||
|
||||
def get_project(self, project_id: str) -> Optional[Project]:
|
||||
"""Obtener un proyecto por ID"""
|
||||
return self.projects.get(project_id)
|
||||
|
||||
def get_all_projects(self) -> List[Project]:
|
||||
"""Obtener todos los proyectos"""
|
||||
return list(self.projects.values())
|
||||
|
||||
def get_projects_by_status(self, status: ProjectStatus) -> List[Project]:
|
||||
"""Obtener proyectos por estado"""
|
||||
return [p for p in self.projects.values() if p.current_status == status]
|
||||
|
||||
def get_enabled_projects(self) -> List[Project]:
|
||||
"""Obtener proyectos habilitados"""
|
||||
return [p for p in self.projects.values() if p.enabled]
|
||||
|
||||
def update_statistics(self) -> None:
|
||||
"""Actualizar estadísticas generales"""
|
||||
self.statistics["projects_with_errors"] = len(
|
||||
self.get_projects_by_status(ProjectStatus.ERROR)
|
||||
)
|
||||
self.statistics["projects_pending_retry"] = len(
|
||||
self.get_projects_by_status(ProjectStatus.RETRY_PENDING)
|
||||
)
|
||||
self.save_projects()
|
|
@ -0,0 +1,12 @@
|
|||
"""
|
||||
AutoBackups - Services
|
||||
Servicios principales para backup y descubrimiento de proyectos
|
||||
"""
|
||||
|
||||
# Imports comentados temporalmente para evitar problemas de dependencias circulares
|
||||
# from .project_discovery_service import ProjectDiscoveryService
|
||||
# from .backup_service import BackupService
|
||||
# from .scheduler_service import SchedulerService
|
||||
from .basic_backup_service import BasicBackupService
|
||||
|
||||
__all__ = ['BasicBackupService']
|
|
@ -0,0 +1,94 @@
|
|||
"""
|
||||
Servicio de Backup Básico
|
||||
Versión simplificada para Fase 1 sin Everything API
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Any
|
||||
|
||||
|
||||
class BasicBackupService:
|
||||
"""Servicio básico de backup sin Everything API"""
|
||||
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
def check_system_requirements(self) -> bool:
|
||||
"""Verificar requerimientos básicos del sistema"""
|
||||
try:
|
||||
# Verificar directorio de backup
|
||||
backup_dest = self.config.backup_destination
|
||||
if not os.path.exists(backup_dest):
|
||||
self.logger.error(f"Directorio de backup no existe: {backup_dest}")
|
||||
return False
|
||||
|
||||
# Verificar directorios de observación
|
||||
obs_dirs = self.config.observation_directories
|
||||
for obs_dir in obs_dirs:
|
||||
if obs_dir.get("enabled", True):
|
||||
path = obs_dir["path"]
|
||||
if not os.path.exists(path):
|
||||
self.logger.warning(f"Directorio de observación no existe: {path}")
|
||||
|
||||
self.logger.info("Requerimientos básicos del sistema verificados")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error verificando requerimientos: {e}")
|
||||
return False
|
||||
|
||||
def discover_projects_basic(self) -> List[Dict[str, Any]]:
|
||||
"""Descubrimiento básico de proyectos usando búsqueda de archivos"""
|
||||
projects = []
|
||||
|
||||
try:
|
||||
obs_dirs = [
|
||||
obs_dir for obs_dir in self.config.observation_directories
|
||||
if obs_dir.get("enabled", True) and obs_dir.get("type") == "siemens_s7"
|
||||
]
|
||||
|
||||
for obs_dir in obs_dirs:
|
||||
self.logger.info(f"Buscando proyectos S7 en: {obs_dir['path']}")
|
||||
|
||||
# Buscar archivos .s7p recursivamente
|
||||
base_path = Path(obs_dir["path"])
|
||||
if base_path.exists():
|
||||
s7p_files = list(base_path.rglob("*.s7p"))
|
||||
|
||||
for s7p_file in s7p_files:
|
||||
project_info = self._create_project_info(s7p_file, obs_dir)
|
||||
projects.append(project_info)
|
||||
self.logger.info(f"Proyecto encontrado: {project_info['name']}")
|
||||
|
||||
self.logger.info(f"Descubrimiento completado. {len(projects)} proyectos encontrados")
|
||||
return projects
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error en descubrimiento de proyectos: {e}")
|
||||
return []
|
||||
|
||||
def _create_project_info(self, s7p_file: Path, obs_dir: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Crear información de proyecto desde archivo .s7p"""
|
||||
project_path = s7p_file.parent
|
||||
obs_path = Path(obs_dir["path"])
|
||||
|
||||
# Calcular ruta relativa
|
||||
try:
|
||||
relative_path = project_path.relative_to(obs_path)
|
||||
except ValueError:
|
||||
relative_path = project_path.name
|
||||
|
||||
return {
|
||||
"name": project_path.name,
|
||||
"path": str(project_path),
|
||||
"s7p_file": str(s7p_file),
|
||||
"observation_directory": obs_dir["path"],
|
||||
"relative_path": str(relative_path),
|
||||
"backup_path": str(relative_path),
|
||||
"type": "siemens_s7",
|
||||
"auto_discovered": True,
|
||||
"discovery_method": "filesystem_search"
|
||||
}
|
|
@ -0,0 +1,325 @@
|
|||
"""
|
||||
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
|
|
@ -0,0 +1,10 @@
|
|||
"""
|
||||
AutoBackups - Utilities
|
||||
Funciones de utilidad para hashing, manejo de archivos y otras operaciones
|
||||
"""
|
||||
|
||||
from .hash_utils import HashCalculator
|
||||
from .file_utils import FileChecker, DiskSpaceChecker
|
||||
from .everything_wrapper import EverythingSearcher
|
||||
|
||||
__all__ = ['HashCalculator', 'FileChecker', 'DiskSpaceChecker', 'EverythingSearcher']
|
|
@ -0,0 +1,272 @@
|
|||
"""
|
||||
Everything API Wrapper
|
||||
Integración con Everything para búsqueda rápida de archivos .s7p
|
||||
"""
|
||||
|
||||
import os
|
||||
import ctypes
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Tuple
|
||||
import logging
|
||||
|
||||
|
||||
class EverythingSearcher:
|
||||
"""Wrapper para la API de Everything"""
|
||||
|
||||
def __init__(self, dll_path: str = None):
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.dll = None
|
||||
self.is_initialized = False
|
||||
|
||||
if dll_path:
|
||||
self.load_dll(dll_path)
|
||||
|
||||
def load_dll(self, dll_path: str) -> bool:
|
||||
"""Cargar la DLL de Everything"""
|
||||
try:
|
||||
if not os.path.exists(dll_path):
|
||||
self.logger.error(f"DLL de Everything no encontrada: {dll_path}")
|
||||
return False
|
||||
|
||||
self.dll = ctypes.WinDLL(dll_path)
|
||||
|
||||
# Definir funciones de la API
|
||||
self._setup_api_functions()
|
||||
|
||||
# Inicializar Everything
|
||||
self.is_initialized = self._initialize_everything()
|
||||
|
||||
if self.is_initialized:
|
||||
self.logger.info("Everything API inicializada correctamente")
|
||||
else:
|
||||
self.logger.error("Error inicializando Everything API")
|
||||
|
||||
return self.is_initialized
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error cargando DLL de Everything: {e}")
|
||||
return False
|
||||
|
||||
def _setup_api_functions(self):
|
||||
"""Configurar las funciones de la API"""
|
||||
try:
|
||||
# Everything_SetSearchW - Establecer consulta de búsqueda
|
||||
self.dll.Everything_SetSearchW.argtypes = [ctypes.c_wchar_p]
|
||||
self.dll.Everything_SetSearchW.restype = None
|
||||
|
||||
# Everything_SetMatchPath - Habilitar coincidencia de ruta completa
|
||||
self.dll.Everything_SetMatchPath.argtypes = [ctypes.c_bool]
|
||||
self.dll.Everything_SetMatchPath.restype = None
|
||||
|
||||
# Everything_SetMatchCase - Habilitar coincidencia de mayúsculas/minúsculas
|
||||
self.dll.Everything_SetMatchCase.argtypes = [ctypes.c_bool]
|
||||
self.dll.Everything_SetMatchCase.restype = None
|
||||
|
||||
# Everything_QueryW - Ejecutar consulta
|
||||
self.dll.Everything_QueryW.argtypes = [ctypes.c_bool]
|
||||
self.dll.Everything_QueryW.restype = ctypes.c_bool
|
||||
|
||||
# Everything_GetNumResults - Obtener número de resultados
|
||||
self.dll.Everything_GetNumResults.restype = ctypes.c_uint
|
||||
|
||||
# Everything_GetResultFullPathNameW - Obtener ruta completa del resultado
|
||||
self.dll.Everything_GetResultFullPathNameW.argtypes = [
|
||||
ctypes.c_uint, ctypes.c_wchar_p, ctypes.c_uint
|
||||
]
|
||||
self.dll.Everything_GetResultFullPathNameW.restype = ctypes.c_uint
|
||||
|
||||
# Everything_GetLastError - Obtener último error
|
||||
self.dll.Everything_GetLastError.restype = ctypes.c_uint
|
||||
|
||||
self.logger.debug("Funciones API configuradas correctamente")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error configurando funciones API: {e}")
|
||||
raise
|
||||
|
||||
def _initialize_everything(self) -> bool:
|
||||
"""Inicializar Everything con configuraciones por defecto"""
|
||||
try:
|
||||
# Configurar opciones de búsqueda
|
||||
self.dll.Everything_SetMatchPath(True)
|
||||
self.dll.Everything_SetMatchCase(False)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error inicializando Everything: {e}")
|
||||
return False
|
||||
|
||||
def search_s7p_files(self, search_directories: List[str]) -> List[str]:
|
||||
"""
|
||||
Buscar archivos .s7p en los directorios especificados
|
||||
Retorna lista de rutas completas a archivos .s7p encontrados
|
||||
"""
|
||||
if not self.is_initialized:
|
||||
self.logger.error("Everything no está inicializado")
|
||||
return []
|
||||
|
||||
try:
|
||||
all_s7p_files = []
|
||||
|
||||
for directory in search_directories:
|
||||
s7p_files = self._search_s7p_in_directory(directory)
|
||||
all_s7p_files.extend(s7p_files)
|
||||
|
||||
# Eliminar duplicados manteniendo el orden
|
||||
unique_files = []
|
||||
seen = set()
|
||||
for file_path in all_s7p_files:
|
||||
if file_path not in seen:
|
||||
seen.add(file_path)
|
||||
unique_files.append(file_path)
|
||||
|
||||
self.logger.info(f"Encontrados {len(unique_files)} archivos .s7p únicos")
|
||||
return unique_files
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error buscando archivos S7P: {e}")
|
||||
return []
|
||||
|
||||
def _search_s7p_in_directory(self, directory: str) -> List[str]:
|
||||
"""Buscar archivos .s7p en un directorio específico"""
|
||||
try:
|
||||
# Normalizar la ruta del directorio
|
||||
directory = os.path.normpath(directory)
|
||||
|
||||
# Construir consulta de búsqueda
|
||||
# Buscar archivos .s7p en el directorio y subdirectorios
|
||||
search_query = f'"{directory}" ext:s7p'
|
||||
|
||||
self.logger.debug(f"Búsqueda en Everything: {search_query}")
|
||||
|
||||
# Establecer consulta
|
||||
self.dll.Everything_SetSearchW(search_query)
|
||||
|
||||
# Ejecutar búsqueda
|
||||
if not self.dll.Everything_QueryW(True):
|
||||
error_code = self.dll.Everything_GetLastError()
|
||||
self.logger.error(f"Error en consulta Everything: código {error_code}")
|
||||
return []
|
||||
|
||||
# Obtener número de resultados
|
||||
num_results = self.dll.Everything_GetNumResults()
|
||||
self.logger.debug(f"Everything encontró {num_results} resultados para {directory}")
|
||||
|
||||
if num_results == 0:
|
||||
return []
|
||||
|
||||
# Extraer rutas de los resultados
|
||||
results = []
|
||||
buffer_size = 4096 # Tamaño del buffer para rutas
|
||||
|
||||
for i in range(num_results):
|
||||
# Buffer para almacenar la ruta
|
||||
path_buffer = ctypes.create_unicode_buffer(buffer_size)
|
||||
|
||||
# Obtener ruta completa
|
||||
chars_copied = self.dll.Everything_GetResultFullPathNameW(
|
||||
i, path_buffer, buffer_size
|
||||
)
|
||||
|
||||
if chars_copied > 0:
|
||||
file_path = path_buffer.value
|
||||
if file_path and os.path.exists(file_path):
|
||||
results.append(file_path)
|
||||
self.logger.debug(f"Archivo S7P encontrado: {file_path}")
|
||||
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error buscando en directorio {directory}: {e}")
|
||||
return []
|
||||
|
||||
def search_files_by_pattern(self, pattern: str, max_results: int = 1000) -> List[str]:
|
||||
"""
|
||||
Buscar archivos usando un patrón específico
|
||||
"""
|
||||
if not self.is_initialized:
|
||||
self.logger.error("Everything no está inicializado")
|
||||
return []
|
||||
|
||||
try:
|
||||
self.logger.debug(f"Búsqueda por patrón: {pattern}")
|
||||
|
||||
# Establecer consulta
|
||||
self.dll.Everything_SetSearchW(pattern)
|
||||
|
||||
# Ejecutar búsqueda
|
||||
if not self.dll.Everything_QueryW(True):
|
||||
error_code = self.dll.Everything_GetLastError()
|
||||
self.logger.error(f"Error en consulta Everything: código {error_code}")
|
||||
return []
|
||||
|
||||
# Obtener número de resultados
|
||||
num_results = self.dll.Everything_GetNumResults()
|
||||
actual_results = min(num_results, max_results)
|
||||
|
||||
self.logger.debug(f"Procesando {actual_results} de {num_results} resultados")
|
||||
|
||||
results = []
|
||||
buffer_size = 4096
|
||||
|
||||
for i in range(actual_results):
|
||||
path_buffer = ctypes.create_unicode_buffer(buffer_size)
|
||||
chars_copied = self.dll.Everything_GetResultFullPathNameW(
|
||||
i, path_buffer, buffer_size
|
||||
)
|
||||
|
||||
if chars_copied > 0:
|
||||
file_path = path_buffer.value
|
||||
if file_path and os.path.exists(file_path):
|
||||
results.append(file_path)
|
||||
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error buscando patrón {pattern}: {e}")
|
||||
return []
|
||||
|
||||
def is_everything_available(self) -> bool:
|
||||
"""Verificar si Everything está disponible y funcionando"""
|
||||
return self.is_initialized
|
||||
|
||||
def get_project_directory_from_s7p(self, s7p_file_path: str,
|
||||
observation_directory: str) -> Tuple[str, str]:
|
||||
"""
|
||||
Determinar el directorio del proyecto y la ruta relativa desde un archivo .s7p
|
||||
Retorna: (directorio_proyecto, ruta_relativa)
|
||||
"""
|
||||
try:
|
||||
s7p_path = Path(s7p_file_path)
|
||||
obs_path = Path(observation_directory)
|
||||
|
||||
# El directorio del proyecto es el padre del archivo .s7p
|
||||
project_dir = s7p_path.parent
|
||||
|
||||
# Calcular ruta relativa desde el directorio de observación
|
||||
try:
|
||||
relative_path = project_dir.relative_to(obs_path)
|
||||
return str(project_dir), str(relative_path)
|
||||
except ValueError:
|
||||
# Si no se puede calcular la ruta relativa, usar ruta absoluta
|
||||
self.logger.warning(f"No se puede calcular ruta relativa para {s7p_file_path}")
|
||||
return str(project_dir), str(project_dir)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error determinando directorio de proyecto para {s7p_file_path}: {e}")
|
||||
return "", ""
|
||||
|
||||
|
||||
# Funciones de utilidad para crear instancias
|
||||
def create_everything_searcher(dll_path: str) -> Optional[EverythingSearcher]:
|
||||
"""Crear una instancia de EverythingSearcher con manejo de errores"""
|
||||
try:
|
||||
searcher = EverythingSearcher(dll_path)
|
||||
if searcher.is_everything_available():
|
||||
return searcher
|
||||
else:
|
||||
return None
|
||||
except Exception as e:
|
||||
logging.getLogger(__name__).error(f"Error creando EverythingSearcher: {e}")
|
||||
return None
|
|
@ -0,0 +1,247 @@
|
|||
"""
|
||||
File Utilities
|
||||
Utilidades para verificación de archivos y espacio en disco
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import psutil
|
||||
from pathlib import Path
|
||||
from typing import Tuple, List, Optional, Dict
|
||||
import logging
|
||||
|
||||
|
||||
class FileChecker:
|
||||
"""Verificador de acceso a archivos y detección de archivos en uso"""
|
||||
|
||||
def __init__(self):
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
def check_file_accessibility(self, file_path: str) -> Tuple[bool, str]:
|
||||
"""
|
||||
Verificar si un archivo es accesible para lectura
|
||||
Retorna: (es_accesible, mensaje_error)
|
||||
"""
|
||||
try:
|
||||
path = Path(file_path)
|
||||
if not path.exists():
|
||||
return False, f"Archivo no existe: {file_path}"
|
||||
|
||||
# Intentar abrir el archivo en modo lectura
|
||||
with open(file_path, 'rb') as f:
|
||||
# Leer un byte para verificar acceso real
|
||||
f.read(1)
|
||||
|
||||
return True, ""
|
||||
|
||||
except PermissionError:
|
||||
return False, f"Sin permisos para acceder: {file_path}"
|
||||
except OSError as e:
|
||||
if "being used by another process" in str(e).lower():
|
||||
return False, f"Archivo en uso: {file_path}"
|
||||
return False, f"Error de sistema: {e}"
|
||||
except Exception as e:
|
||||
return False, f"Error inesperado: {e}"
|
||||
|
||||
def check_directory_accessibility(self, directory_path: str,
|
||||
check_all_files: bool = False) -> Tuple[bool, List[str]]:
|
||||
"""
|
||||
Verificar accesibilidad de un directorio
|
||||
Si check_all_files es True, verifica todos los archivos
|
||||
Retorna: (es_accesible, lista_errores)
|
||||
"""
|
||||
try:
|
||||
path = Path(directory_path)
|
||||
if not path.exists():
|
||||
return False, [f"Directorio no existe: {directory_path}"]
|
||||
|
||||
if not path.is_dir():
|
||||
return False, [f"La ruta no es un directorio: {directory_path}"]
|
||||
|
||||
errors = []
|
||||
|
||||
if check_all_files:
|
||||
# Verificar todos los archivos en el directorio
|
||||
for file_path in path.rglob("*"):
|
||||
if file_path.is_file():
|
||||
accessible, error_msg = self.check_file_accessibility(str(file_path))
|
||||
if not accessible:
|
||||
errors.append(error_msg)
|
||||
else:
|
||||
# Solo verificar acceso básico al directorio
|
||||
try:
|
||||
list(path.iterdir())
|
||||
except PermissionError:
|
||||
errors.append(f"Sin permisos para acceder al directorio: {directory_path}")
|
||||
except Exception as e:
|
||||
errors.append(f"Error accediendo al directorio: {e}")
|
||||
|
||||
return len(errors) == 0, errors
|
||||
|
||||
except Exception as e:
|
||||
return False, [f"Error verificando directorio {directory_path}: {e}"]
|
||||
|
||||
def check_s7p_file_exclusively(self, s7p_file_path: str) -> Tuple[bool, str]:
|
||||
"""
|
||||
Verificar específicamente el archivo .s7p para exclusividad
|
||||
Método rápido antes de procesar todo el proyecto
|
||||
"""
|
||||
try:
|
||||
# Primero verificar acceso básico
|
||||
accessible, error_msg = self.check_file_accessibility(s7p_file_path)
|
||||
if not accessible:
|
||||
return False, error_msg
|
||||
|
||||
# Intentar crear un backup temporal para verificar exclusividad
|
||||
import tempfile
|
||||
temp_dir = tempfile.gettempdir()
|
||||
temp_file = Path(temp_dir) / f"autobackup_test_{Path(s7p_file_path).name}"
|
||||
|
||||
try:
|
||||
shutil.copy2(s7p_file_path, temp_file)
|
||||
# Si la copia fue exitosa, el archivo no está en uso exclusivo
|
||||
temp_file.unlink() # Eliminar archivo temporal
|
||||
return True, ""
|
||||
except Exception as e:
|
||||
if "being used by another process" in str(e).lower():
|
||||
return False, f"Archivo S7P en uso exclusivo: {s7p_file_path}"
|
||||
return False, f"Error verificando exclusividad S7P: {e}"
|
||||
|
||||
except Exception as e:
|
||||
return False, f"Error verificando archivo S7P {s7p_file_path}: {e}"
|
||||
|
||||
def is_file_locked(self, file_path: str) -> bool:
|
||||
"""
|
||||
Verificar si un archivo específico está bloqueado
|
||||
"""
|
||||
accessible, _ = self.check_file_accessibility(file_path)
|
||||
return not accessible
|
||||
|
||||
def get_locked_files_in_directory(self, directory_path: str) -> List[str]:
|
||||
"""
|
||||
Obtener lista de archivos bloqueados en un directorio
|
||||
"""
|
||||
locked_files = []
|
||||
try:
|
||||
path = Path(directory_path)
|
||||
for file_path in path.rglob("*"):
|
||||
if file_path.is_file():
|
||||
if self.is_file_locked(str(file_path)):
|
||||
locked_files.append(str(file_path))
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error buscando archivos bloqueados en {directory_path}: {e}")
|
||||
|
||||
return locked_files
|
||||
|
||||
|
||||
class DiskSpaceChecker:
|
||||
"""Verificador de espacio en disco"""
|
||||
|
||||
def __init__(self):
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
def get_free_space_mb(self, path: str) -> float:
|
||||
"""
|
||||
Obtener espacio libre en MB para una ruta específica
|
||||
"""
|
||||
try:
|
||||
if os.name == 'nt': # Windows
|
||||
free_bytes = shutil.disk_usage(path).free
|
||||
else: # Linux/Mac
|
||||
statvfs = os.statvfs(path)
|
||||
free_bytes = statvfs.f_frsize * statvfs.f_bavail
|
||||
|
||||
return free_bytes / (1024 * 1024) # Convertir a MB
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error obteniendo espacio libre para {path}: {e}")
|
||||
return 0.0
|
||||
|
||||
def get_disk_usage_info(self, path: str) -> Tuple[float, float, float]:
|
||||
"""
|
||||
Obtener información completa de uso de disco
|
||||
Retorna: (total_mb, usado_mb, libre_mb)
|
||||
"""
|
||||
try:
|
||||
if os.name == 'nt': # Windows
|
||||
usage = shutil.disk_usage(path)
|
||||
total_mb = usage.total / (1024 * 1024)
|
||||
free_mb = usage.free / (1024 * 1024)
|
||||
used_mb = total_mb - free_mb
|
||||
else: # Linux/Mac
|
||||
statvfs = os.statvfs(path)
|
||||
total_mb = (statvfs.f_frsize * statvfs.f_blocks) / (1024 * 1024)
|
||||
free_mb = (statvfs.f_frsize * statvfs.f_bavail) / (1024 * 1024)
|
||||
used_mb = total_mb - free_mb
|
||||
|
||||
return total_mb, used_mb, free_mb
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error obteniendo información de disco para {path}: {e}")
|
||||
return 0.0, 0.0, 0.0
|
||||
|
||||
def has_sufficient_space(self, path: str, required_mb: float,
|
||||
min_free_mb: float = 100.0) -> Tuple[bool, float]:
|
||||
"""
|
||||
Verificar si hay suficiente espacio para una operación
|
||||
Retorna: (tiene_espacio_suficiente, espacio_libre_actual_mb)
|
||||
"""
|
||||
try:
|
||||
free_mb = self.get_free_space_mb(path)
|
||||
total_required = required_mb + min_free_mb
|
||||
|
||||
return free_mb >= total_required, free_mb
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error verificando espacio suficiente para {path}: {e}")
|
||||
return False, 0.0
|
||||
|
||||
def estimate_directory_size_mb(self, directory_path: str) -> float:
|
||||
"""
|
||||
Estimar el tamaño de un directorio en MB
|
||||
"""
|
||||
try:
|
||||
total_size = 0
|
||||
path = Path(directory_path)
|
||||
|
||||
for file_path in path.rglob("*"):
|
||||
if file_path.is_file():
|
||||
try:
|
||||
total_size += file_path.stat().st_size
|
||||
except (OSError, PermissionError):
|
||||
# Saltar archivos inaccesibles
|
||||
continue
|
||||
|
||||
return total_size / (1024 * 1024) # Convertir a MB
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error estimando tamaño de {directory_path}: {e}")
|
||||
return 0.0
|
||||
|
||||
def get_system_disk_info(self) -> Dict[str, Tuple[float, float, float]]:
|
||||
"""
|
||||
Obtener información de todos los discos del sistema
|
||||
Retorna: {drive: (total_mb, usado_mb, libre_mb)}
|
||||
"""
|
||||
disk_info = {}
|
||||
try:
|
||||
# Obtener todas las particiones
|
||||
partitions = psutil.disk_partitions()
|
||||
|
||||
for partition in partitions:
|
||||
try:
|
||||
usage = psutil.disk_usage(partition.mountpoint)
|
||||
total_mb = usage.total / (1024 * 1024)
|
||||
used_mb = usage.used / (1024 * 1024)
|
||||
free_mb = usage.free / (1024 * 1024)
|
||||
|
||||
disk_info[partition.device] = (total_mb, used_mb, free_mb)
|
||||
|
||||
except PermissionError:
|
||||
# Saltar unidades sin acceso
|
||||
continue
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error obteniendo información del sistema: {e}")
|
||||
|
||||
return disk_info
|
|
@ -0,0 +1,197 @@
|
|||
"""
|
||||
Hash Utilities
|
||||
Sistema de hash en dos etapas para optimizar detección de cambios
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Tuple, Optional
|
||||
import logging
|
||||
|
||||
|
||||
class HashCalculator:
|
||||
"""Calculadora de hash optimizada para el sistema de dos etapas"""
|
||||
|
||||
def __init__(self, algorithm: str = "md5"):
|
||||
self.algorithm = algorithm.lower()
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
def calculate_file_hash(self, file_path: str) -> str:
|
||||
"""
|
||||
Calcular hash de un archivo basado en timestamp + tamaño
|
||||
No lee el contenido del archivo para mayor velocidad
|
||||
"""
|
||||
try:
|
||||
path = Path(file_path)
|
||||
if not path.exists():
|
||||
return ""
|
||||
|
||||
stat = path.stat()
|
||||
timestamp = str(stat.st_mtime)
|
||||
size = str(stat.st_size)
|
||||
|
||||
# Crear hash combinando timestamp y tamaño
|
||||
hash_input = f"{timestamp}:{size}".encode('utf-8')
|
||||
|
||||
if self.algorithm == "md5":
|
||||
return hashlib.md5(hash_input).hexdigest()
|
||||
elif self.algorithm == "sha256":
|
||||
return hashlib.sha256(hash_input).hexdigest()
|
||||
else:
|
||||
raise ValueError(f"Algoritmo de hash no soportado: {self.algorithm}")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error calculando hash para {file_path}: {e}")
|
||||
return ""
|
||||
|
||||
def calculate_s7p_hash(self, s7p_file_path: str) -> Tuple[str, str, int]:
|
||||
"""
|
||||
Calcular hash específico para archivo .s7p
|
||||
Retorna: (hash, timestamp_iso, size_bytes)
|
||||
"""
|
||||
try:
|
||||
path = Path(s7p_file_path)
|
||||
if not path.exists():
|
||||
return "", "", 0
|
||||
|
||||
stat = path.stat()
|
||||
timestamp = str(stat.st_mtime)
|
||||
size = stat.st_size
|
||||
|
||||
# Crear hash
|
||||
hash_input = f"{timestamp}:{size}".encode('utf-8')
|
||||
if self.algorithm == "md5":
|
||||
file_hash = hashlib.md5(hash_input).hexdigest()
|
||||
else:
|
||||
file_hash = hashlib.sha256(hash_input).hexdigest()
|
||||
|
||||
# Convertir timestamp a ISO format
|
||||
from datetime import datetime
|
||||
timestamp_iso = datetime.fromtimestamp(stat.st_mtime).isoformat()
|
||||
|
||||
return file_hash, timestamp_iso, size
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error calculando hash S7P para {s7p_file_path}: {e}")
|
||||
return "", "", 0
|
||||
|
||||
def calculate_directory_hash(self, directory_path: str, exclude_patterns: List[str] = None) -> Tuple[str, int, int]:
|
||||
"""
|
||||
Calcular hash de todos los archivos en un directorio
|
||||
Retorna: (hash_completo, cantidad_archivos, tamaño_total)
|
||||
"""
|
||||
try:
|
||||
path = Path(directory_path)
|
||||
if not path.exists():
|
||||
return "", 0, 0
|
||||
|
||||
if exclude_patterns is None:
|
||||
exclude_patterns = []
|
||||
|
||||
file_hashes = []
|
||||
file_count = 0
|
||||
total_size = 0
|
||||
|
||||
# Recorrer todos los archivos recursivamente
|
||||
for file_path in path.rglob("*"):
|
||||
if file_path.is_file():
|
||||
# Verificar si el archivo debe ser excluido
|
||||
should_exclude = False
|
||||
for pattern in exclude_patterns:
|
||||
if pattern in str(file_path):
|
||||
should_exclude = True
|
||||
break
|
||||
|
||||
if should_exclude:
|
||||
continue
|
||||
|
||||
try:
|
||||
stat = file_path.stat()
|
||||
file_hash = self.calculate_file_hash(str(file_path))
|
||||
if file_hash:
|
||||
# Incluir la ruta relativa en el hash para detectar movimientos
|
||||
relative_path = file_path.relative_to(path)
|
||||
combined_hash_input = f"{relative_path}:{file_hash}".encode('utf-8')
|
||||
|
||||
if self.algorithm == "md5":
|
||||
combined_hash = hashlib.md5(combined_hash_input).hexdigest()
|
||||
else:
|
||||
combined_hash = hashlib.sha256(combined_hash_input).hexdigest()
|
||||
|
||||
file_hashes.append(combined_hash)
|
||||
file_count += 1
|
||||
total_size += stat.st_size
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Error procesando archivo {file_path}: {e}")
|
||||
continue
|
||||
|
||||
# Crear hash final combinando todos los hashes de archivos
|
||||
if file_hashes:
|
||||
# Ordenar para asegurar consistencia
|
||||
file_hashes.sort()
|
||||
combined_input = ":".join(file_hashes).encode('utf-8')
|
||||
|
||||
if self.algorithm == "md5":
|
||||
final_hash = hashlib.md5(combined_input).hexdigest()
|
||||
else:
|
||||
final_hash = hashlib.sha256(combined_input).hexdigest()
|
||||
|
||||
return final_hash, file_count, total_size
|
||||
else:
|
||||
return "", 0, 0
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error calculando hash de directorio {directory_path}: {e}")
|
||||
return "", 0, 0
|
||||
|
||||
def compare_hashes(self, hash1: str, hash2: str) -> bool:
|
||||
"""Comparar dos hashes de forma segura"""
|
||||
if not hash1 or not hash2:
|
||||
return False
|
||||
return hash1 == hash2
|
||||
|
||||
def has_s7p_changed(self, s7p_file_path: str, last_known_hash: str,
|
||||
last_known_timestamp: str, last_known_size: int) -> bool:
|
||||
"""
|
||||
Verificar si el archivo .s7p ha cambiado (Etapa 1 del sistema de hash)
|
||||
"""
|
||||
try:
|
||||
current_hash, current_timestamp, current_size = self.calculate_s7p_hash(s7p_file_path)
|
||||
|
||||
if not current_hash:
|
||||
return True # Si no se puede calcular, asumir que cambió
|
||||
|
||||
# Comparar hash actual con el último conocido
|
||||
if not last_known_hash:
|
||||
return True # Si no hay hash previo, definitivamente cambió
|
||||
|
||||
return not self.compare_hashes(current_hash, last_known_hash)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error verificando cambios en S7P {s7p_file_path}: {e}")
|
||||
return True # En caso de error, asumir que cambió
|
||||
|
||||
def has_directory_changed(self, directory_path: str, last_known_hash: str,
|
||||
exclude_patterns: List[str] = None) -> Tuple[bool, str, int, int]:
|
||||
"""
|
||||
Verificar si el directorio ha cambiado (Etapa 2 del sistema de hash)
|
||||
Retorna: (ha_cambiado, nuevo_hash, cantidad_archivos, tamaño_total)
|
||||
"""
|
||||
try:
|
||||
current_hash, file_count, total_size = self.calculate_directory_hash(
|
||||
directory_path, exclude_patterns
|
||||
)
|
||||
|
||||
if not current_hash:
|
||||
return True, "", 0, 0
|
||||
|
||||
if not last_known_hash:
|
||||
return True, current_hash, file_count, total_size
|
||||
|
||||
has_changed = not self.compare_hashes(current_hash, last_known_hash)
|
||||
return has_changed, current_hash, file_count, total_size
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error verificando cambios en directorio {directory_path}: {e}")
|
||||
return True, "", 0, 0
|
Loading…
Reference in New Issue