Problemas

This commit is contained in:
Miguel 2025-09-01 17:13:38 +02:00
parent be61956a54
commit 581aa551c4
20 changed files with 2822 additions and 1 deletions

View File

@ -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

258
.docs/SPECIFICATION.md Normal file
View File

@ -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.

View File

@ -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?

60
.docs/config.json Normal file
View File

@ -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"
}
}

62
.docs/projects.json Normal file
View File

@ -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
}
}

32
.docs/requirements.txt Normal file
View File

@ -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

216
autobackups/.gitignore vendored Normal file
View File

@ -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

72
autobackups/README.md Normal file
View File

@ -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.

View File

@ -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?

275
autobackups/src/app.py Normal file
View File

@ -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()

View File

@ -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']

View File

@ -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()

View File

@ -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()

View File

@ -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']

View File

@ -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"
}

View File

@ -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

View File

@ -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']

View File

@ -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

View File

@ -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

View File

@ -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