6 Commits

43 changed files with 3407 additions and 352 deletions

21
.gitignore vendored
View File

@@ -25,6 +25,8 @@ MANIFEST
# Virtual Environment # Virtual Environment
venv/ venv/
venv-windows/
venv-linux/
ENV/ ENV/
env/ env/
@@ -35,16 +37,25 @@ env/
*.swo *.swo
*~ *~
# Logs # Logs y datos locales
logs/*.log logs/*.log
logs/*.log.* logs/*.log.*
# Database
data/*.db data/*.db
data/*.db-journal data/*.db-journal
config/.env
config/data/
config/logs/
config/odbc/
config/7zip/
config/.bootstrap_ok
config/.autostart_registered
# PyInstaller # PyInstaller / empaquetado
*.spec packaging/bundled/
dist/installers/
# Variables de entorno locales
.env
# OS # OS
.DS_Store .DS_Store

191
BUILD.md Normal file
View File

@@ -0,0 +1,191 @@
# BUILD.md — Compilación, empaquetado y despliegue
Referencia de **todos** los comandos, ejecutables y scripts para generar, empaquetar,
instalar y verificar CloudRestoreAS en Windows y Linux.
El ejecutable es **autocontenido**: no requiere Python, 7-Zip, driver ODBC ni librerías
Qt instaladas en el equipo destino. Todo se embebe dentro del binario en tiempo de build.
---
## 1. Todo en una tarea (recomendado) — `build-all.sh`
Orquestador único. **Se ejecuta desde WSL** (bash). Genera Windows + Linux + los paquetes.
```bash
./build-all.sh # Windows + Linux + dist/release/*.tar.gz y *.zip
./build-all.sh --linux-only # solo Linux (Docker)
./build-all.sh --windows-only # solo Windows (PowerShell + build.ps1)
./build-all.sh --no-package # compila sin generar .tar.gz/.zip
./build-all.sh --clean # rebuild desde cero (borra venvs, bundled, dist)
./build-all.sh --help
```
**Requisitos:**
- Linux: **Docker** (usa `docker-build-linux.sh` en ubuntu:22.04).
- Windows: **`powershell.exe`** accesible desde WSL + **Python 3.11+** instalado en Windows
(`build.ps1` lo auto-detecta en `%LOCALAPPDATA%\Programs\Python\Python31X`).
- Es *fail-soft*: si no hay `powershell.exe`, compila solo Linux y avisa.
> El build de Windows se hace sobre la ruta `\\wsl.localhost\...`, por eso tarda
> (~15-18 min: pip install de PySide6 + PyInstaller). El de Linux (Docker, con deps
> cacheadas) es rápido.
`packaging/scripts/build-all.sh` es un wrapper que delega en este mismo script.
---
## 2. Builds individuales
| Objetivo | Comando | Salida | Notas |
|---|---|---|---|
| **Linux (Docker, recomendado)** | `bash packaging/scripts/docker-build-linux.sh` | `dist/CloudRestoreAS` | ubuntu:22.04 con todas las deps + `patchelf`; garantiza autocontención |
| **Linux (host)** | `./build.sh` | `dist/CloudRestoreAS` | Requiere que el host tenga las libs Qt/ODBC; usar solo si no hay Docker |
| **Windows** | `.\build.ps1` (en Windows/PowerShell) | `dist\CloudRestoreAS.exe` | Auto-detecta Python 3.11+; crea `venv-windows` |
Ambos usan el mismo spec: [packaging/CloudRestoreAS.spec](packaging/CloudRestoreAS.spec) (PyInstaller **onefile**, `console=False`).
---
## 3. Dependencias embebidas (build-time)
Se descargan e integran al binario. No se instalan en el destino.
| Script | Qué embebe |
|---|---|
| `packaging/scripts/download-bundled-deps.sh` (Linux) | `7zz` (7-Zip Linux), driver **MS ODBC 18** + unixODBC + Kerberos/GSSAPI + libltdl + OpenSSL, y cluster **Qt xcb/X11 + EGL**. Aplica `patchelf --set-rpath '$ORIGIN'` a las `.so` del cluster ODBC para que resuelvan entre sí. |
| `packaging/scripts/download-bundled-deps.ps1` (Windows) | `7z.exe` y `msodbcsql18.dll`. |
Versiones/URLs en [packaging/bundled-versions.json](packaging/bundled-versions.json).
Los binarios quedan en `packaging/bundled/{linux,windows}/` (git-ignored, se generan en el build).
**Ubicación en runtime** (creada por el bootstrap en la primera ejecución):
- `config/7zip/7zz` — extractor.
- `config/odbc/lib/` — driver ODBC + toda su cadena (resuelven por `$ORIGIN`).
- Cluster Qt xcb/EGL — dentro del onefile, en `PySide6/Qt/lib`.
---
## 4. Empaquetado — `package-release.sh`
```bash
bash packaging/scripts/package-release.sh
```
Genera en `dist/release/`:
| Archivo | Contenido |
|---|---|
| `CloudRestoreAS-linux.tar.gz` | `CloudRestoreAS/` → binario + `install.sh` + `packaging/linux/cloudrestoreas.service` + `LEEME.txt` |
| `CloudRestoreAS-win.zip` | `CloudRestoreAS/``CloudRestoreAS.exe` + `install.ps1` + `LEEME.txt` |
---
## 5. Instalación / despliegue
### Linux — `install.sh` (no instala nada del sistema)
```bash
tar xzf CloudRestoreAS-linux.tar.gz && cd CloudRestoreAS
sudo ./install.sh --service # servicio systemd 24/7 headless (recomendado en servidor)
./install.sh --desktop # autostart .desktop (requiere sesión gráfica)
./install.sh # solo instala + bootstrap; lo corres a mano
./install.sh --help
```
Variables: `PREFIX=/opt/cloudrestoreas` (destino), `SERVICE_USER=<usuario>` (usuario del servicio).
Servicio systemd:
```bash
systemctl status cloudrestoreas
journalctl -u cloudrestoreas -f
sudo systemctl restart cloudrestoreas # tras editar config/.env
```
### Windows
Copiar `CloudRestoreAS.exe` a una carpeta y ejecutarlo (o usar `install.ps1`). Al iniciar
crea `config/` y un icono en la bandeja. Ver [packaging/LEEME.txt](packaging/LEEME.txt).
---
## 6. Ejecución manual y flags del binario
```bash
# Linux servidor sin pantalla (headless): motor de restauración sin GUI
QT_QPA_PLATFORM=offscreen ./CloudRestoreAS --start-engine --headless
# Linux con escritorio o Windows: abre la GUI normal
./CloudRestoreAS
```
| Flag / variable | Efecto |
|---|---|
| `--start-engine` | Inicia el motor de restauración al arrancar |
| `--headless` | Fuerza modo sin interfaz (Qt `offscreen`), para servidores sin display |
| `--minimized` | Inicia minimizado en la bandeja |
| `QT_QPA_PLATFORM=offscreen` | Plataforma Qt sin display (el binario ya cae a esto automáticamente si no hay `DISPLAY`/`WAYLAND_DISPLAY` en Linux) |
En Linux sin `DISPLAY`, el binario selecciona `offscreen` **solo**; con display usa `xcb`.
---
## 7. Configuración (primera ejecución)
El binario crea automáticamente: `config/`, `config/.env`, `Entrada/`, `Procesados/`,
`Fallados/`, `Temp/`. Editar `config/.env`:
```
CLOUDRESTORE_AUTO_START=true # el motor arranca solo
CLOUDRESTORE_PANEL_API_URL=... # servicio PANEL_BASES_ANEXO24 (enrutamiento)
CLOUDRESTORE_PANEL_API_TOKEN=...
CLOUDRESTORE_PANEL_INSTANCE_KEY=...
```
El PANEL entrega el servidor SQL destino y su `data_folder`. Con SQL Server sobre Linux
serán rutas POSIX (p. ej. `/var/opt/mssql/data`); el `RESTORE ... MOVE` adapta el separador
automáticamente según el formato del `data_folder`.
---
## 8. Verificación de autocontención (contenedor pelado)
Confirma que el binario Linux corre sin instalar NADA del sistema:
```bash
docker run --rm -v "$PWD/dist:/dist:ro" debian:12-slim bash -c '
cp /dist/CloudRestoreAS /root/app && cd /root
QT_QPA_PLATFORM=offscreen timeout 12 ./app --headless --start-engine 2>&1 | \
grep -iE "offscreen|Ventana principal|Traceback|platform plugin"
# ODBC: driver + cadena resuelven por $ORIGIN (debe dar 0)
ldd /root/config/odbc/lib/libmsodbcsql-18*.so* 2>&1 | grep -c "not found"
# 7-Zip embebido
/root/config/7zip/7zz | head -1
'
```
Esperado: arranca en `offscreen` sin errores Qt, `0` deps ODBC faltantes, `7zz` ejecuta.
---
## 9. Artefactos y ubicaciones
| Ruta | Qué es |
|---|---|
| `dist/CloudRestoreAS` | Binario Linux onefile |
| `dist/CloudRestoreAS.exe` | Ejecutable Windows onefile |
| `dist/release/*.tar.gz` / `*.zip` | Paquetes de despliegue (binario + instalador + docs) |
| `packaging/bundled/{linux,windows}/` | Deps embebidas (generadas en build; git-ignored) |
| `venv-linux/`, `venv-windows/` | Entornos virtuales de build (git-ignored) |
---
## 10. Scripts de referencia rápida
| Script | Propósito |
|---|---|
| `build-all.sh` | **Todo en uno** (Windows + Linux + paquetes) desde WSL |
| `build.sh` / `build.ps1` | Build individual Linux / Windows |
| `packaging/scripts/docker-build-linux.sh` | Build Linux en contenedor controlado |
| `packaging/scripts/download-bundled-deps.sh` / `.ps1` | Descarga+embebe deps |
| `packaging/scripts/package-release.sh` | Genera `.tar.gz` / `.zip` |
| `install.sh` / `install.ps1` | Instalador Linux / Windows |
| `packaging/linux/cloudrestoreas.service` | Unit systemd (24/7 headless) |

View File

@@ -126,10 +126,10 @@ C:\CloudRestore\
## Para Ejecutar 24/7 ## Para Ejecutar 24/7
1. Inicia la aplicación 1. Inicia la aplicación (el motor arranca solo si `CLOUDRESTORE_AUTO_START=true`)
2. Menú Motor → Iniciar 2. Cierra la ventana con **X** — se minimiza a la bandeja del sistema
3. Minimiza (se va a la bandeja del sistema) 3. La app continuará monitoreando en segundo plano
4. La app continuará monitoreando en segundo plano 4. Para salir por completo: bandeja → **Salir** o menú **Archivo → Salir**
--- ---

View File

@@ -24,6 +24,9 @@ CloudRestoreAS es una aplicación Windows de escritorio desarrollada en Python 3
### Sistema Operativo ### Sistema Operativo
- Windows 10/11 - Windows 10/11
- Windows Server 2019/2022 o superior - Windows Server 2019/2022 o superior
- Linux x86-64 (servidor o escritorio). En servidor **headless** (sin pantalla) corre
automáticamente en modo Qt `offscreen`. El binario Linux es **autocontenido**: no
requiere instalar Python, 7-Zip, driver ODBC ni librerías Qt en el destino.
### Software Requerido ### Software Requerido
@@ -73,19 +76,49 @@ CloudRestoreAS es una aplicación Windows de escritorio desarrollada en Python 3
python runner.py python runner.py
``` ```
### Opción 2: Ejecutable Empaquetado ### Opción 2: Ejecutable portable (recomendado para producción)
1. **Generar el ejecutable** **Todo de una vez (desde WSL)** — genera Windows + Linux + los paquetes de release:
```powershell ```bash
.\build.ps1 ./build-all.sh # ambos + dist/release/*.tar.gz y *.zip
./build-all.sh --linux-only # solo Linux (Docker)
./build-all.sh --windows-only # solo Windows (PowerShell + build.ps1)
./build-all.sh --clean # rebuild desde cero
``` ```
Requiere Docker (para Linux) y, para Windows, `powershell.exe` accesible desde WSL con Python 3.11+ instalado en Windows.
Esto creará `CloudRestoreAS.exe` en la carpeta `dist\` **Solo Windows** — generar: `.\build.ps1` → `dist\CloudRestoreAS.exe`
**Solo Linux** — generar: `./build.sh` (o `packaging/scripts/docker-build-linux.sh` para el entorno controlado) → `dist/CloudRestoreAS`
2. **Distribuir** > 📦 Referencia completa de compilación, empaquetado, instalación (`install.sh`/systemd),
- Copiar `CloudRestoreAS.exe` a la ubicación deseada > flags del binario y verificación: **[BUILD.md](BUILD.md)**.
- Crear carpetas `data` y `logs` en el mismo directorio
- El ejecutable es portable (no requiere instalación) > El build de Linux embebe dentro del binario, además de la app: las libs de sistema
> de Qt (cluster xcb/X11 + EGL), el driver **ODBC 18** con sus dependencias
> (unixODBC, Kerberos/GSSAPI, OpenSSL) y el **7-Zip** (`7zz`). Por eso el destino no
> necesita `apt install` de nada. (El build se hace en un entorno con esas libs; ver
> `packaging/scripts/download-bundled-deps.sh` y `docker-build-linux.sh`.)
**Desplegar en cada equipo** (sin Python, sin instalador, sin dependencias de sistema):
1. Copiar **solo** el ejecutable a una carpeta.
2. Ejecutarlo (doble clic o `./CloudRestoreAS`).
3. Se crean automáticamente: `config/`, `Entrada/`, `Procesados/`, `Fallados/`, `Temp/`.
4. Editar `config/.env` (URL, token e instancia del panel).
5. Reiniciar la aplicación.
**Despliegue en servidor Linux (headless / SQL Server sobre Linux):**
```bash
sudo ./install.sh --service # servicio systemd 24/7 (offscreen, motor auto-inicio)
# o manual:
QT_QPA_PLATFORM=offscreen ./CloudRestoreAS --start-engine --headless
```
`install.sh` **no instala ni descarga nada** del sistema: solo coloca el binario, hace
el bootstrap de `config/` y registra el arranque (systemd headless o autostart de
escritorio con `--desktop`). El `data_folder` del destino viene del PANEL; con SQL
Server sobre Linux serán rutas POSIX (p. ej. `/var/opt/mssql/data`) y el `RESTORE`
las adapta automáticamente.
Ver `packaging/LEEME.txt` para instrucciones resumidas.
## ⚙️ Configuración Inicial ## ⚙️ Configuración Inicial

36
app/config/__init__.py Normal file
View File

@@ -0,0 +1,36 @@
"""Configuración de entorno, bootstrap y arranque."""
from .bootstrap import ensure_runtime_layout
from .env_loader import (
apply_env_overrides,
get_launch_options,
is_panel_configured,
load_env_file,
)
from .odbc_setup import configure_odbc_environment
from .autostart import register_autostart_if_requested
def initialize_runtime() -> None:
"""Prepara directorios, ODBC y auto-arranque (llamar antes de importar pyodbc)."""
ensure_runtime_layout()
configure_odbc_environment()
def initialize_env() -> None:
"""Post-bootstrap: carga .env y registra auto-arranque si aplica."""
load_env_file()
register_autostart_if_requested()
__all__ = [
"initialize_runtime",
"initialize_env",
"ensure_runtime_layout",
"load_env_file",
"apply_env_overrides",
"get_launch_options",
"is_panel_configured",
"configure_odbc_environment",
"register_autostart_if_requested",
]

88
app/config/autostart.py Normal file
View File

@@ -0,0 +1,88 @@
"""Registro de arranque automático al login (Windows / Linux)."""
import os
import subprocess
import sys
from pathlib import Path
from ..constants import APP_DIR, CONFIG_DIR, IS_WINDOWS
from .env_loader import _env_bool, load_env_file
def _marker_path() -> Path:
return CONFIG_DIR / ".autostart_registered"
def _executable_command() -> tuple[str, list[str]]:
if getattr(sys, "frozen", False):
exe = Path(sys.executable).resolve()
return str(exe), ["--start-engine"]
runner = (APP_DIR / "runner.py").resolve()
return sys.executable, [str(runner), "--start-engine"]
def _register_windows(exe: str, args: list[str]) -> None:
task_name = "CloudRestoreAS"
arg_str = " ".join(f'"{a}"' if " " in a else a for a in args)
tr = f'"{exe}" {arg_str}'
check = subprocess.run(
["schtasks", "/Query", "/TN", task_name],
capture_output=True,
creationflags=subprocess.CREATE_NO_WINDOW if hasattr(subprocess, "CREATE_NO_WINDOW") else 0,
)
if check.returncode == 0:
return
subprocess.run(
[
"schtasks",
"/Create",
"/TN",
task_name,
"/TR",
tr,
"/SC",
"ONLOGON",
"/RL",
"LIMITED",
"/F",
],
check=False,
creationflags=subprocess.CREATE_NO_WINDOW if hasattr(subprocess, "CREATE_NO_WINDOW") else 0,
)
def _register_linux(exe: str, args: list[str]) -> None:
autostart_dir = Path.home() / ".config" / "autostart"
autostart_dir.mkdir(parents=True, exist_ok=True)
desktop = autostart_dir / "cloudrestoreas.desktop"
if desktop.exists():
return
exec_line = " ".join([exe, *args])
content = f"""[Desktop Entry]
Type=Application
Name=CloudRestoreAS
Comment=Restauración automática SQL Server
Exec={exec_line}
Path={APP_DIR}
Terminal=false
X-GNOME-Autostart-enabled=true
"""
desktop.write_text(content, encoding="utf-8")
def register_autostart_if_requested() -> None:
load_env_file()
if not _env_bool("CLOUDRESTORE_REGISTER_AUTOSTART", True):
return
if _marker_path().exists():
return
exe, args = _executable_command()
try:
if IS_WINDOWS:
_register_windows(exe, args)
else:
_register_linux(exe, args)
_marker_path().write_text("ok", encoding="utf-8")
except OSError:
pass

107
app/config/bootstrap.py Normal file
View File

@@ -0,0 +1,107 @@
"""Creación automática de config/ y carpetas de trabajo."""
import shutil
import sys
from pathlib import Path
from ..constants import (
APP_DIR,
BUNDLE_DIR,
BUNDLED_SOURCE_7ZIP,
BUNDLED_SOURCE_ODBC,
CONFIG_DIR,
DATA_DIR,
DIR_ENTRADA,
DIR_FALLADOS,
DIR_PROCESADOS,
DIR_TEMP,
ENV_PATH,
IS_WINDOWS,
LOGS_DIR,
ODBC_DIR,
SEVEN_ZIP_DIR,
)
from ..db.database import DatabaseManager
from .env_loader import render_env_template
def _copy_tree_if_missing(src: Path, dest: Path) -> None:
if not src.is_dir():
return
# dest puede existir pero VACÍO: ensure_runtime_layout crea ODBC_DIR/SEVEN_ZIP_DIR
# antes de llamar aquí. Solo saltar si ya tiene contenido (evita re-copiar en cada
# arranque). Antes se saltaba por dest.exists(), dejando 7-Zip/ODBC sin desplegar.
if dest.exists() and any(dest.iterdir()):
return
dest.mkdir(parents=True, exist_ok=True)
shutil.copytree(src, dest, dirs_exist_ok=True)
def _ensure_seven_zip_executable() -> None:
"""
Asegura el bit de ejecución del 7-Zip embebido en Linux/macOS. shutil.copytree
suele preservar los permisos, pero esto es una salvaguarda barata: si el binario
quedó sin 'x', la extracción fallaría con PermissionError.
"""
if IS_WINDOWS:
return
for exe_name in ("7zz", "7za", "7z"):
exe_path = SEVEN_ZIP_DIR / exe_name
if exe_path.is_file():
try:
exe_path.chmod(0o755)
except OSError:
pass
def _write_env_if_missing() -> None:
if ENV_PATH.exists():
return
template_path = BUNDLE_DIR / "packaging" / "templates" / "env.default"
if not template_path.is_file():
template_path = APP_DIR / "packaging" / "templates" / "env.default"
if template_path.is_file():
content = template_path.read_text(encoding="utf-8")
content = content.replace("{APP_DIR}", str(APP_DIR))
else:
content = render_env_template(APP_DIR)
ENV_PATH.write_text(content, encoding="utf-8")
try:
ENV_PATH.chmod(0o600)
except OSError:
pass
def ensure_runtime_layout() -> None:
"""
Idempotente: crea config/, carpetas de trabajo y .env si no existen.
No sobrescribe .env ni app.db existentes.
"""
for directory in (
CONFIG_DIR,
DATA_DIR,
LOGS_DIR,
ODBC_DIR,
SEVEN_ZIP_DIR,
DIR_ENTRADA,
DIR_PROCESADOS,
DIR_FALLADOS,
DIR_TEMP,
):
directory.mkdir(parents=True, exist_ok=True)
_write_env_if_missing()
_copy_tree_if_missing(BUNDLED_SOURCE_7ZIP, SEVEN_ZIP_DIR)
_copy_tree_if_missing(BUNDLED_SOURCE_ODBC, ODBC_DIR)
_ensure_seven_zip_executable()
from ..constants import DB_PATH
if not DB_PATH.exists():
DatabaseManager(DB_PATH)
if getattr(sys, "frozen", False):
marker = CONFIG_DIR / ".bootstrap_ok"
if not marker.exists():
marker.write_text("ok", encoding="utf-8")

139
app/config/env_loader.py Normal file
View File

@@ -0,0 +1,139 @@
"""Carga de variables desde config/.env."""
import os
from dataclasses import dataclass
from pathlib import Path
from dotenv import load_dotenv
from ..constants import (
APP_DIR,
DIR_ENTRADA,
DIR_FALLADOS,
DIR_PROCESADOS,
DIR_TEMP,
ENV_PATH,
default_seven_zip_path,
)
def _env_bool(name: str, default: bool = False) -> bool:
raw = os.getenv(name, "").strip().lower()
if not raw:
return default
return raw in ("1", "true", "yes", "on")
def load_env_file() -> bool:
"""Carga config/.env si existe."""
if ENV_PATH.is_file():
load_dotenv(ENV_PATH, override=True)
return True
return False
def is_panel_configured() -> bool:
"""True si URL, token e instancia del panel están definidos."""
url = os.getenv("CLOUDRESTORE_PANEL_API_URL", "").strip()
token = os.getenv("CLOUDRESTORE_PANEL_API_TOKEN", "").strip()
instance = os.getenv("CLOUDRESTORE_PANEL_INSTANCE_KEY", "").strip()
return bool(url and token and instance)
@dataclass
class LaunchOptions:
minimized: bool
start_engine: bool
register_autostart: bool
def get_launch_options() -> LaunchOptions:
load_env_file()
return LaunchOptions(
minimized=_env_bool("CLOUDRESTORE_START_MINIMIZED", False),
start_engine=_env_bool("CLOUDRESTORE_AUTO_START", True),
register_autostart=_env_bool("CLOUDRESTORE_REGISTER_AUTOSTART", True),
)
def _path_from_env(name: str, default: Path) -> str:
value = os.getenv(name, "").strip()
return value if value else str(default)
def apply_env_overrides(config: dict) -> dict:
"""Aplica variables de entorno sobre la configuración en memoria."""
load_env_file()
paths = config.setdefault("paths", {})
paths["input_folder"] = _path_from_env("CLOUDRESTORE_INPUT_FOLDER", DIR_ENTRADA)
paths["processed_folder"] = _path_from_env(
"CLOUDRESTORE_PROCESSED_FOLDER", DIR_PROCESADOS
)
paths["failed_folder"] = _path_from_env("CLOUDRESTORE_FAILED_FOLDER", DIR_FALLADOS)
paths["extract_folder"] = _path_from_env("CLOUDRESTORE_EXTRACT_FOLDER", DIR_TEMP)
data_sql = os.getenv("CLOUDRESTORE_DATA_SQL_FOLDER", "").strip()
if data_sql:
paths["data_sql_folder"] = data_sql
seven_zip = os.getenv("CLOUDRESTORE_SEVEN_ZIP_EXE", "").strip()
paths["seven_zip_exe"] = seven_zip or default_seven_zip_path()
panel = config.setdefault("panel", {})
api_url = os.getenv("CLOUDRESTORE_PANEL_API_URL", "").strip()
if api_url:
panel["api_url"] = api_url
api_token = os.getenv("CLOUDRESTORE_PANEL_API_TOKEN", "").strip()
if api_token:
panel["api_token"] = api_token
instance = os.getenv("CLOUDRESTORE_PANEL_INSTANCE_KEY", "").strip()
if instance:
panel["instance_key"] = instance
if os.getenv("CLOUDRESTORE_PANEL_VERIFY_SSL"):
panel["verify_ssl"] = _env_bool("CLOUDRESTORE_PANEL_VERIFY_SSL", False)
sql = config.setdefault("sql", {})
server = os.getenv("CLOUDRESTORE_SQL_SERVER", "").strip()
if server:
sql["server"] = server
if os.getenv("CLOUDRESTORE_SQL_USERNAME"):
sql["username"] = os.getenv("CLOUDRESTORE_SQL_USERNAME", "").strip()
if os.getenv("CLOUDRESTORE_SQL_USE_WINDOWS_AUTH"):
sql["use_windows_auth"] = _env_bool("CLOUDRESTORE_SQL_USE_WINDOWS_AUTH", False)
return config
def render_env_template(app_dir: Path | None = None) -> str:
"""Genera contenido inicial de config/.env."""
base = (app_dir or APP_DIR).resolve()
sep = "\\" if os.name == "nt" else "/"
def p(*parts: str) -> str:
return sep.join([str(base), *parts])
return f"""# CloudRestoreAS — configuración local (editar y reiniciar la app)
# Arranca el motor al abrir. La ventana SIEMPRE se muestra al iniciar; cerrar (X) la
# minimiza a la bandeja del sistema (START_MINIMIZED ya no oculta la ventana).
CLOUDRESTORE_AUTO_START=true
CLOUDRESTORE_START_MINIMIZED=false
CLOUDRESTORE_REGISTER_AUTOSTART=true
# Servicio PANEL_BASES_ANEXO24 (enrutamiento automático de nodos)
CLOUDRESTORE_PANEL_API_URL=
CLOUDRESTORE_PANEL_API_TOKEN=
CLOUDRESTORE_PANEL_INSTANCE_KEY=
CLOUDRESTORE_PANEL_VERIFY_SSL=false
# Carpetas de trabajo
CLOUDRESTORE_INPUT_FOLDER={p("Entrada")}
CLOUDRESTORE_PROCESSED_FOLDER={p("Procesados")}
CLOUDRESTORE_FAILED_FOLDER={p("Fallados")}
CLOUDRESTORE_EXTRACT_FOLDER={p("Temp")}
# SQL local (solo si no usa panel para credenciales)
# CLOUDRESTORE_SQL_SERVER=localhost
# CLOUDRESTORE_SQL_USERNAME=
# CLOUDRESTORE_DATA_SQL_FOLDER=
"""

84
app/config/odbc_setup.py Normal file
View File

@@ -0,0 +1,84 @@
"""Configura ODBC portable desde config/odbc/."""
import os
import sys
from pathlib import Path
from ..constants import IS_WINDOWS, ODBC_DIR
from ..utils.logger import app_logger
def _write_portable_odbc_ini(odbc_dir: Path) -> None:
"""Genera odbcinst.ini mínimo si no existe."""
inst = odbc_dir / "odbcinst.ini"
if inst.exists():
return
if IS_WINDOWS:
driver_path = odbc_dir / "msodbcsql18.dll"
if not driver_path.exists():
for candidate in odbc_dir.glob("msodbcsql*.dll"):
driver_path = candidate
break
driver_line = str(driver_path.resolve()) if driver_path.exists() else "msodbcsql18.dll"
content = f"""[ODBC Driver 18 for SQL Server]
Description=Microsoft ODBC Driver 18 for SQL Server
Driver={driver_line}
UsageCount=1
"""
else:
# Los .so embebidos viven en config/odbc/lib (no en la raíz). Buscar ahí
# primero y usar SIEMPRE la ruta absoluta del archivo versionado real
# (p. ej. libmsodbcsql-18.5.so.1.1); un nombre suelto como
# "libmsodbcsql-18.so" no resuelve porque ese symlink apunta a /opt.
lib_dir = odbc_dir / "lib"
search_dirs = [d for d in (lib_dir, odbc_dir) if d.is_dir()]
so_path = None
for base in search_dirs:
# Preferir el archivo real versionado sobre symlinks (que pueden estar rotos).
candidates = sorted(
(p for p in base.glob("libmsodbcsql-18*.so*") if p.is_file() and not p.is_symlink()),
key=lambda p: len(p.name),
reverse=True,
)
if not candidates:
candidates = [p for p in base.glob("libmsodbcsql-18*.so*") if p.exists()]
if candidates:
so_path = candidates[0]
break
driver_line = str(so_path.resolve()) if so_path else "libmsodbcsql-18.so"
content = f"""[ODBC Driver 18 for SQL Server]
Description=Microsoft ODBC Driver 18 for SQL Server
Driver={driver_line}
UsageCount=1
"""
inst.write_text(content, encoding="utf-8")
ini = odbc_dir / "odbc.ini"
if not ini.exists():
ini.write_text("[ODBC Data Sources]\n", encoding="utf-8")
def configure_odbc_environment() -> None:
"""Apunta pyodbc al driver embebido en config/odbc si está disponible."""
if not ODBC_DIR.is_dir():
return
lib_dir = ODBC_DIR / "lib"
if lib_dir.is_dir():
existing = os.environ.get("LD_LIBRARY_PATH", "")
lib_path = str(lib_dir.resolve())
if lib_path not in existing.split(":"):
os.environ["LD_LIBRARY_PATH"] = (
f"{lib_path}:{existing}" if existing else lib_path
)
_write_portable_odbc_ini(ODBC_DIR)
odbc_sys = str(ODBC_DIR.resolve())
os.environ["ODBCSYSINI"] = odbc_sys
os.environ["ODBCINI"] = str((ODBC_DIR / "odbc.ini").resolve())
if IS_WINDOWS:
os.environ["PATH"] = odbc_sys + os.pathsep + os.environ.get("PATH", "")
app_logger.debug(f"ODBC configurado: ODBCSYSINI={odbc_sys}")

View File

@@ -1,13 +1,51 @@
"""Constantes globales de la aplicación.""" """Constantes globales de la aplicación."""
import sys
from pathlib import Path from pathlib import Path
# Rutas base
APP_DIR = Path(__file__).parent.parent
DATA_DIR = APP_DIR / "data"
LOGS_DIR = APP_DIR / "logs"
# Base de datos def _resolve_app_dir() -> Path:
"""Directorio donde vive el ejecutable (persistente)."""
if getattr(sys, "frozen", False):
return Path(sys.executable).resolve().parent
return Path(__file__).resolve().parent.parent
def _resolve_bundle_dir() -> Path:
"""Recursos embebidos en el binario PyInstaller onefile."""
if getattr(sys, "frozen", False):
return Path(sys._MEIPASS)
platform = "windows" if sys.platform == "win32" else "linux"
return _resolve_app_dir() / "packaging" / "bundled" / platform
APP_DIR = _resolve_app_dir()
CONFIG_DIR = APP_DIR / "config"
ENV_PATH = CONFIG_DIR / ".env"
DATA_DIR = CONFIG_DIR / "data"
LOGS_DIR = CONFIG_DIR / "logs"
ODBC_DIR = CONFIG_DIR / "odbc"
SEVEN_ZIP_DIR = CONFIG_DIR / "7zip"
IS_WINDOWS = sys.platform == "win32"
BUNDLE_DIR = _resolve_bundle_dir()
BUNDLED_SOURCE_7ZIP = BUNDLE_DIR / "bundled" / "7zip"
BUNDLED_SOURCE_ODBC = BUNDLE_DIR / "bundled" / "odbc"
def _default_7zip_name() -> str:
if IS_WINDOWS:
return "7z.exe"
return "7zz"
BUNDLED_7ZIP_EXE = SEVEN_ZIP_DIR / _default_7zip_name()
BUNDLED_7ZIP_FALLBACK = SEVEN_ZIP_DIR / ("7za.exe" if IS_WINDOWS else "7zz")
# Carpetas de trabajo junto al ejecutable
DIR_ENTRADA = APP_DIR / "Entrada"
DIR_PROCESADOS = APP_DIR / "Procesados"
DIR_FALLADOS = APP_DIR / "Fallados"
DIR_TEMP = APP_DIR / "Temp"
DB_PATH = DATA_DIR / "app.db" DB_PATH = DATA_DIR / "app.db"
# Estados de jobs # Estados de jobs
@@ -21,7 +59,7 @@ class JobStatus:
FAILED_RESTART = "failed_restart" FAILED_RESTART = "failed_restart"
CANCELLED = "cancelled" CANCELLED = "cancelled"
# Niveles de log
class LogLevel: class LogLevel:
DEBUG = "DEBUG" DEBUG = "DEBUG"
INFO = "INFO" INFO = "INFO"
@@ -29,7 +67,7 @@ class LogLevel:
ERROR = "ERROR" ERROR = "ERROR"
CRITICAL = "CRITICAL" CRITICAL = "CRITICAL"
# Tipos de paso de job
class StepType: class StepType:
STABILITY_CHECK = "stability_check" STABILITY_CHECK = "stability_check"
NODE_MAPPING = "node_mapping" NODE_MAPPING = "node_mapping"
@@ -39,57 +77,63 @@ class StepType:
FILELIST = "filelist" FILELIST = "filelist"
RESTORE = "restore" RESTORE = "restore"
CLEANUP = "cleanup" CLEANUP = "cleanup"
SFTP_COPY = "sftp_copy" # Transferencia del .bak al servidor SQL externo (SFTP/SSH) SFTP_COPY = "sftp_copy"
FORWARD_ZIP = "forward_zip" # Reenvío del ZIP al input_folder del servidor destino FORWARD_ZIP = "forward_zip"
# Configuración por defecto
DEFAULT_CONFIG = { def default_seven_zip_path() -> str:
"paths": { for candidate in (BUNDLED_7ZIP_EXE, BUNDLED_7ZIP_FALLBACK):
"input_folder": "", if candidate.exists():
"processed_folder": "", return str(candidate)
"failed_folder": "", return ""
"extract_folder": "",
def default_work_paths() -> dict[str, str]:
return {
"input_folder": str(DIR_ENTRADA),
"processed_folder": str(DIR_PROCESADOS),
"failed_folder": str(DIR_FALLADOS),
"extract_folder": str(DIR_TEMP),
"data_sql_folder": "", "data_sql_folder": "",
"seven_zip_exe": "" "seven_zip_exe": default_seven_zip_path(),
}, }
DEFAULT_CONFIG = {
"paths": default_work_paths(),
"sql": { "sql": {
"server": "localhost", "server": "localhost",
"use_windows_auth": True, "use_windows_auth": IS_WINDOWS,
"username": "", "username": "",
"password_encrypted": "" "password_encrypted": "",
}, },
"concurrency": { "concurrency": {
"extract_workers": 1, "extract_workers": 1,
"restore_workers": 1 "restore_workers": 1,
}, },
"stability": { "stability": {
"check_enabled": True, "check_enabled": True,
"check_interval_seconds": 5, "check_interval_seconds": 5,
"stable_duration_seconds": 10, "stable_duration_seconds": 10,
"use_ready_marker": False "use_ready_marker": False,
}, },
"timeouts": { "timeouts": {
"extract_minutes": 30, "extract_minutes": 30,
"restore_minutes": 60 "restore_minutes": 60,
}, },
"retries": { "retries": {
"max_attempts": 3, "max_attempts": 3,
"retry_delay_seconds": 60 "retry_delay_seconds": 60,
}, },
"features": { "features": {
"dry_run_mode": False, "dry_run_mode": False,
"auto_scan_enabled": True, "auto_scan_enabled": True,
"scan_interval_seconds": 30 "scan_interval_seconds": 30,
}, },
# Integración con PANEL_BASES_ANEXO24. Si api_url está vacío, CloudRestoreAS
# opera en modo local usando la sección "sql" (retrocompatibilidad).
"panel": { "panel": {
"api_url": "", "api_url": "",
"api_token": "", "api_token": "",
# Nombre del restore_target en el panel (identidad de este CRA).
# Con panel configurado: enrutamiento automático restore_local | forward.
"instance_key": "", "instance_key": "",
# False en dev: el panel local usa HTTPS con certificado propio.
"verify_ssl": False, "verify_ssl": False,
} },
} }

View File

@@ -12,6 +12,7 @@ from .. import __version__
from ..db.job_repository import JobRepository from ..db.job_repository import JobRepository
from ..db.event_repository import EventRepository from ..db.event_repository import EventRepository
from ..db.config_repository import ConfigRepository from ..db.config_repository import ConfigRepository
from ..config.env_loader import apply_env_overrides, load_env_file
from ..constants import JobStatus, DEFAULT_CONFIG from ..constants import JobStatus, DEFAULT_CONFIG
from ..panel import panel_client from ..panel import panel_client
from ..utils.logger import app_logger from ..utils.logger import app_logger
@@ -50,14 +51,15 @@ class RestoreEngine(QObject):
app_logger.info("RestoreEngine inicializado") app_logger.info("RestoreEngine inicializado")
def _load_config(self) -> dict: def _load_config(self) -> dict:
"""Carga la configuración desde la base de datos.""" """Carga la configuración desde la base de datos y variables de entorno."""
load_env_file()
config = ConfigRepository.get("app_config", DEFAULT_CONFIG.copy()) config = ConfigRepository.get("app_config", DEFAULT_CONFIG.copy())
# Asegurar que tiene todas las claves
for key, value in DEFAULT_CONFIG.items(): for key, value in DEFAULT_CONFIG.items():
if key not in config: if key not in config:
config[key] = value config[key] = value
config = apply_env_overrides(config)
self.signals.config_loaded.emit(config) self.signals.config_loaded.emit(config)
return config return config

View File

@@ -335,10 +335,16 @@ class RestoreWorker(QRunnable):
stderr=stderr[:1000] if stderr else None stderr=stderr[:1000] if stderr else None
) )
# Buscar archivo .bak # Buscar archivo .bak. El backup puede venir con terminaciones extra
# (p.ej. ".KNOWNWORLD") tanto en el ZIP como en el propio .bak; se
# normaliza a "<nodo>.bak". El nodo es siempre el primer segmento del
# nombre del archivo de origen.
locate_step_id = JobStepRepository.create(self.job_id, StepType.LOCATE_BAK) locate_step_id = JobStepRepository.create(self.job_id, StepType.LOCATE_BAK)
self._bak_path = SevenZipExtractor.find_bak_file(self._extract_dir) node_base = Path(job.source_name).name.split(".")[0].strip() or None
self._bak_path = SevenZipExtractor.find_bak_file(
self._extract_dir, node_name=node_base
)
if not self._bak_path: if not self._bak_path:
raise FileNotFoundError("No se encontró archivo .bak en el archivo extraído") raise FileNotFoundError("No se encontró archivo .bak en el archivo extraído")

View File

@@ -1,9 +1,13 @@
"""Gestión de extracción de archivos con 7-Zip.""" """Gestión de extracción de archivos con 7-Zip."""
import shutil
import subprocess import subprocess
import sys
from pathlib import Path from pathlib import Path
from typing import Optional, Tuple from typing import Optional, Tuple
import time import time
from ..constants import BUNDLED_7ZIP_EXE, BUNDLED_7ZIP_FALLBACK, IS_WINDOWS
from ..utils.logger import app_logger from ..utils.logger import app_logger
@@ -25,17 +29,28 @@ class SevenZipExtractor:
@staticmethod @staticmethod
def _auto_detect_7zip() -> Optional[str]: def _auto_detect_7zip() -> Optional[str]:
"""Auto-detecta la ubicación de 7z.exe.""" """Prioriza 7-Zip embebido; en desarrollo busca en el sistema."""
for candidate in (BUNDLED_7ZIP_EXE, BUNDLED_7ZIP_FALLBACK):
if candidate.exists():
app_logger.info(f"7-Zip embebido: {candidate}")
return str(candidate)
if not getattr(sys, "frozen", False):
if IS_WINDOWS:
possible_paths = [ possible_paths = [
r"C:\Program Files\7-Zip\7z.exe", r"C:\Program Files\7-Zip\7z.exe",
r"D:\Program Files\7-Zip\7z.exe",
r"C:\Program Files (x86)\7-Zip\7z.exe", r"C:\Program Files (x86)\7-Zip\7z.exe",
r"D:\Program Files (x86)\7-Zip\7z.exe"
] ]
else:
possible_paths = []
for name in ("7z", "7zz", "7za"):
found = shutil.which(name)
if found:
possible_paths.append(found)
for path in possible_paths: for path in possible_paths:
if Path(path).exists(): if Path(path).exists():
app_logger.info(f"7-Zip auto-detectado en: {path}") app_logger.info(f"7-Zip detectado en: {path}")
return path return path
return None return None
@@ -134,35 +149,78 @@ class SevenZipExtractor:
return result.returncode, result.stdout, result.stderr return result.returncode, result.stdout, result.stderr
@staticmethod @staticmethod
def find_bak_file(extract_dir: str) -> Optional[str]: def find_bak_file(extract_dir: str, node_name: Optional[str] = None) -> Optional[str]:
""" """
Busca el archivo .bak extraído en el directorio. Busca el backup extraído y, si se indica node_name, lo normaliza a
'<node_name>.bak'.
Tolera terminaciones extra en el nombre (el backup puede venir como
'nodo.KNOWNWORLD.bak', 'nodo.bak.unknownworld' o incluso sin extensión
'.bak'). Estrategia de búsqueda en orden:
1. archivos que terminan exactamente en '.bak'
2. si no hay, archivos cuyo nombre contiene '.bak' en cualquier parte
3. si no hay, el archivo más grande de la extracción (el backup suele serlo)
Args: Args:
extract_dir: Directorio donde se extrajo extract_dir: Directorio donde se extrajo
node_name: Nombre del nodo; si se indica, el backup se renombra a
'<node_name>.bak'
Returns: Returns:
Ruta al archivo .bak o None si no se encuentra Ruta al archivo .bak (ya renombrado si node_name) o None si no hay archivos.
Raises:
ValueError: Si hay múltiples archivos .bak
""" """
extract_path = Path(extract_dir) extract_path = Path(extract_dir)
bak_files = list(extract_path.rglob("*.bak")) all_files = [p for p in extract_path.rglob("*") if p.is_file()]
if not bak_files: if not all_files:
app_logger.error(f"No se encontró archivo .bak en {extract_dir}") app_logger.error(f"No se encontró ningún archivo en {extract_dir}")
return None return None
if len(bak_files) > 1: bak_files = [p for p in all_files if p.suffix.lower() == ".bak"]
# Si hay múltiples, tomar el más reciente if not bak_files:
bak_files = [p for p in all_files if ".bak" in p.name.lower()]
if not bak_files:
largest = max(all_files, key=lambda p: p.stat().st_size)
app_logger.warning( app_logger.warning(
f"Múltiples archivos .bak encontrados ({len(bak_files)}), " "No se encontró archivo con extensión .bak; usando el archivo más "
f"grande de la extracción: {largest.name}"
)
bak_files = [largest]
if len(bak_files) > 1:
# Preferir el que empiece con el nombre del nodo; si no, el más reciente.
if node_name:
prefixed = [
p for p in bak_files
if p.name.lower().startswith(node_name.lower())
]
if prefixed:
bak_files = prefixed
if len(bak_files) > 1:
app_logger.warning(
f"Múltiples backups encontrados ({len(bak_files)}), "
"seleccionando el más reciente" "seleccionando el más reciente"
) )
bak_files.sort(key=lambda p: p.stat().st_mtime, reverse=True) bak_files.sort(key=lambda p: p.stat().st_mtime, reverse=True)
bak_path = str(bak_files[0]) bak_file = bak_files[0]
# Normalizar el nombre a '<node_name>.bak'.
if node_name:
target = bak_file.parent / f"{node_name}.bak"
if bak_file.name != target.name:
try:
if target.exists() and not bak_file.samefile(target):
target.unlink()
bak_file = bak_file.rename(target)
app_logger.info(f"Backup normalizado a: {bak_file.name}")
except OSError as e:
# No es fatal para el RESTORE: se continúa con el nombre original.
app_logger.error(
f"No se pudo renombrar el backup a '{target.name}': {e}"
)
bak_path = str(bak_file)
app_logger.info(f"Archivo .bak encontrado: {bak_path}") app_logger.info(f"Archivo .bak encontrado: {bak_path}")
return bak_path return bak_path

View File

@@ -4,8 +4,28 @@ import pyodbc
from typing import Optional, Tuple, List from typing import Optional, Tuple, List
import time import time
from dataclasses import dataclass from dataclasses import dataclass
from ..utils.logger import app_logger from ..utils.logger import app_logger
_ODBC_DRIVER_CANDIDATES = (
"ODBC Driver 18 for SQL Server",
"ODBC Driver 17 for SQL Server",
"ODBC Driver 13 for SQL Server",
"FreeTDS",
)
def resolve_odbc_driver() -> str:
"""Devuelve el primer driver ODBC de SQL Server disponible."""
installed = set(pyodbc.drivers())
for name in _ODBC_DRIVER_CANDIDATES:
if name in installed:
return name
raise RuntimeError(
"No se encontró driver ODBC para SQL Server. "
f"Instalados: {', '.join(pyodbc.drivers()) or 'ninguno'}"
)
@dataclass @dataclass
class LogicalFile: class LogicalFile:
@@ -50,10 +70,13 @@ class SQLServerManager:
Returns: Returns:
Cadena de conexión ODBC Cadena de conexión ODBC
""" """
driver = resolve_odbc_driver()
parts = [ parts = [
f"DRIVER={{ODBC Driver 17 for SQL Server}}", f"DRIVER={{{driver}}}",
f"SERVER={self.server}" f"SERVER={self.server}",
] ]
if "18" in driver:
parts.append("TrustServerCertificate=yes")
if database: if database:
parts.append(f"DATABASE={database}") parts.append(f"DATABASE={database}")
@@ -355,6 +378,24 @@ class SQLServerManager:
except Exception: except Exception:
app_logger.warning("No se pudo cerrar la conexión SQL tras el RESTORE") app_logger.warning("No se pudo cerrar la conexión SQL tras el RESTORE")
@staticmethod
def _sql_path_sep(data_folder: str) -> str:
"""
Separador de rutas que espera el SQL Server destino, deducido del formato
de data_folder. Windows (C:\\..., o contiene '\\') usa '\\'; SQL Server
sobre Linux (rutas POSIX '/var/opt/mssql/...') usa '/'. Las rutas del MOVE
son del filesystem del SERVIDOR, no de donde corre la app.
"""
if "\\" in data_folder:
return "\\"
# Ruta POSIX explícita ("/...") o sin separador Windows → '/'.
if data_folder.startswith("/"):
return "/"
# Heurística: "C:" u otra letra de unidad ⇒ Windows.
if len(data_folder) >= 2 and data_folder[1] == ":":
return "\\"
return "/"
@staticmethod @staticmethod
def _build_move_clauses(db_name, data_folder, logical_files) -> list: def _build_move_clauses(db_name, data_folder, logical_files) -> list:
""" """
@@ -363,7 +404,13 @@ class SQLServerManager:
- 1er data → {db}.mdf, siguientes → {db}_N.ndf - 1er data → {db}.mdf, siguientes → {db}_N.ndf
- 1er log → {db}_log.ldf, siguientes → {db}_log_N.ldf - 1er log → {db}_log.ldf, siguientes → {db}_log_N.ldf
- otros tipos (FILESTREAM/full-text) → {db}_{nombre_lógico_saneado} - otros tipos (FILESTREAM/full-text) → {db}_{nombre_lógico_saneado}
El separador se adapta al SO del SQL Server destino (deducido de
data_folder), de modo que funciona tanto con SQL Server en Windows
(C:\\...\\db.mdf) como en Linux (/var/opt/mssql/data/db.mdf).
""" """
sep = SQLServerManager._sql_path_sep(data_folder)
base = data_folder.rstrip("\\/")
clauses = [] clauses = []
data_idx = 0 data_idx = 0
log_idx = 0 log_idx = 0
@@ -371,16 +418,16 @@ class SQLServerManager:
if lf.type == 'D': if lf.type == 'D':
suffix = "" if data_idx == 0 else f"_{data_idx}" suffix = "" if data_idx == 0 else f"_{data_idx}"
ext = "mdf" if data_idx == 0 else "ndf" ext = "mdf" if data_idx == 0 else "ndf"
new_path = f"{data_folder}\\{db_name}{suffix}.{ext}" new_path = f"{base}{sep}{db_name}{suffix}.{ext}"
data_idx += 1 data_idx += 1
elif lf.type == 'L': elif lf.type == 'L':
suffix = "" if log_idx == 0 else f"_{log_idx}" suffix = "" if log_idx == 0 else f"_{log_idx}"
new_path = f"{data_folder}\\{db_name}_log{suffix}.ldf" new_path = f"{base}{sep}{db_name}_log{suffix}.ldf"
log_idx += 1 log_idx += 1
else: else:
# No descartar otros tipos: moverlos preservando el nombre lógico. # No descartar otros tipos: moverlos preservando el nombre lógico.
safe = "".join(c if c.isalnum() else "_" for c in lf.logical_name) safe = "".join(c if c.isalnum() else "_" for c in lf.logical_name)
new_path = f"{data_folder}\\{db_name}_{safe}" new_path = f"{base}{sep}{db_name}_{safe}"
clauses.append(f"MOVE N'{lf.logical_name}' TO N'{new_path}'") clauses.append(f"MOVE N'{lf.logical_name}' TO N'{new_path}'")
return clauses return clauses

View File

@@ -1,5 +1,7 @@
"""Tab de configuración.""" """Tab de configuración."""
import sys
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QWidget, QVBoxLayout, QHBoxLayout, QGroupBox,
QFormLayout, QLineEdit, QPushButton, QSpinBox, QFormLayout, QLineEdit, QPushButton, QSpinBox,
@@ -102,7 +104,6 @@ class ConfigTab(QWidget):
self.sql_windows_auth_checkbox = QCheckBox() self.sql_windows_auth_checkbox = QCheckBox()
self.sql_windows_auth_checkbox.setChecked(True) self.sql_windows_auth_checkbox.setChecked(True)
self.sql_windows_auth_checkbox.toggled.connect(self._on_auth_changed)
sql_layout.addRow("Usar Windows Auth:", self.sql_windows_auth_checkbox) sql_layout.addRow("Usar Windows Auth:", self.sql_windows_auth_checkbox)
self.sql_username_input = QLineEdit() self.sql_username_input = QLineEdit()
@@ -114,6 +115,18 @@ class ConfigTab(QWidget):
self.sql_password_input.setEnabled(False) self.sql_password_input.setEnabled(False)
sql_layout.addRow("Contraseña SQL:", self.sql_password_input) sql_layout.addRow("Contraseña SQL:", self.sql_password_input)
# Conectar el handler DESPUÉS de crear usuario/contraseña: en Linux se
# desmarca el checkbox (abajo), lo que dispararía _on_auth_changed antes de
# que existan esos campos si se conectara arriba.
self.sql_windows_auth_checkbox.toggled.connect(self._on_auth_changed)
if sys.platform != "win32":
# Windows Auth no aplica en Linux: forzar SQL Auth y habilitar campos.
self.sql_windows_auth_checkbox.setChecked(False)
self.sql_windows_auth_checkbox.setEnabled(False)
self.sql_windows_auth_checkbox.setToolTip(
"Windows Auth no está disponible en Linux; use SQL Auth."
)
test_sql_btn = QPushButton("🔌 Probar Conexión") test_sql_btn = QPushButton("🔌 Probar Conexión")
test_sql_btn.clicked.connect(self._test_sql_connection) test_sql_btn.clicked.connect(self._test_sql_connection)
sql_layout.addRow("", test_sql_btn) sql_layout.addRow("", test_sql_btn)
@@ -423,12 +436,17 @@ class ConfigTab(QWidget):
line_edit.setText(folder) line_edit.setText(folder)
def _browse_seven_zip(self): def _browse_seven_zip(self):
"""Abre el diálogo para seleccionar 7z.exe.""" """Abre el diálogo para seleccionar el ejecutable de 7-Zip."""
if sys.platform == "win32":
title = "Seleccionar 7z.exe"
default_dir = "C:\\Program Files\\7-Zip"
file_filter = "Ejecutables (*.exe)"
else:
title = "Seleccionar 7z / 7zz"
default_dir = ""
file_filter = "Ejecutables (*)"
file_path, _ = QFileDialog.getOpenFileName( file_path, _ = QFileDialog.getOpenFileName(
self, self, title, default_dir, file_filter
"Seleccionar 7z.exe",
"C:\\Program Files\\7-Zip",
"Ejecutables (*.exe)"
) )
if file_path: if file_path:
self.seven_zip_input.setText(file_path) self.seven_zip_input.setText(file_path)
@@ -467,6 +485,9 @@ class ConfigTab(QWidget):
def _on_auth_changed(self, checked: bool): def _on_auth_changed(self, checked: bool):
"""Maneja el cambio en el tipo de autenticación.""" """Maneja el cambio en el tipo de autenticación."""
# Guard: la señal puede dispararse durante la construcción de la UI.
if not hasattr(self, "sql_username_input"):
return
self.sql_username_input.setEnabled(not checked) self.sql_username_input.setEnabled(not checked)
self.sql_password_input.setEnabled(not checked) self.sql_password_input.setEnabled(not checked)

View File

@@ -33,6 +33,14 @@ class DashboardTab(QWidget):
title.setFont(title_font) title.setFont(title_font)
layout.addWidget(title) layout.addWidget(title)
self.panel_warning_label = QLabel("")
self.panel_warning_label.setWordWrap(True)
self.panel_warning_label.setStyleSheet(
"background-color: #fff3cd; color: #856404; padding: 8px; border-radius: 4px;"
)
self.panel_warning_label.hide()
layout.addWidget(self.panel_warning_label)
# Grid de estadísticas # Grid de estadísticas
stats_group = QGroupBox("Estadísticas de Jobs") stats_group = QGroupBox("Estadísticas de Jobs")
stats_layout = QGridLayout() stats_layout = QGridLayout()
@@ -94,6 +102,18 @@ class DashboardTab(QWidget):
times_group.setLayout(times_layout) times_group.setLayout(times_layout)
layout.addWidget(times_group) layout.addWidget(times_group)
self.engine_status_badge = QLabel("Detenido")
badge_font = QFont()
badge_font.setPointSize(12)
badge_font.setBold(True)
self.engine_status_badge.setFont(badge_font)
self.engine_status_badge.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.engine_status_badge.setStyleSheet(
"background-color: #e9ecef; color: #495057; padding: 6px 14px; "
"border-radius: 4px;"
)
layout.addWidget(self.engine_status_badge)
# Estado del motor # Estado del motor
motor_group = QGroupBox("Estado del Motor") motor_group = QGroupBox("Estado del Motor")
motor_layout = QHBoxLayout() motor_layout = QHBoxLayout()
@@ -136,6 +156,7 @@ class DashboardTab(QWidget):
def _load_initial_stats(self): def _load_initial_stats(self):
"""Carga las estadísticas iniciales.""" """Carga las estadísticas iniciales."""
self.update_motor_status()
stats = self.engine.get_stats() stats = self.engine.get_stats()
self.update_stats(stats) self.update_stats(stats)
self._refresh_recent_jobs() self._refresh_recent_jobs()
@@ -193,3 +214,40 @@ class DashboardTab(QWidget):
self.recent_table.setItem(i, 4, status_item) self.recent_table.setItem(i, 4, status_item)
self.recent_table.resizeColumnsToContents() self.recent_table.resizeColumnsToContents()
def update_motor_status(self) -> None:
"""Actualiza badge y etiqueta según el estado actual del motor."""
if not self.engine.is_running():
text = "Detenido"
badge_style = (
"background-color: #e9ecef; color: #495057; padding: 6px 14px; "
"border-radius: 4px;"
)
label_style = "color: red;"
elif self.engine.is_paused():
text = "Pausado"
badge_style = (
"background-color: #fff3cd; color: #856404; padding: 6px 14px; "
"border-radius: 4px;"
)
label_style = "color: orange;"
else:
text = "Activo"
badge_style = (
"background-color: #d4edda; color: #155724; padding: 6px 14px; "
"border-radius: 4px;"
)
label_style = "color: green;"
self.engine_status_badge.setText(f"Motor: {text}")
self.engine_status_badge.setStyleSheet(badge_style)
self.motor_status_label.setText(text)
self.motor_status_label.setStyleSheet(label_style)
def set_panel_warning(self, message: str) -> None:
"""Muestra aviso cuando falta configuración del servicio de bases en config/.env."""
if message:
self.panel_warning_label.setText(message)
self.panel_warning_label.show()
else:
self.panel_warning_label.hide()

View File

@@ -1,6 +1,7 @@
"""Tab de logs y soporte.""" """Tab de logs y soporte."""
import subprocess import subprocess
import sys
from pathlib import Path from pathlib import Path
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
@@ -91,10 +92,15 @@ class LogsTab(QWidget):
self.logs_text.setPlainText("\n".join(log_lines)) self.logs_text.setPlainText("\n".join(log_lines))
def _open_logs_folder(self): def _open_logs_folder(self):
"""Abre la carpeta de logs en el explorador.""" """Abre la carpeta de logs en el explorador de archivos del sistema."""
try: try:
if LOGS_DIR.exists(): if LOGS_DIR.exists():
if sys.platform == "win32":
subprocess.run(["explorer", str(LOGS_DIR)]) subprocess.run(["explorer", str(LOGS_DIR)])
elif sys.platform == "darwin":
subprocess.run(["open", str(LOGS_DIR)])
else:
subprocess.run(["xdg-open", str(LOGS_DIR)])
else: else:
QMessageBox.warning( QMessageBox.warning(
self, self,

View File

@@ -2,16 +2,17 @@
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QMainWindow, QWidget, QVBoxLayout, QTabWidget, QMainWindow, QWidget, QVBoxLayout, QTabWidget,
QSystemTrayIcon, QMenu QSystemTrayIcon, QMenu, QMessageBox, QApplication,
) )
from PySide6.QtCore import Qt, QTimer, Signal from PySide6.QtCore import Qt, QTimer, Signal
from PySide6.QtGui import QIcon, QCloseEvent, QAction from PySide6.QtGui import QCloseEvent, QAction
from .dashboard_tab import DashboardTab from .dashboard_tab import DashboardTab
from .jobs_tab import JobsTab from .jobs_tab import JobsTab
from .nodes_tab import NodesTab from .nodes_tab import NodesTab
from .config_tab import ConfigTab from .config_tab import ConfigTab
from .logs_tab import LogsTab from .logs_tab import LogsTab
from .tray_assets import load_tray_icon
from ..engine.engine import RestoreEngine from ..engine.engine import RestoreEngine
from ..utils.logger import app_logger from ..utils.logger import app_logger
@@ -19,58 +20,68 @@ from ..utils.logger import app_logger
class MainWindow(QMainWindow): class MainWindow(QMainWindow):
"""Ventana principal de la aplicación.""" """Ventana principal de la aplicación."""
# Señal para minimizar a tray
minimize_to_tray = Signal() minimize_to_tray = Signal()
def __init__(self): def __init__(
self,
minimized: bool = False,
start_engine: bool = False,
panel_configured: bool = True,
):
"""Inicializa la ventana principal.""" """Inicializa la ventana principal."""
super().__init__() super().__init__()
# Motor de restauración self._start_on_load = start_engine
self._minimized_on_load = minimized
self._panel_configured = panel_configured
self._force_quit = False
self._tray_hint_shown = False
self.engine = RestoreEngine() self.engine = RestoreEngine()
# Configurar ventana
self.setWindowTitle("CloudRestoreAS - Restauración Automática SQL Server") self.setWindowTitle("CloudRestoreAS - Restauración Automática SQL Server")
self.setMinimumSize(1200, 800) self.setMinimumSize(1200, 800)
self.setWindowIcon(load_tray_icon())
# System tray self.tray_icon: QSystemTrayIcon | None = None
self.tray_icon: QSystemTrayIcon = None
self._setup_tray() self._setup_tray()
# UI
self._setup_ui() self._setup_ui()
# Conectar señales del motor
self._connect_engine_signals() self._connect_engine_signals()
# Timer para actualizar stats
self.stats_timer = QTimer() self.stats_timer = QTimer()
self.stats_timer.timeout.connect(self._update_stats) self.stats_timer.timeout.connect(self._update_stats)
self.stats_timer.start(5000) # Cada 5 segundos self.stats_timer.start(5000)
if not self._panel_configured:
self.dashboard_tab.set_panel_warning(
"Configure la conexión al servicio de bases en config/.env "
"(CLOUDRESTORE_PANEL_API_URL, TOKEN e INSTANCE_KEY)."
)
if self._start_on_load:
QTimer.singleShot(500, self._start_engine)
self._update_tray_status()
self.dashboard_tab.update_motor_status()
app_logger.info("Ventana principal inicializada") app_logger.info("Ventana principal inicializada")
def _setup_ui(self): def _setup_ui(self):
"""Configura la interfaz de usuario.""" """Configura la interfaz de usuario."""
# Widget central
central_widget = QWidget() central_widget = QWidget()
self.setCentralWidget(central_widget) self.setCentralWidget(central_widget)
# Layout principal
layout = QVBoxLayout(central_widget) layout = QVBoxLayout(central_widget)
layout.setContentsMargins(10, 10, 10, 10) layout.setContentsMargins(10, 10, 10, 10)
# Tabs
self.tabs = QTabWidget() self.tabs = QTabWidget()
# Crear tabs
self.dashboard_tab = DashboardTab(self.engine) self.dashboard_tab = DashboardTab(self.engine)
self.jobs_tab = JobsTab(self.engine) self.jobs_tab = JobsTab(self.engine)
self.nodes_tab = NodesTab(self.engine) self.nodes_tab = NodesTab(self.engine)
self.config_tab = ConfigTab(self.engine) self.config_tab = ConfigTab(self.engine)
self.logs_tab = LogsTab(self.engine) self.logs_tab = LogsTab(self.engine)
# Agregar tabs
self.tabs.addTab(self.dashboard_tab, "📊 Dashboard") self.tabs.addTab(self.dashboard_tab, "📊 Dashboard")
self.tabs.addTab(self.jobs_tab, "📋 Jobs") self.tabs.addTab(self.jobs_tab, "📋 Jobs")
self.tabs.addTab(self.nodes_tab, "🔗 Nodos") self.tabs.addTab(self.nodes_tab, "🔗 Nodos")
@@ -78,15 +89,18 @@ class MainWindow(QMainWindow):
self.tabs.addTab(self.logs_tab, "📝 Logs") self.tabs.addTab(self.logs_tab, "📝 Logs")
layout.addWidget(self.tabs) layout.addWidget(self.tabs)
# Menu bar
self._setup_menu() self._setup_menu()
def _setup_menu(self): def _setup_menu(self):
"""Configura el menú.""" """Configura el menú."""
menubar = self.menuBar() menubar = self.menuBar()
# Menú Motor file_menu = menubar.addMenu("Archivo")
quit_action = QAction("Salir", self)
quit_action.triggered.connect(self._quit_application)
file_menu.addAction(quit_action)
motor_menu = menubar.addMenu("Motor") motor_menu = menubar.addMenu("Motor")
self.start_action = QAction("▶️ Iniciar", self) self.start_action = QAction("▶️ Iniciar", self)
@@ -114,20 +128,28 @@ class MainWindow(QMainWindow):
self.scan_action.triggered.connect(self._scan_now) self.scan_action.triggered.connect(self._scan_now)
motor_menu.addAction(self.scan_action) motor_menu.addAction(self.scan_action)
# Menú Ayuda
help_menu = menubar.addMenu("Ayuda") help_menu = menubar.addMenu("Ayuda")
about_action = QAction("Acerca de", self) about_action = QAction("Acerca de", self)
about_action.triggered.connect(self._show_about) about_action.triggered.connect(self._show_about)
help_menu.addAction(about_action) help_menu.addAction(about_action)
def _setup_tray(self): def _setup_tray(self, _attempt: int = 0):
"""Configura el icono de bandeja del sistema.""" """Configura el icono de bandeja del sistema (reintenta si aún no está listo)."""
# Crear icono (usar un icono por defecto o crear uno simple) if not QSystemTrayIcon.isSystemTrayAvailable():
if _attempt < 20:
# La bandeja puede no estar lista justo tras el login: reintentar ~10s.
QTimer.singleShot(500, lambda: self._setup_tray(_attempt + 1))
else:
app_logger.warning(
"Bandeja del sistema no disponible tras varios reintentos"
)
return
self.tray_icon = QSystemTrayIcon(self) self.tray_icon = QSystemTrayIcon(self)
self.tray_icon.setIcon(load_tray_icon())
self.tray_icon.setToolTip("CloudRestoreAS") self.tray_icon.setToolTip("CloudRestoreAS")
# Menú del tray
tray_menu = QMenu() tray_menu = QMenu()
show_action = QAction("Mostrar", self) show_action = QAction("Mostrar", self)
@@ -150,16 +172,12 @@ class MainWindow(QMainWindow):
tray_menu.addSeparator() tray_menu.addSeparator()
quit_action = QAction("Salir", self) quit_tray_action = QAction("Salir", self)
quit_action.triggered.connect(self._quit_application) quit_tray_action.triggered.connect(self._quit_application)
tray_menu.addAction(quit_action) tray_menu.addAction(quit_tray_action)
self.tray_icon.setContextMenu(tray_menu) self.tray_icon.setContextMenu(tray_menu)
# Doble clic en tray para mostrar/ocultar
self.tray_icon.activated.connect(self._on_tray_activated) self.tray_icon.activated.connect(self._on_tray_activated)
# Mostrar tray
self.tray_icon.show() self.tray_icon.show()
def _connect_engine_signals(self): def _connect_engine_signals(self):
@@ -169,13 +187,27 @@ class MainWindow(QMainWindow):
self.engine.signals.config_loaded.connect(self._on_config_loaded) self.engine.signals.config_loaded.connect(self._on_config_loaded)
def closeEvent(self, event: QCloseEvent): def closeEvent(self, event: QCloseEvent):
"""Maneja el evento de cierre de ventana (minimizar a tray).""" """Nunca cierra la app: minimiza a la bandeja (o a la barra de tareas si no hay bandeja)."""
if self.tray_icon.isVisible(): if self._force_quit:
self.hide()
event.ignore()
app_logger.info("Ventana minimizada a bandeja del sistema")
else:
event.accept() event.accept()
return
event.ignore()
if self.tray_icon and self.tray_icon.isVisible():
self.hide()
app_logger.info("Ventana minimizada a bandeja del sistema")
if not self._tray_hint_shown:
self.tray_icon.showMessage(
"CloudRestoreAS sigue en ejecución",
"La aplicación está en la bandeja. Use clic derecho → Salir para cerrar.",
QSystemTrayIcon.MessageIcon.Information,
4000,
)
self._tray_hint_shown = True
else:
# Sin bandeja disponible: minimizar a la barra de tareas, nunca cerrar.
self.showMinimized()
app_logger.info("Ventana minimizada a la barra de tareas (sin bandeja)")
def _on_tray_activated(self, reason): def _on_tray_activated(self, reason):
"""Maneja la activación del icono de tray.""" """Maneja la activación del icono de tray."""
@@ -191,12 +223,29 @@ class MainWindow(QMainWindow):
self.activateWindow() self.activateWindow()
self.raise_() self.raise_()
def _motor_status_text(self) -> str:
if not self.engine.is_running():
return "Detenido"
if self.engine.is_paused():
return "Pausado"
return "Activo"
def _update_tray_status(self):
"""Actualiza tooltip del icono de bandeja según estado del motor."""
if not self.tray_icon:
return
status = self._motor_status_text()
self.tray_icon.setToolTip(f"CloudRestoreAS — Motor: {status}")
def _start_engine(self): def _start_engine(self):
"""Inicia el motor.""" """Inicia el motor."""
self.engine.start() self.engine.start()
self.start_action.setEnabled(False) self.start_action.setEnabled(False)
self.pause_action.setEnabled(True) self.pause_action.setEnabled(True)
self.stop_action.setEnabled(True) self.stop_action.setEnabled(True)
self.resume_action.setEnabled(False)
self._update_tray_status()
self.dashboard_tab.update_motor_status()
app_logger.info("Motor iniciado desde UI") app_logger.info("Motor iniciado desde UI")
def _pause_engine(self): def _pause_engine(self):
@@ -204,6 +253,8 @@ class MainWindow(QMainWindow):
self.engine.pause() self.engine.pause()
self.pause_action.setEnabled(False) self.pause_action.setEnabled(False)
self.resume_action.setEnabled(True) self.resume_action.setEnabled(True)
self._update_tray_status()
self.dashboard_tab.update_motor_status()
app_logger.info("Motor pausado desde UI") app_logger.info("Motor pausado desde UI")
def _resume_engine(self): def _resume_engine(self):
@@ -211,6 +262,8 @@ class MainWindow(QMainWindow):
self.engine.resume() self.engine.resume()
self.resume_action.setEnabled(False) self.resume_action.setEnabled(False)
self.pause_action.setEnabled(True) self.pause_action.setEnabled(True)
self._update_tray_status()
self.dashboard_tab.update_motor_status()
app_logger.info("Motor reanudado desde UI") app_logger.info("Motor reanudado desde UI")
def _stop_engine(self): def _stop_engine(self):
@@ -220,6 +273,8 @@ class MainWindow(QMainWindow):
self.pause_action.setEnabled(False) self.pause_action.setEnabled(False)
self.resume_action.setEnabled(False) self.resume_action.setEnabled(False)
self.start_action.setEnabled(True) self.start_action.setEnabled(True)
self._update_tray_status()
self.dashboard_tab.update_motor_status()
app_logger.info("Motor detenido desde UI") app_logger.info("Motor detenido desde UI")
def _scan_now(self): def _scan_now(self):
@@ -229,6 +284,8 @@ class MainWindow(QMainWindow):
def _update_stats(self): def _update_stats(self):
"""Actualiza las estadísticas.""" """Actualiza las estadísticas."""
self._update_tray_status()
self.dashboard_tab.update_motor_status()
if self.engine.is_running(): if self.engine.is_running():
stats = self.engine.get_stats() stats = self.engine.get_stats()
self.dashboard_tab.update_stats(stats) self.dashboard_tab.update_stats(stats)
@@ -248,20 +305,21 @@ class MainWindow(QMainWindow):
def _show_about(self): def _show_about(self):
"""Muestra el diálogo Acerca de.""" """Muestra el diálogo Acerca de."""
from PySide6.QtWidgets import QMessageBox
QMessageBox.about( QMessageBox.about(
self, self,
"Acerca de CloudRestoreAS", "Acerca de CloudRestoreAS",
"CloudRestoreAS v1.0.0\n\n" "CloudRestoreAS v1.0.0\n\n"
"Aplicación de restauración automática de bases de datos SQL Server.\n\n" "Aplicación de restauración automática de bases de datos SQL Server.\n\n"
"© 2026 Aduanasoft" "Al cerrar la ventana, la aplicación permanece en la bandeja del sistema.\n"
"Use Archivo → Salir o la bandeja → Salir para cerrar por completo.\n\n"
"© 2026 Aduanasoft",
) )
def _quit_application(self): def _quit_application(self):
"""Sale de la aplicación completamente.""" """Sale de la aplicación completamente."""
app_logger.info("Saliendo de la aplicación") app_logger.info("Saliendo de la aplicación")
self._force_quit = True
self.engine.stop() self.engine.stop()
if self.tray_icon:
self.tray_icon.hide() self.tray_icon.hide()
self.close()
from PySide6.QtWidgets import QApplication
QApplication.quit() QApplication.quit()

55
app/ui/tray_assets.py Normal file
View File

@@ -0,0 +1,55 @@
"""Icono de bandeja del sistema."""
import sys
from pathlib import Path
from PySide6.QtCore import Qt
from PySide6.QtGui import QColor, QIcon, QPainter, QPixmap
_ASSETS_DIR = Path(__file__).resolve().parent.parent.parent / "packaging" / "assets"
def _generated_tray_icon() -> QIcon:
"""Icono simple en memoria si no hay archivo empaquetado."""
size = 32
pixmap = QPixmap(size, size)
pixmap.fill(Qt.GlobalColor.transparent)
painter = QPainter(pixmap)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
painter.setBrush(QColor(0, 120, 215))
painter.setPen(Qt.PenStyle.NoPen)
painter.drawRoundedRect(2, 2, size - 4, size - 4, 6, 6)
painter.setPen(QColor(255, 255, 255))
font = painter.font()
font.setBold(True)
font.setPointSize(14)
painter.setFont(font)
painter.drawText(pixmap.rect(), Qt.AlignmentFlag.AlignCenter, "C")
painter.end()
return QIcon(pixmap)
def load_tray_icon() -> QIcon:
"""Carga icono de bandeja desde assets empaquetados o genera uno por defecto."""
candidates: list[Path] = []
meipass = getattr(sys, "_MEIPASS", None)
if meipass:
base = Path(meipass)
candidates.extend(
[
base / "packaging" / "assets" / "tray-icon.png",
base / "packaging" / "assets" / "tray-icon.ico",
]
)
candidates.extend(
[
_ASSETS_DIR / "tray-icon.png",
_ASSETS_DIR / "tray-icon.ico",
]
)
for path in candidates:
if path.is_file():
icon = QIcon(str(path))
if not icon.isNull():
return icon
return _generated_tray_icon()

View File

@@ -1,59 +1,68 @@
"""Utilidades de cifrado para passwords usando DPAPI en Windows.""" """Utilidades de cifrado para passwords (DPAPI en Windows, Fernet en Linux)."""
import base64 import base64
import win32crypt import sys
from pathlib import Path
from typing import Optional from typing import Optional
from ..constants import DATA_DIR
_IS_WINDOWS = sys.platform == "win32"
_SECRET_FILE = DATA_DIR / ".secret"
def _get_fernet():
from cryptography.fernet import Fernet
DATA_DIR.mkdir(parents=True, exist_ok=True)
if not _SECRET_FILE.exists():
key = Fernet.generate_key()
_SECRET_FILE.write_bytes(key)
try:
_SECRET_FILE.chmod(0o600)
except OSError:
pass
else:
key = _SECRET_FILE.read_bytes()
return Fernet(key)
def encrypt_password(password: str) -> str: def encrypt_password(password: str) -> str:
""" """Cifra una contraseña."""
Cifra una contraseña usando DPAPI de Windows.
Args:
password: Contraseña en texto plano
Returns:
Contraseña cifrada en base64
"""
if not password: if not password:
return "" return ""
try: try:
if _IS_WINDOWS:
import win32crypt
encrypted_bytes = win32crypt.CryptProtectData( encrypted_bytes = win32crypt.CryptProtectData(
password.encode('utf-8'), password.encode("utf-8"), None, None, None, None, 0
None,
None,
None,
None,
0
) )
return base64.b64encode(encrypted_bytes).decode('ascii') return base64.b64encode(encrypted_bytes).decode("ascii")
fernet = _get_fernet()
return fernet.encrypt(password.encode("utf-8")).decode("ascii")
except Exception as e: except Exception as e:
raise RuntimeError(f"Error cifrando contraseña: {e}") raise RuntimeError(f"Error cifrando contraseña: {e}") from e
def decrypt_password(encrypted_password: str) -> str: def decrypt_password(encrypted_password: str) -> str:
""" """Descifra una contraseña."""
Descifra una contraseña usando DPAPI de Windows.
Args:
encrypted_password: Contraseña cifrada en base64
Returns:
Contraseña en texto plano
"""
if not encrypted_password: if not encrypted_password:
return "" return ""
try: try:
if _IS_WINDOWS:
import win32crypt
encrypted_bytes = base64.b64decode(encrypted_password) encrypted_bytes = base64.b64decode(encrypted_password)
decrypted_bytes = win32crypt.CryptUnprotectData( decrypted_bytes = win32crypt.CryptUnprotectData(
encrypted_bytes, encrypted_bytes, None, None, None, 0
None,
None,
None,
0
)[1] )[1]
return decrypted_bytes.decode('utf-8') return decrypted_bytes.decode("utf-8")
fernet = _get_fernet()
return fernet.decrypt(encrypted_password.encode("ascii")).decode("utf-8")
except Exception as e: except Exception as e:
raise RuntimeError(f"Error descifrando contraseña: {e}") raise RuntimeError(f"Error descifrando contraseña: {e}") from e

131
build-all.sh Executable file
View File

@@ -0,0 +1,131 @@
#!/usr/bin/env bash
# =============================================================================
# CloudRestoreAS — build de TODO en una sola tarea (ejecutar desde WSL).
#
# Linux : build en Docker (ubuntu:22.04) -> dist/CloudRestoreAS (autocontenido)
# Windows: build vía powershell.exe -> build.ps1 -> dist/CloudRestoreAS.exe
# Paquete: dist/release/CloudRestoreAS-linux.tar.gz y CloudRestoreAS-win.zip
#
# Uso:
# ./build-all.sh # ambos + empaquetado
# ./build-all.sh --linux-only # solo Linux
# ./build-all.sh --windows-only # solo Windows
# ./build-all.sh --no-package # sin generar .tar.gz/.zip
# ./build-all.sh --clean # rebuild desde cero (borra venvs/bundled/dist)
# =============================================================================
set -uo pipefail
ROOT="$(cd "$(dirname "$0")" && pwd)"
cd "$ROOT"
DO_LINUX=1
DO_WINDOWS=1
DO_PACKAGE=1
DO_CLEAN=0
for arg in "$@"; do
case "$arg" in
--linux-only) DO_WINDOWS=0 ;;
--windows-only) DO_LINUX=0 ;;
--no-package) DO_PACKAGE=0 ;;
--clean) DO_CLEAN=1 ;;
-h|--help)
grep '^#' "$0" | grep -v '^#!' | sed 's/^# \{0,1\}//'; exit 0 ;;
*) echo "Opción desconocida: $arg" >&2; exit 2 ;;
esac
done
# --- helpers -----------------------------------------------------------------
BLUE='\033[36m'; GREEN='\033[32m'; YELLOW='\033[33m'; RED='\033[31m'; NC='\033[0m'
say() { echo -e "${BLUE}==>${NC} $*"; }
ok() { echo -e "${GREEN}OK:${NC} $*"; }
warn() { echo -e "${YELLOW}AVISO:${NC} $*" >&2; }
err() { echo -e "${RED}ERROR:${NC} $*" >&2; }
# SECONDS es incremental de bash; lo usamos para medir cada etapa.
stage_start() { STAGE_T0=$SECONDS; }
stage_end() { echo -e " (${1} en $((SECONDS - STAGE_T0))s)"; }
WIN_OK=0
LIN_OK=0
# --- limpieza opcional -------------------------------------------------------
if [[ "$DO_CLEAN" -eq 1 ]]; then
say "Limpieza (--clean): borrando venvs, bundled y dist"
# bundled/ y venv-linux los crea Docker como root; borrar vía contenedor.
if command -v docker >/dev/null 2>&1; then
docker run --rm -v "$ROOT:/app" ubuntu:22.04 bash -c \
'rm -rf /app/packaging/bundled/linux /app/venv-linux /app/dist/CloudRestoreAS /app/dist/release' \
>/dev/null 2>&1 || true
fi
rm -rf "$ROOT/venv-windows" "$ROOT/packaging/bundled/windows" \
"$ROOT/dist/CloudRestoreAS.exe" 2>/dev/null || true
ok "limpieza completada"
fi
# --- Build Linux (Docker) ----------------------------------------------------
if [[ "$DO_LINUX" -eq 1 ]]; then
say "Build Linux (Docker, autocontenido)"
stage_start
if ! command -v docker >/dev/null 2>&1; then
err "docker no disponible; no se puede construir Linux"
[[ "$DO_WINDOWS" -eq 0 ]] && exit 1
elif bash "$ROOT/packaging/scripts/docker-build-linux.sh"; then
if [[ -f "$ROOT/dist/CloudRestoreAS" ]]; then
LIN_OK=1; ok "dist/CloudRestoreAS"
else
err "el build Linux terminó sin binario"
fi
else
err "falló el build Linux"
fi
stage_end "Linux"
fi
# --- Build Windows (PowerShell -> build.ps1) ---------------------------------
if [[ "$DO_WINDOWS" -eq 1 ]]; then
say "Build Windows (PowerShell + build.ps1)"
stage_start
WIN_PATH="$(wslpath -w "$ROOT" 2>/dev/null || true)"
if command -v powershell.exe >/dev/null 2>&1 && [[ -n "$WIN_PATH" ]]; then
# build.ps1 auto-detecta el Python de Windows (Python313/312/311) y arma venv-windows.
powershell.exe -NoProfile -ExecutionPolicy Bypass -Command \
"Set-Location -LiteralPath '$WIN_PATH'; & .\\build.ps1"
PS_RC=$?
if [[ "$PS_RC" -eq 0 && -f "$ROOT/dist/CloudRestoreAS.exe" ]]; then
WIN_OK=1; ok "dist/CloudRestoreAS.exe"
else
err "falló el build Windows (código $PS_RC)"
[[ "$DO_LINUX" -eq 0 ]] && { stage_end "Windows"; exit 1; }
fi
else
warn "powershell.exe no disponible desde WSL; se omite el build Windows."
warn "Ejecuta build.ps1 en Windows para generar el .exe."
[[ "$DO_LINUX" -eq 0 ]] && exit 1
fi
stage_end "Windows"
fi
# --- Empaquetado -------------------------------------------------------------
if [[ "$DO_PACKAGE" -eq 1 ]]; then
say "Empaquetado (.tar.gz Linux / .zip Windows)"
stage_start
bash "$ROOT/packaging/scripts/package-release.sh" || warn "empaquetado con incidencias"
stage_end "Empaquetado"
fi
# --- Resumen -----------------------------------------------------------------
echo
say "Resumen"
[[ "$DO_LINUX" -eq 1 ]] && { [[ "$LIN_OK" -eq 1 ]] && ok "Linux regenerado" || err "Linux NO generado"; }
[[ "$DO_WINDOWS" -eq 1 ]] && { [[ "$WIN_OK" -eq 1 ]] && ok "Windows regenerado" || warn "Windows NO generado"; }
if [[ -d "$ROOT/dist/release" ]]; then
echo "Artefactos en dist/release/:"
ls -lh "$ROOT/dist/release" | awk 'NR>1 {print " " $9 " " $5}'
fi
# Éxito si todo lo solicitado se generó.
FAIL=0
[[ "$DO_LINUX" -eq 1 && "$LIN_OK" -ne 1 ]] && FAIL=1
[[ "$DO_WINDOWS" -eq 1 && "$WIN_OK" -ne 1 ]] && FAIL=1
exit $FAIL

500
build-linux.log Normal file
View File

@@ -0,0 +1,500 @@
debconf: delaying package configuration, since apt-utils is not installed
===============================================
CloudRestoreAS - Build Linux (onefile)
===============================================
==> 7-Zip Linux (7zz)
7zz ya existe
==> ODBC Driver 18 + unixODBC
ODBC ya existe
Listo: /app/packaging/bundled/linux
Ejecutando PyInstaller (onefile)...
46 INFO: PyInstaller: 6.20.0, contrib hooks: 2026.6
46 INFO: Python: 3.10.12
47 INFO: Platform: Linux-6.6.87.2-microsoft-standard-WSL2-x86_64-with-glibc2.35
47 INFO: Python environment: /app/venv-linux
49 INFO: Removing temporary files and cleaning cache in /root/.cache/pyinstaller
77 WARNING: Failed to collect submodules for 'PySide6.scripts.deploy_lib' because importing 'PySide6.scripts.deploy_lib' raised: ModuleNotFoundError: No module named 'project_lib'
604 INFO: Module search paths (PYTHONPATH):
['/app',
'/usr/lib/python310.zip',
'/usr/lib/python3.10',
'/usr/lib/python3.10/lib-dynload',
'/app/venv-linux/lib/python3.10/site-packages',
'/app']
684 INFO: Appending 'binaries' from .spec
688 INFO: Appending 'datas' from .spec
745 INFO: checking Analysis
745 INFO: Building Analysis because Analysis-00.toc is non existent
745 INFO: Looking for Python shared library...
753 INFO: Using Python shared library: /lib/x86_64-linux-gnu/libpython3.10.so.1.0
753 INFO: Running Analysis Analysis-00.toc
753 INFO: Target bytecode optimization level: 0
753 INFO: Initializing module dependency graph...
753 INFO: Initializing module graph hook caches...
759 INFO: Analyzing modules for base_library.zip ...
989 INFO: Processing standard module hook 'hook-heapq.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
1019 INFO: Processing standard module hook 'hook-encodings.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
1674 INFO: Processing standard module hook 'hook-pickle.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
2243 INFO: Caching module dependency graph...
2266 INFO: Analyzing /app/runner.py
2280 INFO: Processing standard module hook 'hook-PySide6.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
2378 INFO: Processing standard module hook 'hook-shiboken6.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
2455 INFO: Processing standard module hook 'hook-PySide6.QtNetwork.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
2973 INFO: Processing standard module hook 'hook-PySide6.QtCore.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
3365 INFO: Processing standard module hook 'hook-PySide6.QtWidgets.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
3866 INFO: Processing standard module hook 'hook-PySide6.QtGui.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
4329 INFO: Processing standard module hook 'hook-sqlite3.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
4522 INFO: Processing standard module hook 'hook-platform.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
4540 INFO: Processing standard module hook 'hook-cryptography.py' from '/app/venv-linux/lib/python3.10/site-packages/_pyinstaller_hooks_contrib/stdhooks'
4807 INFO: hook-cryptography: cryptography does not seem to be using dynamically linked OpenSSL.
4855 INFO: Processing standard module hook 'hook-pyodbc.py' from '/app/venv-linux/lib/python3.10/site-packages/_pyinstaller_hooks_contrib/stdhooks'
4893 INFO: Processing standard module hook 'hook-urllib3.py' from '/app/venv-linux/lib/python3.10/site-packages/_pyinstaller_hooks_contrib/stdhooks'
4961 INFO: Processing pre-safe-import-module hook 'hook-backports.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks/pre_safe_import_module'
4962 INFO: SetuptoolsInfo: initializing cached setuptools info...
5130 INFO: Processing pre-safe-import-module hook 'hook-typing_extensions.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks/pre_safe_import_module'
5213 INFO: Processing standard module hook 'hook-multiprocessing.util.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
5241 INFO: Processing standard module hook 'hook-xml.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
5365 INFO: Processing standard module hook 'hook-_ctypes.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
5819 INFO: Processing standard module hook 'hook-certifi.py' from '/app/venv-linux/lib/python3.10/site-packages/_pyinstaller_hooks_contrib/stdhooks'
5839 INFO: Processing standard module hook 'hook-charset_normalizer.py' from '/app/venv-linux/lib/python3.10/site-packages/_pyinstaller_hooks_contrib/stdhooks'
6028 INFO: Processing standard module hook 'hook-bcrypt.py' from '/app/venv-linux/lib/python3.10/site-packages/_pyinstaller_hooks_contrib/stdhooks'
6348 INFO: Processing standard module hook 'hook-difflib.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
6488 INFO: Processing standard module hook 'hook-nacl.py' from '/app/venv-linux/lib/python3.10/site-packages/_pyinstaller_hooks_contrib/stdhooks'
6645 INFO: Analyzing hidden import 'PySide6.Qt3DAnimation'
6658 INFO: Processing standard module hook 'hook-PySide6.Qt3DAnimation.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
6777 INFO: Processing standard module hook 'hook-PySide6.Qt3DCore.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
6965 INFO: Processing standard module hook 'hook-PySide6.Qt3DRender.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
8337 INFO: Processing standard module hook 'hook-PySide6.QtOpenGL.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
8430 INFO: Analyzing hidden import 'PySide6.Qt3DExtras'
8463 INFO: Processing standard module hook 'hook-PySide6.Qt3DExtras.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
8542 INFO: Analyzing hidden import 'PySide6.Qt3DInput'
8552 INFO: Processing standard module hook 'hook-PySide6.Qt3DInput.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
8628 INFO: Analyzing hidden import 'PySide6.Qt3DLogic'
8629 INFO: Processing standard module hook 'hook-PySide6.Qt3DLogic.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
8708 INFO: Analyzing hidden import 'PySide6.QtAsyncio'
8724 INFO: Analyzing hidden import 'PySide6.QtBluetooth'
8756 INFO: Processing standard module hook 'hook-PySide6.QtBluetooth.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
8838 INFO: Analyzing hidden import 'PySide6.QtCanvasPainter'
8909 INFO: Processing standard module hook 'hook-PySide6.QtQuick.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
9019 INFO: Processing standard module hook 'hook-PySide6.QtQml.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
--- Logging error ---
Traceback (most recent call last):
File "/usr/lib/python3.10/logging/__init__.py", line 1100, in emit
msg = self.format(record)
File "/usr/lib/python3.10/logging/__init__.py", line 943, in format
return fmt.format(record)
File "/usr/lib/python3.10/logging/__init__.py", line 678, in format
record.message = record.getMessage()
File "/usr/lib/python3.10/logging/__init__.py", line 368, in getMessage
msg = msg % self.args
TypeError: not enough arguments for format string
Call stack:
File "/usr/lib/python3.10/runpy.py", line 196, in _run_module_as_main
return _run_code(code, main_globals, None,
File "/usr/lib/python3.10/runpy.py", line 86, in _run_code
exec(code, run_globals)
File "/app/venv-linux/lib/python3.10/site-packages/PyInstaller/__main__.py", line 321, in <module>
run()
File "/app/venv-linux/lib/python3.10/site-packages/PyInstaller/__main__.py", line 215, in run
run_build(pyi_config, spec_file, **vars(args))
File "/app/venv-linux/lib/python3.10/site-packages/PyInstaller/__main__.py", line 70, in run_build
PyInstaller.building.build_main.main(pyi_config, spec_file, **kwargs)
File "/app/venv-linux/lib/python3.10/site-packages/PyInstaller/building/build_main.py", line 1275, in main
build(specfile, distpath, workpath, clean_build)
File "/app/venv-linux/lib/python3.10/site-packages/PyInstaller/building/build_main.py", line 1213, in build
exec(code, spec_namespace)
File "packaging/CloudRestoreAS.spec", line 59, in <module>
a = Analysis(
File "/app/venv-linux/lib/python3.10/site-packages/PyInstaller/building/build_main.py", line 584, in __init__
self.__postinit__()
File "/app/venv-linux/lib/python3.10/site-packages/PyInstaller/building/datastruct.py", line 184, in __postinit__
self.assemble()
File "/app/venv-linux/lib/python3.10/site-packages/PyInstaller/building/build_main.py", line 719, in assemble
self.graph.add_hiddenimports(self.hiddenimports)
File "/app/venv-linux/lib/python3.10/site-packages/PyInstaller/depend/analysis.py", line 770, in add_hiddenimports
nodes = self.import_hook(modnm)
File "/app/venv-linux/lib/python3.10/site-packages/PyInstaller/lib/modulegraph/modulegraph.py", line 1279, in import_hook
submodule = self._safe_import_module(head, mname, submodule)
File "/app/venv-linux/lib/python3.10/site-packages/PyInstaller/depend/analysis.py", line 539, in _safe_import_module
return super()._safe_import_module(module_basename, module_name, parent_package)
File "/app/venv-linux/lib/python3.10/site-packages/PyInstaller/lib/modulegraph/modulegraph.py", line 1826, in _safe_import_module
self._process_imports(n)
File "/app/venv-linux/lib/python3.10/site-packages/PyInstaller/lib/modulegraph/modulegraph.py", line 2654, in _process_imports
target_modules = self._safe_import_hook(*import_info, **kwargs)
File "/app/venv-linux/lib/python3.10/site-packages/PyInstaller/depend/analysis.py", line 477, in _safe_import_hook
ret_modules = super()._safe_import_hook(
File "/app/venv-linux/lib/python3.10/site-packages/PyInstaller/lib/modulegraph/modulegraph.py", line 2103, in _safe_import_hook
target_modules = self.import_hook(
File "/app/venv-linux/lib/python3.10/site-packages/PyInstaller/lib/modulegraph/modulegraph.py", line 1279, in import_hook
submodule = self._safe_import_module(head, mname, submodule)
File "/app/venv-linux/lib/python3.10/site-packages/PyInstaller/depend/analysis.py", line 539, in _safe_import_module
return super()._safe_import_module(module_basename, module_name, parent_package)
File "/app/venv-linux/lib/python3.10/site-packages/PyInstaller/lib/modulegraph/modulegraph.py", line 1826, in _safe_import_module
self._process_imports(n)
File "/app/venv-linux/lib/python3.10/site-packages/PyInstaller/lib/modulegraph/modulegraph.py", line 2654, in _process_imports
target_modules = self._safe_import_hook(*import_info, **kwargs)
File "/app/venv-linux/lib/python3.10/site-packages/PyInstaller/depend/analysis.py", line 477, in _safe_import_hook
ret_modules = super()._safe_import_hook(
File "/app/venv-linux/lib/python3.10/site-packages/PyInstaller/lib/modulegraph/modulegraph.py", line 2103, in _safe_import_hook
target_modules = self.import_hook(
File "/app/venv-linux/lib/python3.10/site-packages/PyInstaller/lib/modulegraph/modulegraph.py", line 1279, in import_hook
submodule = self._safe_import_module(head, mname, submodule)
File "/app/venv-linux/lib/python3.10/site-packages/PyInstaller/depend/analysis.py", line 539, in _safe_import_module
return super()._safe_import_module(module_basename, module_name, parent_package)
File "/app/venv-linux/lib/python3.10/site-packages/PyInstaller/lib/modulegraph/modulegraph.py", line 1826, in _safe_import_module
self._process_imports(n)
File "/app/venv-linux/lib/python3.10/site-packages/PyInstaller/lib/modulegraph/modulegraph.py", line 2654, in _process_imports
target_modules = self._safe_import_hook(*import_info, **kwargs)
File "/app/venv-linux/lib/python3.10/site-packages/PyInstaller/depend/analysis.py", line 385, in _safe_import_hook
excluded_imports = self._find_all_excluded_imports(source_module.identifier)
File "/app/venv-linux/lib/python3.10/site-packages/PyInstaller/depend/analysis.py", line 373, in _find_all_excluded_imports
excluded_imports.update(module_hook.excludedimports)
File "/app/venv-linux/lib/python3.10/site-packages/PyInstaller/depend/imphook.py", line 343, in __getattr__
self._load_hook_module()
File "/app/venv-linux/lib/python3.10/site-packages/PyInstaller/depend/imphook.py", line 422, in _load_hook_module
self._hook_module = importlib_load_source(self.hook_module_name, self.hook_filename)
File "/app/venv-linux/lib/python3.10/site-packages/PyInstaller/compat.py", line 566, in importlib_load_source
mod_loader.exec_module(mod)
File "<frozen importlib._bootstrap_external>", line 883, in exec_module
File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed
File "/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks/hook-PySide6.QtQml.py", line 15, in <module>
qml_binaries, qml_datas = pyside6_library_info.collect_qtqml_files()
File "/app/venv-linux/lib/python3.10/site-packages/PyInstaller/utils/hooks/qt/__init__.py", line 963, in collect_qtqml_files
plugin_binaries, plugin_datas = self._process_qml_plugin(qmldir_file)
File "/app/venv-linux/lib/python3.10/site-packages/PyInstaller/utils/hooks/qt/__init__.py", line 1014, in _process_qml_plugin
logger.warn("%s: QML plugin binary %r does not exist!", str(plugin_file))
Message: '%s: QML plugin binary %r does not exist!'
Arguments: ('/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/qml/Qt/labs/assetdownloader/libqmlassetdownloaderprivateplugin.so',)
10247 INFO: Analyzing hidden import 'PySide6.QtCharts'
10302 INFO: Processing standard module hook 'hook-PySide6.QtCharts.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
10383 INFO: Analyzing hidden import 'PySide6.QtConcurrent'
10385 INFO: Processing standard module hook 'hook-PySide6.QtConcurrent.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
10457 INFO: Analyzing hidden import 'PySide6.QtDBus'
10474 INFO: Processing standard module hook 'hook-PySide6.QtDBus.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
10549 INFO: Analyzing hidden import 'PySide6.QtDataVisualization'
10610 INFO: Processing standard module hook 'hook-PySide6.QtDataVisualization.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
10687 INFO: Analyzing hidden import 'PySide6.QtDesigner'
10701 INFO: Processing standard module hook 'hook-PySide6.QtDesigner.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
10809 INFO: Analyzing hidden import 'PySide6.QtGraphs'
10882 INFO: Processing standard module hook 'hook-PySide6.QtGraphs.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
10963 INFO: Analyzing hidden import 'PySide6.QtGraphsWidgets'
10985 INFO: Processing standard module hook 'hook-PySide6.QtGraphsWidgets.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
11060 INFO: Processing standard module hook 'hook-PySide6.QtQuickWidgets.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
11138 INFO: Analyzing hidden import 'PySide6.QtHelp'
11145 INFO: Processing standard module hook 'hook-PySide6.QtHelp.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
11233 INFO: Analyzing hidden import 'PySide6.QtHttpServer'
11239 INFO: Processing standard module hook 'hook-PySide6.QtHttpServer.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
11318 INFO: Analyzing hidden import 'PySide6.QtLocation'
11345 INFO: Processing standard module hook 'hook-PySide6.QtLocation.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
11461 INFO: Processing standard module hook 'hook-PySide6.QtPositioning.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
11557 INFO: Analyzing hidden import 'PySide6.QtMultimedia'
11595 INFO: Processing standard module hook 'hook-PySide6.QtMultimedia.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
11688 INFO: Analyzing hidden import 'PySide6.QtMultimediaWidgets'
11690 INFO: Processing standard module hook 'hook-PySide6.QtMultimediaWidgets.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
11769 INFO: Analyzing hidden import 'PySide6.QtNetworkAuth'
11781 INFO: Processing standard module hook 'hook-PySide6.QtNetworkAuth.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
11861 INFO: Analyzing hidden import 'PySide6.QtNfc'
11870 INFO: Processing standard module hook 'hook-PySide6.QtNfc.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
11945 INFO: Analyzing hidden import 'PySide6.QtOpenGLWidgets'
11947 INFO: Processing standard module hook 'hook-PySide6.QtOpenGLWidgets.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
12070 INFO: Analyzing hidden import 'PySide6.QtPdf'
12103 INFO: Processing standard module hook 'hook-PySide6.QtPdf.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
12211 INFO: Analyzing hidden import 'PySide6.QtPdfWidgets'
12214 INFO: Processing standard module hook 'hook-PySide6.QtPdfWidgets.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
12294 INFO: Analyzing hidden import 'PySide6.QtPrintSupport'
12302 INFO: Processing standard module hook 'hook-PySide6.QtPrintSupport.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
12389 INFO: Analyzing hidden import 'PySide6.QtQuick3D'
12395 INFO: Processing standard module hook 'hook-PySide6.QtQuick3D.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
12474 INFO: Analyzing hidden import 'PySide6.QtQuickControls2'
12475 INFO: Processing standard module hook 'hook-PySide6.QtQuickControls2.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
12555 INFO: Analyzing hidden import 'PySide6.QtQuickTest'
12556 INFO: Analyzing hidden import 'PySide6.QtRemoteObjects'
12566 INFO: Processing standard module hook 'hook-PySide6.QtRemoteObjects.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
12642 INFO: Analyzing hidden import 'PySide6.QtScxml'
12650 INFO: Processing standard module hook 'hook-PySide6.QtScxml.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
12739 INFO: Analyzing hidden import 'PySide6.QtSensors'
12753 INFO: Processing standard module hook 'hook-PySide6.QtSensors.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
12847 INFO: Analyzing hidden import 'PySide6.QtSerialBus'
12864 INFO: Processing standard module hook 'hook-PySide6.QtSerialBus.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
12975 INFO: Analyzing hidden import 'PySide6.QtSerialPort'
12979 INFO: Processing standard module hook 'hook-PySide6.QtSerialPort.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
13071 INFO: Analyzing hidden import 'PySide6.QtSpatialAudio'
13081 INFO: Processing standard module hook 'hook-PySide6.QtSpatialAudio.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
13194 INFO: Analyzing hidden import 'PySide6.QtSql'
13212 INFO: Processing standard module hook 'hook-PySide6.QtSql.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
13330 INFO: Analyzing hidden import 'PySide6.QtStateMachine'
13338 INFO: Processing standard module hook 'hook-PySide6.QtStateMachine.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
13425 INFO: Analyzing hidden import 'PySide6.QtSvg'
13429 INFO: Processing standard module hook 'hook-PySide6.QtSvg.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
13515 INFO: Analyzing hidden import 'PySide6.QtSvgWidgets'
13517 INFO: Processing standard module hook 'hook-PySide6.QtSvgWidgets.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
13604 INFO: Analyzing hidden import 'PySide6.QtTest'
13615 INFO: Processing standard module hook 'hook-PySide6.QtTest.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
13693 INFO: Analyzing hidden import 'PySide6.QtTextToSpeech'
13698 INFO: Processing standard module hook 'hook-PySide6.QtTextToSpeech.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
13795 INFO: Analyzing hidden import 'PySide6.QtUiTools'
13796 INFO: Processing standard module hook 'hook-PySide6.QtUiTools.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
13886 INFO: Analyzing hidden import 'PySide6.QtWebChannel'
13888 INFO: Processing standard module hook 'hook-PySide6.QtWebChannel.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
13970 INFO: Analyzing hidden import 'PySide6.QtWebEngineCore'
14022 INFO: Processing standard module hook 'hook-PySide6.QtWebEngineCore.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
14173 INFO: Analyzing hidden import 'PySide6.QtWebEngineQuick'
14176 INFO: Processing standard module hook 'hook-PySide6.QtWebEngineQuick.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
14316 INFO: Analyzing hidden import 'PySide6.QtWebEngineWidgets'
14320 INFO: Processing standard module hook 'hook-PySide6.QtWebEngineWidgets.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
14411 INFO: Analyzing hidden import 'PySide6.QtWebSockets'
14416 INFO: Processing standard module hook 'hook-PySide6.QtWebSockets.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
14492 INFO: Analyzing hidden import 'PySide6.QtWebView'
14495 INFO: Analyzing hidden import 'PySide6.QtXml'
14505 INFO: Processing standard module hook 'hook-PySide6.QtXml.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
14580 INFO: Analyzing hidden import 'PySide6._config'
14581 INFO: Analyzing hidden import 'PySide6._git_pyside_version'
14581 INFO: Analyzing hidden import 'PySide6.scripts'
14582 INFO: Analyzing hidden import 'PySide6.scripts.android_deploy'
14584 INFO: Analyzing hidden import 'PySide6.scripts.deploy'
14587 INFO: Analyzing hidden import 'PySide6.scripts.metaobjectdump'
14593 INFO: Analyzing hidden import 'PySide6.scripts.project'
14599 INFO: Analyzing hidden import 'PySide6.scripts.project_lib'
14629 INFO: Processing standard module hook 'hook-xml.etree.cElementTree.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
14633 INFO: Processing pre-safe-import-module hook 'hook-tomli.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks/pre_safe_import_module'
14654 INFO: Analyzing hidden import 'PySide6.scripts.pyside_tool'
14666 INFO: Processing standard module hook 'hook-sysconfig.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
14670 INFO: Analyzing hidden import 'PySide6.scripts.qml'
14673 INFO: Analyzing hidden import 'PySide6.scripts.qtpy2cpp'
14675 INFO: Analyzing hidden import 'PySide6.support'
14675 INFO: Analyzing hidden import 'PySide6.support.deprecated'
14675 INFO: Analyzing hidden import 'PySide6.support.generate_pyi'
14677 INFO: Processing module hooks (post-graph stage)...
14758 INFO: Performing binary vs. data reclassification (3662 entries)
15617 INFO: Looking for ctypes DLLs
15620 INFO: Analyzing run-time hooks ...
15623 INFO: Including run-time hook 'pyi_rth_inspect.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks/rthooks'
15624 INFO: Including run-time hook 'pyi_rth_pkgutil.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks/rthooks'
15625 INFO: Including run-time hook 'pyi_rth_multiprocessing.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks/rthooks'
15625 INFO: Including run-time hook 'pyi_rth_cryptography_openssl.py' from '/app/venv-linux/lib/python3.10/site-packages/_pyinstaller_hooks_contrib/rthooks'
15626 INFO: Including run-time hook 'pyi_rth_pyside6.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks/rthooks'
15627 INFO: Processing pre-find-module-path hook 'hook-_pyi_rth_utils.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks/pre_find_module_path'
15628 INFO: Processing standard module hook 'hook-_pyi_rth_utils.py' from '/app/venv-linux/lib/python3.10/site-packages/PyInstaller/hooks'
15694 INFO: Creating base_library.zip...
15704 INFO: Looking for dynamic libraries
20131 WARNING: Library not found: could not resolve 'libpulse.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6FFmpegStub-crypto.so.3'.
20131 WARNING: Library not found: could not resolve 'libpulse.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6FFmpegStub-ssl.so.3'.
20131 WARNING: Library not found: could not resolve 'libpulse.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6FFmpegStub-va-drm.so.2'.
20131 WARNING: Library not found: could not resolve 'libpulse.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6FFmpegStub-va-x11.so.2'.
20131 WARNING: Library not found: could not resolve 'libpulse.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6FFmpegStub-va.so.2'.
20131 WARNING: Library not found: could not resolve 'libpulse.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6Multimedia.so.6'.
20131 WARNING: Library not found: could not resolve 'libpulse.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6MultimediaQuick.so.6'.
20131 WARNING: Library not found: could not resolve 'libpulse.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6MultimediaWidgets.so.6'.
20131 WARNING: Library not found: could not resolve 'libpcsclite.so.1', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6Nfc.so.6'.
20131 WARNING: Library not found: could not resolve 'libpulse.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6Quick3DSpatialAudio.so.6'.
20131 WARNING: Library not found: could not resolve 'libpulse.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6SpatialAudio.so.6'.
20131 WARNING: Library not found: could not resolve 'libpulse.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6TextToSpeech.so.6'.
20131 WARNING: Library not found: could not resolve 'libXcomposite.so.1', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6WebEngineCore.so.6'.
20131 WARNING: Library not found: could not resolve 'libsmime3.so', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6WebEngineCore.so.6'.
20131 WARNING: Library not found: could not resolve 'libnssutil3.so', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6WebEngineCore.so.6'.
20131 WARNING: Library not found: could not resolve 'libnspr4.so', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6WebEngineCore.so.6'.
20131 WARNING: Library not found: could not resolve 'libXdamage.so.1', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6WebEngineCore.so.6'.
20131 WARNING: Library not found: could not resolve 'libasound.so.2', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6WebEngineCore.so.6'.
20131 WARNING: Library not found: could not resolve 'libnss3.so', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6WebEngineCore.so.6'.
20132 WARNING: Library not found: could not resolve 'libXrandr.so.2', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6WebEngineCore.so.6'.
20132 WARNING: Library not found: could not resolve 'libXtst.so.6', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6WebEngineCore.so.6'.
20132 WARNING: Library not found: could not resolve 'libxkbfile.so.1', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6WebEngineCore.so.6'.
20132 WARNING: Library not found: could not resolve 'libXcomposite.so.1', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6WebEngineQuick.so.6'.
20132 WARNING: Library not found: could not resolve 'libsmime3.so', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6WebEngineQuick.so.6'.
20132 WARNING: Library not found: could not resolve 'libnssutil3.so', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6WebEngineQuick.so.6'.
20132 WARNING: Library not found: could not resolve 'libnspr4.so', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6WebEngineQuick.so.6'.
20132 WARNING: Library not found: could not resolve 'libXdamage.so.1', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6WebEngineQuick.so.6'.
20132 WARNING: Library not found: could not resolve 'libasound.so.2', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6WebEngineQuick.so.6'.
20132 WARNING: Library not found: could not resolve 'libnss3.so', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6WebEngineQuick.so.6'.
20132 WARNING: Library not found: could not resolve 'libXrandr.so.2', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6WebEngineQuick.so.6'.
20132 WARNING: Library not found: could not resolve 'libXtst.so.6', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6WebEngineQuick.so.6'.
20132 WARNING: Library not found: could not resolve 'libxkbfile.so.1', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6WebEngineQuick.so.6'.
20132 WARNING: Library not found: could not resolve 'libXcomposite.so.1', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6WebEngineWidgets.so.6'.
20132 WARNING: Library not found: could not resolve 'libsmime3.so', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6WebEngineWidgets.so.6'.
20132 WARNING: Library not found: could not resolve 'libnssutil3.so', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6WebEngineWidgets.so.6'.
20132 WARNING: Library not found: could not resolve 'libnspr4.so', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6WebEngineWidgets.so.6'.
20132 WARNING: Library not found: could not resolve 'libXdamage.so.1', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6WebEngineWidgets.so.6'.
20132 WARNING: Library not found: could not resolve 'libasound.so.2', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6WebEngineWidgets.so.6'.
20132 WARNING: Library not found: could not resolve 'libnss3.so', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6WebEngineWidgets.so.6'.
20132 WARNING: Library not found: could not resolve 'libXrandr.so.2', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6WebEngineWidgets.so.6'.
20132 WARNING: Library not found: could not resolve 'libXtst.so.6', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6WebEngineWidgets.so.6'.
20132 WARNING: Library not found: could not resolve 'libxkbfile.so.1', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6WebEngineWidgets.so.6'.
20132 WARNING: Library not found: could not resolve 'libxcb-icccm.so.4', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6XcbQpa.so.6'.
20132 WARNING: Library not found: could not resolve 'libxcb-keysyms.so.1', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6XcbQpa.so.6'.
20132 WARNING: Library not found: could not resolve 'libxcb-util.so.1', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6XcbQpa.so.6'.
20132 WARNING: Library not found: could not resolve 'libxcb-image.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6XcbQpa.so.6'.
20132 WARNING: Library not found: could not resolve 'libxcb-render.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6XcbQpa.so.6'.
20132 WARNING: Library not found: could not resolve 'libxcb-cursor.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6XcbQpa.so.6'.
20132 WARNING: Library not found: could not resolve 'libxcb-render-util.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6XcbQpa.so.6'.
20132 WARNING: Library not found: could not resolve 'libxkbcommon-x11.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6XcbQpa.so.6'.
20132 WARNING: Library not found: could not resolve 'libxcb-shape.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libQt6XcbQpa.so.6'.
20132 WARNING: Library not found: could not resolve 'libpulse.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libavcodec.so.61'.
20132 WARNING: Library not found: could not resolve 'libpulse.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libavcodec.so.61.19.101'.
20132 WARNING: Library not found: could not resolve 'libpulse.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libavformat.so.61'.
20132 WARNING: Library not found: could not resolve 'libpulse.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libavformat.so.61.7.100'.
20132 WARNING: Library not found: could not resolve 'libpulse.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libavutil.so.59'.
20132 WARNING: Library not found: could not resolve 'libpulse.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libavutil.so.59.39.100'.
20132 WARNING: Library not found: could not resolve 'libpulse.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libswresample.so.5'.
20132 WARNING: Library not found: could not resolve 'libpulse.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libswresample.so.5.3.100'.
20132 WARNING: Library not found: could not resolve 'libpulse.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libswscale.so.8'.
20132 WARNING: Library not found: could not resolve 'libpulse.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libswscale.so.8.3.100'.
20132 WARNING: Library not found: could not resolve 'libXcomposite.so.1', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/libexec/QtWebEngineProcess'.
20132 WARNING: Library not found: could not resolve 'libsmime3.so', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/libexec/QtWebEngineProcess'.
20132 WARNING: Library not found: could not resolve 'libnssutil3.so', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/libexec/QtWebEngineProcess'.
20132 WARNING: Library not found: could not resolve 'libnspr4.so', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/libexec/QtWebEngineProcess'.
20132 WARNING: Library not found: could not resolve 'libXdamage.so.1', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/libexec/QtWebEngineProcess'.
20132 WARNING: Library not found: could not resolve 'libasound.so.2', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/libexec/QtWebEngineProcess'.
20132 WARNING: Library not found: could not resolve 'libnss3.so', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/libexec/QtWebEngineProcess'.
20132 WARNING: Library not found: could not resolve 'libXrandr.so.2', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/libexec/QtWebEngineProcess'.
20132 WARNING: Library not found: could not resolve 'libXtst.so.6', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/libexec/QtWebEngineProcess'.
20132 WARNING: Library not found: could not resolve 'libxkbfile.so.1', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/libexec/QtWebEngineProcess'.
20133 WARNING: Library not found: could not resolve 'libpulse.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libavcodec.so'.
20133 WARNING: Library not found: could not resolve 'libpulse.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libavformat.so'.
20133 WARNING: Library not found: could not resolve 'libpulse.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libavutil.so'.
20133 WARNING: Library not found: could not resolve 'libpulse.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libswresample.so'.
20133 WARNING: Library not found: could not resolve 'libpulse.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/lib/libswscale.so'.
20133 WARNING: Library not found: could not resolve 'libXcomposite.so.1', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/designer/libqwebengineview.so'.
20133 WARNING: Library not found: could not resolve 'libsmime3.so', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/designer/libqwebengineview.so'.
20133 WARNING: Library not found: could not resolve 'libnssutil3.so', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/designer/libqwebengineview.so'.
20133 WARNING: Library not found: could not resolve 'libnspr4.so', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/designer/libqwebengineview.so'.
20133 WARNING: Library not found: could not resolve 'libXdamage.so.1', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/designer/libqwebengineview.so'.
20133 WARNING: Library not found: could not resolve 'libasound.so.2', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/designer/libqwebengineview.so'.
20133 WARNING: Library not found: could not resolve 'libnss3.so', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/designer/libqwebengineview.so'.
20133 WARNING: Library not found: could not resolve 'libXrandr.so.2', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/designer/libqwebengineview.so'.
20133 WARNING: Library not found: could not resolve 'libXtst.so.6', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/designer/libqwebengineview.so'.
20133 WARNING: Library not found: could not resolve 'libxkbfile.so.1', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/designer/libqwebengineview.so'.
20133 WARNING: Library not found: could not resolve 'libQt6EglFsKmsGbmSupport.so.6', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/egldeviceintegrations/libqeglfs-kms-integration.so'.
20133 WARNING: Library not found: could not resolve 'libXrandr.so.2', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/multimedia/libffmpegmediaplugin.so'.
20133 WARNING: Library not found: could not resolve 'libpulse.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/multimedia/libffmpegmediaplugin.so'.
20133 WARNING: Library not found: could not resolve 'libxcb-icccm.so.4', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/platforms/libqxcb.so'.
20133 WARNING: Library not found: could not resolve 'libxcb-keysyms.so.1', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/platforms/libqxcb.so'.
20133 WARNING: Library not found: could not resolve 'libxcb-util.so.1', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/platforms/libqxcb.so'.
20133 WARNING: Library not found: could not resolve 'libxcb-image.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/platforms/libqxcb.so'.
20133 WARNING: Library not found: could not resolve 'libxcb-render.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/platforms/libqxcb.so'.
20133 WARNING: Library not found: could not resolve 'libxcb-cursor.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/platforms/libqxcb.so'.
20133 WARNING: Library not found: could not resolve 'libxcb-render-util.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/platforms/libqxcb.so'.
20133 WARNING: Library not found: could not resolve 'libxkbcommon-x11.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/platforms/libqxcb.so'.
20133 WARNING: Library not found: could not resolve 'libxcb-shape.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/platforms/libqxcb.so'.
20133 WARNING: Library not found: could not resolve 'libgtk-3.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/platformthemes/libqgtk3.so'.
20133 WARNING: Library not found: could not resolve 'libcairo.so.2', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/platformthemes/libqgtk3.so'.
20133 WARNING: Library not found: could not resolve 'libpango-1.0.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/platformthemes/libqgtk3.so'.
20133 WARNING: Library not found: could not resolve 'libgdk-3.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/platformthemes/libqgtk3.so'.
20133 WARNING: Library not found: could not resolve 'libharfbuzz.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/platformthemes/libqgtk3.so'.
20133 WARNING: Library not found: could not resolve 'libgdk_pixbuf-2.0.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/platformthemes/libqgtk3.so'.
20133 WARNING: Library not found: could not resolve 'libpangocairo-1.0.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/platformthemes/libqgtk3.so'.
20133 WARNING: Library not found: could not resolve 'libatk-1.0.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/platformthemes/libqgtk3.so'.
20133 WARNING: Library not found: could not resolve 'libcairo-gobject.so.2', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/platformthemes/libqgtk3.so'.
20133 WARNING: Library not found: could not resolve 'libcups.so.2', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/printsupport/libcupsprintersupport.so'.
20133 WARNING: Library not found: could not resolve 'libfbclient.so.2', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/sqldrivers/libqsqlibase.so'.
20133 WARNING: Library not found: could not resolve 'libmimerapi.so', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/sqldrivers/libqsqlmimer.so'.
20133 WARNING: Library not found: could not resolve 'libmysqlclient.so.21', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/sqldrivers/libqsqlmysql.so'.
20133 WARNING: Library not found: could not resolve 'libclntsh.so.23.1', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/sqldrivers/libqsqloci.so'.
20133 WARNING: Library not found: could not resolve 'libpq.so.5', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/sqldrivers/libqsqlpsql.so'.
20133 WARNING: Library not found: could not resolve 'libpulse.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/texttospeech/libqtexttospeech_mock.so'.
20133 WARNING: Library not found: could not resolve 'libspeechd.so.2', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/texttospeech/libqtexttospeech_speechd.so'.
20133 WARNING: Library not found: could not resolve 'libpulse.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/texttospeech/libqtexttospeech_speechd.so'.
20133 WARNING: Library not found: could not resolve 'libXcomposite.so.1', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/webview/libqtwebview_webengine.so'.
20133 WARNING: Library not found: could not resolve 'libsmime3.so', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/webview/libqtwebview_webengine.so'.
20133 WARNING: Library not found: could not resolve 'libnssutil3.so', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/webview/libqtwebview_webengine.so'.
20133 WARNING: Library not found: could not resolve 'libnspr4.so', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/webview/libqtwebview_webengine.so'.
20133 WARNING: Library not found: could not resolve 'libXdamage.so.1', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/webview/libqtwebview_webengine.so'.
20133 WARNING: Library not found: could not resolve 'libasound.so.2', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/webview/libqtwebview_webengine.so'.
20133 WARNING: Library not found: could not resolve 'libnss3.so', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/webview/libqtwebview_webengine.so'.
20133 WARNING: Library not found: could not resolve 'libXrandr.so.2', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/webview/libqtwebview_webengine.so'.
20133 WARNING: Library not found: could not resolve 'libXtst.so.6', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/webview/libqtwebview_webengine.so'.
20133 WARNING: Library not found: could not resolve 'libxkbfile.so.1', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/webview/libqtwebview_webengine.so'.
20133 WARNING: Library not found: could not resolve 'libxcb-icccm.so.4', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/xcbglintegrations/libqxcb-egl-integration.so'.
20134 WARNING: Library not found: could not resolve 'libxcb-keysyms.so.1', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/xcbglintegrations/libqxcb-egl-integration.so'.
20134 WARNING: Library not found: could not resolve 'libxcb-util.so.1', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/xcbglintegrations/libqxcb-egl-integration.so'.
20134 WARNING: Library not found: could not resolve 'libxcb-image.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/xcbglintegrations/libqxcb-egl-integration.so'.
20134 WARNING: Library not found: could not resolve 'libxcb-render.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/xcbglintegrations/libqxcb-egl-integration.so'.
20134 WARNING: Library not found: could not resolve 'libxcb-cursor.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/xcbglintegrations/libqxcb-egl-integration.so'.
20134 WARNING: Library not found: could not resolve 'libxcb-render-util.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/xcbglintegrations/libqxcb-egl-integration.so'.
20134 WARNING: Library not found: could not resolve 'libxkbcommon-x11.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/xcbglintegrations/libqxcb-egl-integration.so'.
20134 WARNING: Library not found: could not resolve 'libxcb-shape.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/xcbglintegrations/libqxcb-egl-integration.so'.
20134 WARNING: Library not found: could not resolve 'libxcb-icccm.so.4', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/xcbglintegrations/libqxcb-glx-integration.so'.
20134 WARNING: Library not found: could not resolve 'libxcb-keysyms.so.1', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/xcbglintegrations/libqxcb-glx-integration.so'.
20134 WARNING: Library not found: could not resolve 'libxcb-util.so.1', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/xcbglintegrations/libqxcb-glx-integration.so'.
20134 WARNING: Library not found: could not resolve 'libxcb-image.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/xcbglintegrations/libqxcb-glx-integration.so'.
20134 WARNING: Library not found: could not resolve 'libxcb-render.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/xcbglintegrations/libqxcb-glx-integration.so'.
20134 WARNING: Library not found: could not resolve 'libxcb-cursor.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/xcbglintegrations/libqxcb-glx-integration.so'.
20134 WARNING: Library not found: could not resolve 'libxcb-render-util.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/xcbglintegrations/libqxcb-glx-integration.so'.
20134 WARNING: Library not found: could not resolve 'libxkbcommon-x11.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/xcbglintegrations/libqxcb-glx-integration.so'.
20134 WARNING: Library not found: could not resolve 'libxcb-shape.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/plugins/xcbglintegrations/libqxcb-glx-integration.so'.
20134 WARNING: Library not found: could not resolve 'libpulse.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/qml/QtMultimedia/libquickmultimediaplugin.so'.
20134 WARNING: Library not found: could not resolve 'libQt6QuickShapesDesignHelpers.so.6', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/qml/QtQuick/Shapes/DesignHelpers/libqtquickshapesdesignhelpersplugin.so'.
20134 WARNING: Library not found: could not resolve 'libpulse.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/qml/QtQuick/VirtualKeyboard/Components/libqtvkbcomponentsplugin.so'.
20134 WARNING: Library not found: could not resolve 'libpulse.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/qml/QtQuick3D/SpatialAudio/libquick3dspatialaudioplugin.so'.
20134 WARNING: Library not found: could not resolve 'libpulse.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/qml/QtTextToSpeech/libtexttospeechqmlplugin.so'.
20134 WARNING: Library not found: could not resolve 'libQt6WaylandCompositorIviapplication.so.6', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/qml/QtWayland/Compositor/IviApplication/libwaylandcompositoriviapplicationplugin.so'.
20134 WARNING: Library not found: could not resolve 'libQt6WaylandCompositorPresentationTime.so.6', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/qml/QtWayland/Compositor/PresentationTime/libwaylandcompositorpresentationtimeplugin.so'.
20134 WARNING: Library not found: could not resolve 'libQt6WaylandCompositorWLShell.so.6', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/qml/QtWayland/Compositor/WlShell/libwaylandcompositorwlshellplugin.so'.
20134 WARNING: Library not found: could not resolve 'libQt6WaylandCompositorXdgShell.so.6', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/qml/QtWayland/Compositor/XdgShell/libwaylandcompositorxdgshellplugin.so'.
20134 WARNING: Library not found: could not resolve 'libXcomposite.so.1', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/qml/QtWebEngine/libqtwebenginequickplugin.so'.
20134 WARNING: Library not found: could not resolve 'libsmime3.so', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/qml/QtWebEngine/libqtwebenginequickplugin.so'.
20134 WARNING: Library not found: could not resolve 'libnssutil3.so', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/qml/QtWebEngine/libqtwebenginequickplugin.so'.
20134 WARNING: Library not found: could not resolve 'libnspr4.so', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/qml/QtWebEngine/libqtwebenginequickplugin.so'.
20134 WARNING: Library not found: could not resolve 'libXdamage.so.1', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/qml/QtWebEngine/libqtwebenginequickplugin.so'.
20134 WARNING: Library not found: could not resolve 'libasound.so.2', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/qml/QtWebEngine/libqtwebenginequickplugin.so'.
20134 WARNING: Library not found: could not resolve 'libnss3.so', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/qml/QtWebEngine/libqtwebenginequickplugin.so'.
20134 WARNING: Library not found: could not resolve 'libXrandr.so.2', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/qml/QtWebEngine/libqtwebenginequickplugin.so'.
20134 WARNING: Library not found: could not resolve 'libXtst.so.6', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/qml/QtWebEngine/libqtwebenginequickplugin.so'.
20134 WARNING: Library not found: could not resolve 'libxkbfile.so.1', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/Qt/qml/QtWebEngine/libqtwebenginequickplugin.so'.
20134 WARNING: Library not found: could not resolve 'libXcomposite.so.1', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/QtWebEngineWidgets.abi3.so'.
20134 WARNING: Library not found: could not resolve 'libsmime3.so', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/QtWebEngineWidgets.abi3.so'.
20134 WARNING: Library not found: could not resolve 'libnssutil3.so', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/QtWebEngineWidgets.abi3.so'.
20134 WARNING: Library not found: could not resolve 'libnspr4.so', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/QtWebEngineWidgets.abi3.so'.
20134 WARNING: Library not found: could not resolve 'libXdamage.so.1', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/QtWebEngineWidgets.abi3.so'.
20134 WARNING: Library not found: could not resolve 'libasound.so.2', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/QtWebEngineWidgets.abi3.so'.
20134 WARNING: Library not found: could not resolve 'libnss3.so', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/QtWebEngineWidgets.abi3.so'.
20134 WARNING: Library not found: could not resolve 'libXrandr.so.2', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/QtWebEngineWidgets.abi3.so'.
20134 WARNING: Library not found: could not resolve 'libXtst.so.6', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/QtWebEngineWidgets.abi3.so'.
20134 WARNING: Library not found: could not resolve 'libxkbfile.so.1', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/QtWebEngineWidgets.abi3.so'.
20134 WARNING: Library not found: could not resolve 'libXcomposite.so.1', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/QtWebEngineQuick.abi3.so'.
20134 WARNING: Library not found: could not resolve 'libsmime3.so', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/QtWebEngineQuick.abi3.so'.
20134 WARNING: Library not found: could not resolve 'libnssutil3.so', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/QtWebEngineQuick.abi3.so'.
20135 WARNING: Library not found: could not resolve 'libnspr4.so', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/QtWebEngineQuick.abi3.so'.
20135 WARNING: Library not found: could not resolve 'libXdamage.so.1', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/QtWebEngineQuick.abi3.so'.
20135 WARNING: Library not found: could not resolve 'libasound.so.2', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/QtWebEngineQuick.abi3.so'.
20135 WARNING: Library not found: could not resolve 'libnss3.so', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/QtWebEngineQuick.abi3.so'.
20135 WARNING: Library not found: could not resolve 'libXrandr.so.2', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/QtWebEngineQuick.abi3.so'.
20135 WARNING: Library not found: could not resolve 'libXtst.so.6', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/QtWebEngineQuick.abi3.so'.
20135 WARNING: Library not found: could not resolve 'libxkbfile.so.1', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/QtWebEngineQuick.abi3.so'.
20135 WARNING: Library not found: could not resolve 'libXcomposite.so.1', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/QtWebEngineCore.abi3.so'.
20135 WARNING: Library not found: could not resolve 'libsmime3.so', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/QtWebEngineCore.abi3.so'.
20135 WARNING: Library not found: could not resolve 'libnssutil3.so', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/QtWebEngineCore.abi3.so'.
20135 WARNING: Library not found: could not resolve 'libnspr4.so', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/QtWebEngineCore.abi3.so'.
20135 WARNING: Library not found: could not resolve 'libXdamage.so.1', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/QtWebEngineCore.abi3.so'.
20135 WARNING: Library not found: could not resolve 'libasound.so.2', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/QtWebEngineCore.abi3.so'.
20135 WARNING: Library not found: could not resolve 'libnss3.so', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/QtWebEngineCore.abi3.so'.
20135 WARNING: Library not found: could not resolve 'libXrandr.so.2', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/QtWebEngineCore.abi3.so'.
20135 WARNING: Library not found: could not resolve 'libXtst.so.6', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/QtWebEngineCore.abi3.so'.
20135 WARNING: Library not found: could not resolve 'libxkbfile.so.1', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/QtWebEngineCore.abi3.so'.
20135 WARNING: Library not found: could not resolve 'libpulse.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/QtTextToSpeech.abi3.so'.
20135 WARNING: Library not found: could not resolve 'libpulse.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/QtSpatialAudio.abi3.so'.
20135 WARNING: Library not found: could not resolve 'libpcsclite.so.1', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/QtNfc.abi3.so'.
20135 WARNING: Library not found: could not resolve 'libpulse.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/QtMultimediaWidgets.abi3.so'.
20135 WARNING: Library not found: could not resolve 'libpulse.so.0', dependency of '/app/venv-linux/lib/python3.10/site-packages/PySide6/QtMultimedia.abi3.so'.
20194 INFO: Warnings written to /tmp/cloudrestore-build/CloudRestoreAS/warn-CloudRestoreAS.txt
20218 INFO: Graph cross-reference written to /tmp/cloudrestore-build/CloudRestoreAS/xref-CloudRestoreAS.html
20316 INFO: checking PYZ
20316 INFO: Building PYZ because PYZ-00.toc is non existent
20316 INFO: Building PYZ (ZlibArchive) /tmp/cloudrestore-build/CloudRestoreAS/PYZ-00.pyz
20520 INFO: Building PYZ (ZlibArchive) /tmp/cloudrestore-build/CloudRestoreAS/PYZ-00.pyz completed successfully.
20524 WARNING: Ignoring icon; supported only on Windows and macOS!
20569 INFO: checking PKG
20569 INFO: Building PKG because PKG-00.toc is non existent
20569 INFO: Building PKG (CArchive) CloudRestoreAS.pkg
88430 INFO: Building PKG (CArchive) CloudRestoreAS.pkg completed successfully.
88476 INFO: Bootloader /app/venv-linux/lib/python3.10/site-packages/PyInstaller/bootloader/Linux-64bit-intel/run
88476 INFO: checking EXE
88476 INFO: Building EXE because EXE-00.toc is non existent
88476 INFO: Building EXE from EXE-00.toc
88476 INFO: Copying bootloader EXE to /app/dist/CloudRestoreAS
88477 INFO: Appending PKG archive to custom ELF section in EXE
90592 INFO: Building EXE from EXE-00.toc completed successfully.
90652 INFO: Build complete! The results are available in: /app/dist
Build OK: /app/dist/CloudRestoreAS
Distribuir solo el binario; al ejecutar crea config/ automaticamente.
===LINUX_BUILD_EXIT=0===

343
build-windows.log Normal file
View File

@@ -0,0 +1,343 @@
===============================================
CloudRestoreAS - Build Windows (onefile)
===============================================
==> 7-Zip Extra (26.01)
7-Zip ya existe
==> ODBC Driver 18 (extraer DLLs del MSI)
ODBC ya existe
Listo: \\wsl.localhost\Debian\home\hugo_reyes\dev\CloudRecoveryAS\packaging\bundled\windows
Python: C:\Users\Hugo Reyes\AppData\Local\Programs\Python\Python313\python.exe
[notice] A new release of pip is available: 25.0.1 -> 26.1.2
[notice] To update, run: \\wsl.localhost\Debian\home\hugo_reyes\dev\CloudRecoveryAS\venv-windows\Scripts\python.exe -m pip install --upgrade pip
Ejecutando PyInstaller (onefile)...
Workpath: C:\Users\Hugo Reyes\AppData\Local\Temp\cloudrestore-build
937 INFO: PyInstaller: 6.20.0, contrib hooks: 2026.6
937 INFO: Python: 3.13.3
963 INFO: Platform: Windows-11-10.0.26200-SP0
963 INFO: Python environment: \\wsl.localhost\Debian\home\hugo_reyes\dev\CloudRecoveryAS\venv-windows
971 INFO: Removing temporary files and cleaning cache in C:\Users\Hugo Reyes\AppData\Local\pyinstaller
1653 WARNING: Failed to collect submodules for 'PySide6.scripts.deploy_lib' because importing 'PySide6.scripts.deploy_lib' raised: ModuleNotFoundError: No module named 'project_lib'
41112 INFO: Module search paths (PYTHONPATH):
['\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS',
'C:\\Users\\Hugo '
'Reyes\\AppData\\Local\\Programs\\Python\\Python313\\python313.zip',
'C:\\Users\\Hugo Reyes\\AppData\\Local\\Programs\\Python\\Python313\\DLLs',
'C:\\Users\\Hugo Reyes\\AppData\\Local\\Programs\\Python\\Python313\\Lib',
'C:\\Users\\Hugo Reyes\\AppData\\Local\\Programs\\Python\\Python313',
'\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows',
'\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages',
'\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\win32',
'\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\win32\\lib',
'\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\pythonwin',
'\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS']
42264 INFO: Appending 'binaries' from .spec
43050 INFO: Appending 'datas' from .spec
51557 INFO: checking Analysis
51557 INFO: Building Analysis because Analysis-00.toc is non existent
51557 INFO: Looking for Python shared library...
51557 INFO: Using Python shared library: C:\Users\Hugo Reyes\AppData\Local\Programs\Python\Python313\python313.dll
51557 INFO: Running Analysis Analysis-00.toc
51557 INFO: Target bytecode optimization level: 0
51557 INFO: Initializing module dependency graph...
51558 INFO: Initializing module graph hook caches...
51674 INFO: Analyzing modules for base_library.zip ...
54725 INFO: Processing standard module hook 'hook-encodings.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
56081 INFO: Processing standard module hook 'hook-heapq.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
58920 INFO: Processing standard module hook 'hook-pickle.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
63112 INFO: Caching module dependency graph...
63151 INFO: Analyzing \\wsl.localhost\Debian\home\hugo_reyes\dev\CloudRecoveryAS\runner.py
63182 INFO: Processing standard module hook 'hook-PySide6.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
63885 INFO: Processing standard module hook 'hook-shiboken6.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
64039 INFO: Processing standard module hook 'hook-PySide6.QtNetwork.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
68821 INFO: Processing standard module hook 'hook-PySide6.QtCore.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
73034 INFO: Processing standard module hook 'hook-PySide6.QtWidgets.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
77556 INFO: Processing standard module hook 'hook-PySide6.QtGui.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
106235 INFO: Processing standard module hook 'hook-sqlite3.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
108350 INFO: Processing standard module hook 'hook-platform.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
108409 INFO: Processing standard module hook 'hook-_ctypes.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
108712 INFO: Processing standard module hook 'hook-cryptography.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\_pyinstaller_hooks_contrib\\stdhooks'
112516 INFO: hook-cryptography: cryptography does not seem to be using dynamically linked OpenSSL.
112922 INFO: Processing standard module hook 'hook-pyodbc.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\_pyinstaller_hooks_contrib\\stdhooks'
113246 INFO: Processing standard module hook 'hook-urllib3.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\_pyinstaller_hooks_contrib\\stdhooks'
113483 INFO: Processing pre-safe-import-module hook 'hook-backports.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks\\pre_safe_import_module'
113491 INFO: SetuptoolsInfo: initializing cached setuptools info...
117037 INFO: Setuptools: 'backports' appears to be a full setuptools-vendored copy - creating alias to 'setuptools._vendor.backports'!
117060 INFO: Processing standard module hook 'hook-setuptools.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
117128 INFO: Processing pre-safe-import-module hook 'hook-distutils.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks\\pre_safe_import_module'
117267 INFO: Processing standard module hook 'hook-sysconfig.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
117294 INFO: Processing pre-safe-import-module hook 'hook-jaraco.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks\\pre_safe_import_module'
117301 INFO: Setuptools: 'jaraco' appears to be a full setuptools-vendored copy - creating alias to 'setuptools._vendor.jaraco'!
117334 INFO: Processing pre-safe-import-module hook 'hook-more_itertools.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks\\pre_safe_import_module'
117337 INFO: Setuptools: 'more_itertools' appears to be a setuptools-vendored copy - creating alias to 'setuptools._vendor.more_itertools'!
117657 INFO: Processing pre-safe-import-module hook 'hook-typing_extensions.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks\\pre_safe_import_module'
117854 INFO: Processing pre-safe-import-module hook 'hook-packaging.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks\\pre_safe_import_module'
118406 INFO: Processing standard module hook 'hook-setuptools._vendor.jaraco.text.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
118438 INFO: Processing pre-safe-import-module hook 'hook-importlib_resources.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks\\pre_safe_import_module'
119734 INFO: Processing pre-safe-import-module hook 'hook-importlib_metadata.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks\\pre_safe_import_module'
119737 INFO: Setuptools: 'importlib_metadata' appears to be a setuptools-vendored copy - creating alias to 'setuptools._vendor.importlib_metadata'!
119800 INFO: Processing standard module hook 'hook-setuptools._vendor.importlib_metadata.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
120023 INFO: Processing pre-safe-import-module hook 'hook-zipp.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks\\pre_safe_import_module'
120028 INFO: Setuptools: 'zipp' appears to be a setuptools-vendored copy - creating alias to 'setuptools._vendor.zipp'!
120782 INFO: Processing pre-safe-import-module hook 'hook-tomli.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks\\pre_safe_import_module'
120786 INFO: Setuptools: 'tomli' appears to be a setuptools-vendored copy - creating alias to 'setuptools._vendor.tomli'!
121992 INFO: Processing pre-safe-import-module hook 'hook-wheel.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks\\pre_safe_import_module'
121995 INFO: Setuptools: 'wheel' appears to be a setuptools-vendored copy - creating alias to 'setuptools._vendor.wheel'!
123153 INFO: Processing standard module hook 'hook-certifi.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\_pyinstaller_hooks_contrib\\stdhooks'
123371 INFO: Processing standard module hook 'hook-charset_normalizer.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\_pyinstaller_hooks_contrib\\stdhooks'
124596 INFO: Processing standard module hook 'hook-bcrypt.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\_pyinstaller_hooks_contrib\\stdhooks'
126239 INFO: Processing standard module hook 'hook-difflib.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
126586 INFO: Processing standard module hook 'hook-multiprocessing.util.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
126737 INFO: Processing standard module hook 'hook-xml.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
128497 INFO: Processing standard module hook 'hook-nacl.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\_pyinstaller_hooks_contrib\\stdhooks'
130237 INFO: Analyzing hidden import 'PySide6.Qt3DAnimation'
130274 INFO: Processing standard module hook 'hook-PySide6.Qt3DAnimation.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
131678 INFO: Processing standard module hook 'hook-PySide6.Qt3DCore.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
133194 INFO: Processing standard module hook 'hook-PySide6.Qt3DRender.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
144160 INFO: Processing standard module hook 'hook-PySide6.QtOpenGL.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
148253 INFO: Analyzing hidden import 'PySide6.Qt3DExtras'
148333 INFO: Processing standard module hook 'hook-PySide6.Qt3DExtras.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
150312 INFO: Analyzing hidden import 'PySide6.Qt3DInput'
150345 INFO: Processing standard module hook 'hook-PySide6.Qt3DInput.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
151944 INFO: Analyzing hidden import 'PySide6.Qt3DLogic'
151963 INFO: Processing standard module hook 'hook-PySide6.Qt3DLogic.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
153091 INFO: Analyzing hidden import 'PySide6.QtAsyncio'
153174 INFO: Analyzing hidden import 'PySide6.QtAxContainer'
153200 INFO: Processing standard module hook 'hook-PySide6.QtAxContainer.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
154345 INFO: Analyzing hidden import 'PySide6.QtBluetooth'
154413 INFO: Processing standard module hook 'hook-PySide6.QtBluetooth.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
155394 INFO: Analyzing hidden import 'PySide6.QtCanvasPainter'
155709 INFO: Processing standard module hook 'hook-PySide6.QtQuick.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
159855 INFO: Processing standard module hook 'hook-PySide6.QtQml.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
--- Logging error ---
Traceback (most recent call last):
File "C:\Users\Hugo Reyes\AppData\Local\Programs\Python\Python313\Lib\logging\__init__.py", line 1150, in emit
msg = self.format(record)
File "C:\Users\Hugo Reyes\AppData\Local\Programs\Python\Python313\Lib\logging\__init__.py", line 998, in format
return fmt.format(record)
~~~~~~~~~~^^^^^^^^
File "C:\Users\Hugo Reyes\AppData\Local\Programs\Python\Python313\Lib\logging\__init__.py", line 711, in format
record.message = record.getMessage()
~~~~~~~~~~~~~~~~~^^
File "C:\Users\Hugo Reyes\AppData\Local\Programs\Python\Python313\Lib\logging\__init__.py", line 400, in getMessage
msg = msg % self.args
~~~~^~~~~~~~~~~
TypeError: not enough arguments for format string
Call stack:
File "<frozen runpy>", line 198, in _run_module_as_main
File "<frozen runpy>", line 88, in _run_code
File "\\wsl.localhost\Debian\home\hugo_reyes\dev\CloudRecoveryAS\venv-windows\Lib\site-packages\PyInstaller\__main__.py", line 321, in <module>
run()
File "\\wsl.localhost\Debian\home\hugo_reyes\dev\CloudRecoveryAS\venv-windows\Lib\site-packages\PyInstaller\__main__.py", line 215, in run
run_build(pyi_config, spec_file, **vars(args))
File "\\wsl.localhost\Debian\home\hugo_reyes\dev\CloudRecoveryAS\venv-windows\Lib\site-packages\PyInstaller\__main__.py", line 70, in run_build
PyInstaller.building.build_main.main(pyi_config, spec_file, **kwargs)
File "\\wsl.localhost\Debian\home\hugo_reyes\dev\CloudRecoveryAS\venv-windows\Lib\site-packages\PyInstaller\building\build_main.py", line 1275, in main
build(specfile, distpath, workpath, clean_build)
File "\\wsl.localhost\Debian\home\hugo_reyes\dev\CloudRecoveryAS\venv-windows\Lib\site-packages\PyInstaller\building\build_main.py", line 1213, in build
exec(code, spec_namespace)
File "\\wsl.localhost\Debian\home\hugo_reyes\dev\CloudRecoveryAS\packaging\CloudRestoreAS.spec", line 59, in <module>
a = Analysis(
File "\\wsl.localhost\Debian\home\hugo_reyes\dev\CloudRecoveryAS\venv-windows\Lib\site-packages\PyInstaller\building\build_main.py", line 584, in __init__
self.__postinit__()
File "\\wsl.localhost\Debian\home\hugo_reyes\dev\CloudRecoveryAS\venv-windows\Lib\site-packages\PyInstaller\building\datastruct.py", line 184, in __postinit__
self.assemble()
File "\\wsl.localhost\Debian\home\hugo_reyes\dev\CloudRecoveryAS\venv-windows\Lib\site-packages\PyInstaller\building\build_main.py", line 719, in assemble
self.graph.add_hiddenimports(self.hiddenimports)
File "\\wsl.localhost\Debian\home\hugo_reyes\dev\CloudRecoveryAS\venv-windows\Lib\site-packages\PyInstaller\depend\analysis.py", line 770, in add_hiddenimports
nodes = self.import_hook(modnm)
File "\\wsl.localhost\Debian\home\hugo_reyes\dev\CloudRecoveryAS\venv-windows\Lib\site-packages\PyInstaller\lib\modulegraph\modulegraph.py", line 1279, in import_hook
submodule = self._safe_import_module(head, mname, submodule)
File "\\wsl.localhost\Debian\home\hugo_reyes\dev\CloudRecoveryAS\venv-windows\Lib\site-packages\PyInstaller\depend\analysis.py", line 539, in _safe_import_module
return super()._safe_import_module(module_basename, module_name, parent_package)
File "\\wsl.localhost\Debian\home\hugo_reyes\dev\CloudRecoveryAS\venv-windows\Lib\site-packages\PyInstaller\lib\modulegraph\modulegraph.py", line 1826, in _safe_import_module
self._process_imports(n)
File "\\wsl.localhost\Debian\home\hugo_reyes\dev\CloudRecoveryAS\venv-windows\Lib\site-packages\PyInstaller\lib\modulegraph\modulegraph.py", line 2654, in _process_imports
target_modules = self._safe_import_hook(*import_info, **kwargs)
File "\\wsl.localhost\Debian\home\hugo_reyes\dev\CloudRecoveryAS\venv-windows\Lib\site-packages\PyInstaller\depend\analysis.py", line 477, in _safe_import_hook
ret_modules = super()._safe_import_hook(
File "\\wsl.localhost\Debian\home\hugo_reyes\dev\CloudRecoveryAS\venv-windows\Lib\site-packages\PyInstaller\lib\modulegraph\modulegraph.py", line 2103, in _safe_import_hook
target_modules = self.import_hook(
File "\\wsl.localhost\Debian\home\hugo_reyes\dev\CloudRecoveryAS\venv-windows\Lib\site-packages\PyInstaller\lib\modulegraph\modulegraph.py", line 1279, in import_hook
submodule = self._safe_import_module(head, mname, submodule)
File "\\wsl.localhost\Debian\home\hugo_reyes\dev\CloudRecoveryAS\venv-windows\Lib\site-packages\PyInstaller\depend\analysis.py", line 539, in _safe_import_module
return super()._safe_import_module(module_basename, module_name, parent_package)
File "\\wsl.localhost\Debian\home\hugo_reyes\dev\CloudRecoveryAS\venv-windows\Lib\site-packages\PyInstaller\lib\modulegraph\modulegraph.py", line 1826, in _safe_import_module
self._process_imports(n)
File "\\wsl.localhost\Debian\home\hugo_reyes\dev\CloudRecoveryAS\venv-windows\Lib\site-packages\PyInstaller\lib\modulegraph\modulegraph.py", line 2654, in _process_imports
target_modules = self._safe_import_hook(*import_info, **kwargs)
File "\\wsl.localhost\Debian\home\hugo_reyes\dev\CloudRecoveryAS\venv-windows\Lib\site-packages\PyInstaller\depend\analysis.py", line 477, in _safe_import_hook
ret_modules = super()._safe_import_hook(
File "\\wsl.localhost\Debian\home\hugo_reyes\dev\CloudRecoveryAS\venv-windows\Lib\site-packages\PyInstaller\lib\modulegraph\modulegraph.py", line 2103, in _safe_import_hook
target_modules = self.import_hook(
File "\\wsl.localhost\Debian\home\hugo_reyes\dev\CloudRecoveryAS\venv-windows\Lib\site-packages\PyInstaller\lib\modulegraph\modulegraph.py", line 1279, in import_hook
submodule = self._safe_import_module(head, mname, submodule)
File "\\wsl.localhost\Debian\home\hugo_reyes\dev\CloudRecoveryAS\venv-windows\Lib\site-packages\PyInstaller\depend\analysis.py", line 539, in _safe_import_module
return super()._safe_import_module(module_basename, module_name, parent_package)
File "\\wsl.localhost\Debian\home\hugo_reyes\dev\CloudRecoveryAS\venv-windows\Lib\site-packages\PyInstaller\lib\modulegraph\modulegraph.py", line 1826, in _safe_import_module
self._process_imports(n)
File "\\wsl.localhost\Debian\home\hugo_reyes\dev\CloudRecoveryAS\venv-windows\Lib\site-packages\PyInstaller\lib\modulegraph\modulegraph.py", line 2654, in _process_imports
target_modules = self._safe_import_hook(*import_info, **kwargs)
File "\\wsl.localhost\Debian\home\hugo_reyes\dev\CloudRecoveryAS\venv-windows\Lib\site-packages\PyInstaller\depend\analysis.py", line 385, in _safe_import_hook
excluded_imports = self._find_all_excluded_imports(source_module.identifier)
File "\\wsl.localhost\Debian\home\hugo_reyes\dev\CloudRecoveryAS\venv-windows\Lib\site-packages\PyInstaller\depend\analysis.py", line 373, in _find_all_excluded_imports
excluded_imports.update(module_hook.excludedimports)
File "\\wsl.localhost\Debian\home\hugo_reyes\dev\CloudRecoveryAS\venv-windows\Lib\site-packages\PyInstaller\depend\imphook.py", line 343, in __getattr__
self._load_hook_module()
File "\\wsl.localhost\Debian\home\hugo_reyes\dev\CloudRecoveryAS\venv-windows\Lib\site-packages\PyInstaller\depend\imphook.py", line 422, in _load_hook_module
self._hook_module = importlib_load_source(self.hook_module_name, self.hook_filename)
File "\\wsl.localhost\Debian\home\hugo_reyes\dev\CloudRecoveryAS\venv-windows\Lib\site-packages\PyInstaller\compat.py", line 566, in importlib_load_source
mod_loader.exec_module(mod)
File "<frozen importlib._bootstrap_external>", line 1026, in exec_module
File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
File "\\wsl.localhost\Debian\home\hugo_reyes\dev\CloudRecoveryAS\venv-windows\Lib\site-packages\PyInstaller\hooks\hook-PySide6.QtQml.py", line 15, in <module>
qml_binaries, qml_datas = pyside6_library_info.collect_qtqml_files()
File "\\wsl.localhost\Debian\home\hugo_reyes\dev\CloudRecoveryAS\venv-windows\Lib\site-packages\PyInstaller\utils\hooks\qt\__init__.py", line 963, in collect_qtqml_files
plugin_binaries, plugin_datas = self._process_qml_plugin(qmldir_file)
File "\\wsl.localhost\Debian\home\hugo_reyes\dev\CloudRecoveryAS\venv-windows\Lib\site-packages\PyInstaller\utils\hooks\qt\__init__.py", line 1014, in _process_qml_plugin
logger.warn("%s: QML plugin binary %r does not exist!", str(plugin_file))
Message: '%s: QML plugin binary %r does not exist!'
Arguments: ('\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PySide6\\qml\\Qt\\labs\\assetdownloader\\qmlassetdownloaderprivateplugin.dll',)
230199 INFO: Analyzing hidden import 'PySide6.QtCharts'
230330 INFO: Processing standard module hook 'hook-PySide6.QtCharts.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
232540 INFO: Analyzing hidden import 'PySide6.QtConcurrent'
232556 INFO: Processing standard module hook 'hook-PySide6.QtConcurrent.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
233299 INFO: Analyzing hidden import 'PySide6.QtDBus'
233342 INFO: Processing standard module hook 'hook-PySide6.QtDBus.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
234446 INFO: Analyzing hidden import 'PySide6.QtDataVisualization'
234568 INFO: Processing standard module hook 'hook-PySide6.QtDataVisualization.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
236233 INFO: Analyzing hidden import 'PySide6.QtDesigner'
236274 INFO: Processing standard module hook 'hook-PySide6.QtDesigner.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
241104 INFO: Analyzing hidden import 'PySide6.QtGraphs'
241324 INFO: Processing standard module hook 'hook-PySide6.QtGraphs.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
243536 INFO: Analyzing hidden import 'PySide6.QtGraphsWidgets'
243565 INFO: Processing standard module hook 'hook-PySide6.QtGraphsWidgets.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
244563 INFO: Processing standard module hook 'hook-PySide6.QtQuickWidgets.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
246244 INFO: Analyzing hidden import 'PySide6.QtHelp'
246273 INFO: Processing standard module hook 'hook-PySide6.QtHelp.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
247638 INFO: Analyzing hidden import 'PySide6.QtHttpServer'
247669 INFO: Processing standard module hook 'hook-PySide6.QtHttpServer.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
249385 INFO: Analyzing hidden import 'PySide6.QtLocation'
249581 INFO: Processing standard module hook 'hook-PySide6.QtLocation.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
252459 INFO: Processing standard module hook 'hook-PySide6.QtPositioning.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
254747 INFO: Analyzing hidden import 'PySide6.QtMultimedia'
254834 INFO: Processing standard module hook 'hook-PySide6.QtMultimedia.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
258359 INFO: Analyzing hidden import 'PySide6.QtMultimediaWidgets'
258375 INFO: Processing standard module hook 'hook-PySide6.QtMultimediaWidgets.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
260168 INFO: Analyzing hidden import 'PySide6.QtNetworkAuth'
260207 INFO: Processing standard module hook 'hook-PySide6.QtNetworkAuth.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
261657 INFO: Analyzing hidden import 'PySide6.QtNfc'
261690 INFO: Processing standard module hook 'hook-PySide6.QtNfc.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
262702 INFO: Analyzing hidden import 'PySide6.QtOpenGLWidgets'
262719 INFO: Processing standard module hook 'hook-PySide6.QtOpenGLWidgets.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
264327 INFO: Analyzing hidden import 'PySide6.QtPdf'
264355 INFO: Processing standard module hook 'hook-PySide6.QtPdf.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
265791 INFO: Analyzing hidden import 'PySide6.QtPdfWidgets'
265809 INFO: Processing standard module hook 'hook-PySide6.QtPdfWidgets.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
267252 INFO: Analyzing hidden import 'PySide6.QtPrintSupport'
267279 INFO: Processing standard module hook 'hook-PySide6.QtPrintSupport.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
269442 INFO: Analyzing hidden import 'PySide6.QtQuick3D'
269489 INFO: Processing standard module hook 'hook-PySide6.QtQuick3D.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
271627 INFO: Analyzing hidden import 'PySide6.QtQuickControls2'
271640 INFO: Processing standard module hook 'hook-PySide6.QtQuickControls2.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
273064 INFO: Analyzing hidden import 'PySide6.QtQuickTest'
273078 INFO: Analyzing hidden import 'PySide6.QtRemoteObjects'
273107 INFO: Processing standard module hook 'hook-PySide6.QtRemoteObjects.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
274340 INFO: Analyzing hidden import 'PySide6.QtScxml'
274392 INFO: Processing standard module hook 'hook-PySide6.QtScxml.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
275886 INFO: Analyzing hidden import 'PySide6.QtSensors'
275927 INFO: Processing standard module hook 'hook-PySide6.QtSensors.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
277621 INFO: Analyzing hidden import 'PySide6.QtSerialBus'
277722 INFO: Processing standard module hook 'hook-PySide6.QtSerialBus.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
281072 INFO: Analyzing hidden import 'PySide6.QtSerialPort'
281104 INFO: Processing standard module hook 'hook-PySide6.QtSerialPort.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
282168 INFO: Analyzing hidden import 'PySide6.QtSpatialAudio'
282208 INFO: Processing standard module hook 'hook-PySide6.QtSpatialAudio.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
283945 INFO: Analyzing hidden import 'PySide6.QtSql'
284080 INFO: Processing standard module hook 'hook-PySide6.QtSql.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
288123 INFO: Analyzing hidden import 'PySide6.QtStateMachine'
288150 INFO: Processing standard module hook 'hook-PySide6.QtStateMachine.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
289986 INFO: Analyzing hidden import 'PySide6.QtSvg'
290006 INFO: Processing standard module hook 'hook-PySide6.QtSvg.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
291280 INFO: Analyzing hidden import 'PySide6.QtSvgWidgets'
291297 INFO: Processing standard module hook 'hook-PySide6.QtSvgWidgets.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
292814 INFO: Analyzing hidden import 'PySide6.QtTest'
292847 INFO: Processing standard module hook 'hook-PySide6.QtTest.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
294027 INFO: Analyzing hidden import 'PySide6.QtTextToSpeech'
294048 INFO: Processing standard module hook 'hook-PySide6.QtTextToSpeech.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
296163 INFO: Analyzing hidden import 'PySide6.QtUiTools'
296177 INFO: Processing standard module hook 'hook-PySide6.QtUiTools.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
297703 INFO: Analyzing hidden import 'PySide6.QtWebChannel'
297732 INFO: Processing standard module hook 'hook-PySide6.QtWebChannel.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
299086 INFO: Analyzing hidden import 'PySide6.QtWebEngineCore'
299287 INFO: Processing standard module hook 'hook-PySide6.QtWebEngineCore.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
302818 INFO: Analyzing hidden import 'PySide6.QtWebEngineQuick'
302842 INFO: Processing standard module hook 'hook-PySide6.QtWebEngineQuick.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
304326 INFO: Analyzing hidden import 'PySide6.QtWebEngineWidgets'
304347 INFO: Processing standard module hook 'hook-PySide6.QtWebEngineWidgets.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
306188 INFO: Analyzing hidden import 'PySide6.QtWebSockets'
306220 INFO: Processing standard module hook 'hook-PySide6.QtWebSockets.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
307657 INFO: Analyzing hidden import 'PySide6.QtWebView'
307698 INFO: Analyzing hidden import 'PySide6.QtXml'
307731 INFO: Processing standard module hook 'hook-PySide6.QtXml.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
309023 INFO: Analyzing hidden import 'PySide6._config'
309037 INFO: Analyzing hidden import 'PySide6._git_pyside_version'
309066 INFO: Analyzing hidden import 'PySide6.scripts'
309077 INFO: Analyzing hidden import 'PySide6.scripts.deploy'
309198 INFO: Analyzing hidden import 'PySide6.scripts.metaobjectdump'
309272 INFO: Analyzing hidden import 'PySide6.scripts.project'
309432 INFO: Analyzing hidden import 'PySide6.scripts.project_lib'
309645 INFO: Processing standard module hook 'hook-xml.etree.cElementTree.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
309737 INFO: Analyzing hidden import 'PySide6.scripts.pyside_tool'
309757 INFO: Analyzing hidden import 'PySide6.scripts.qml'
309783 INFO: Analyzing hidden import 'PySide6.scripts.qtpy2cpp'
309805 INFO: Analyzing hidden import 'PySide6.support'
309816 INFO: Analyzing hidden import 'PySide6.support.deprecated'
309829 INFO: Analyzing hidden import 'PySide6.support.generate_pyi'
309853 INFO: Processing module hooks (post-graph stage)...
311602 INFO: Performing binary vs. data reclassification (3706 entries)
374194 INFO: Looking for ctypes DLLs
374269 INFO: Analyzing run-time hooks ...
374273 INFO: Including run-time hook 'pyi_rth_inspect.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks'
374295 INFO: Including run-time hook 'pyi_rth_setuptools.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks'
374315 INFO: Including run-time hook 'pyi_rth_pkgutil.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks'
374345 INFO: Including run-time hook 'pyi_rth_multiprocessing.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks'
374370 INFO: Including run-time hook 'pyi_rth_cryptography_openssl.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\_pyinstaller_hooks_contrib\\rthooks'
374387 INFO: Including run-time hook 'pyi_rth_pyside6.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks'
374409 INFO: Processing pre-find-module-path hook 'hook-_pyi_rth_utils.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks\\pre_find_module_path'
374429 INFO: Processing standard module hook 'hook-_pyi_rth_utils.py' from '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PyInstaller\\hooks'
374573 INFO: Creating base_library.zip...
374601 INFO: Looking for dynamic libraries
388194 INFO: Extra DLL search directories (AddDllDirectory): ['\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\shiboken6']
388194 INFO: Extra DLL search directories (PATH): ['\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PySide6']
550512 WARNING: Library not found: could not resolve 'fbclient.dll', dependency of '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PySide6\\plugins\\sqldrivers\\qsqlibase.dll'.
550537 WARNING: Library not found: could not resolve 'MIMAPI64.dll', dependency of '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PySide6\\plugins\\sqldrivers\\qsqlmimer.dll'.
550564 WARNING: Library not found: could not resolve 'OCI.dll', dependency of '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PySide6\\plugins\\sqldrivers\\qsqloci.dll'.
550599 WARNING: Library not found: could not resolve 'LIBPQ.dll', dependency of '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PySide6\\plugins\\sqldrivers\\qsqlpsql.dll'.
551377 WARNING: Library not found: could not resolve 'Qt6QuickShapesDesignHelpers.dll', dependency of '\\\\wsl.localhost\\Debian\\home\\hugo_reyes\\dev\\CloudRecoveryAS\\venv-windows\\Lib\\site-packages\\PySide6\\qml\\QtQuick\\Shapes\\DesignHelpers\\qtquickshapesdesignhelpersplugin.dll'.
557267 INFO: Warnings written to C:\Users\Hugo Reyes\AppData\Local\Temp\cloudrestore-build\CloudRestoreAS\warn-CloudRestoreAS.txt
557345 INFO: Graph cross-reference written to C:\Users\Hugo Reyes\AppData\Local\Temp\cloudrestore-build\CloudRestoreAS\xref-CloudRestoreAS.html
557674 INFO: checking PYZ
557674 INFO: Building PYZ because PYZ-00.toc is non existent
557674 INFO: Building PYZ (ZlibArchive) C:\Users\Hugo Reyes\AppData\Local\Temp\cloudrestore-build\CloudRestoreAS\PYZ-00.pyz
558391 INFO: Building PYZ (ZlibArchive) C:\Users\Hugo Reyes\AppData\Local\Temp\cloudrestore-build\CloudRestoreAS\PYZ-00.pyz completed successfully.
558559 INFO: checking PKG
558559 INFO: Building PKG because PKG-00.toc is non existent
558559 INFO: Building PKG (CArchive) CloudRestoreAS.pkg
770535 INFO: Building PKG (CArchive) CloudRestoreAS.pkg completed successfully.
770717 INFO: Bootloader \\wsl.localhost\Debian\home\hugo_reyes\dev\CloudRecoveryAS\venv-windows\Lib\site-packages\PyInstaller\bootloader\Windows-64bit-intel\runw.exe
770717 INFO: checking EXE
770717 INFO: Building EXE because EXE-00.toc is non existent
770717 INFO: Building EXE from EXE-00.toc
770723 INFO: Copying bootloader EXE to \\wsl.localhost\Debian\home\hugo_reyes\dev\CloudRecoveryAS\dist\CloudRestoreAS.exe
770978 INFO: Copying icon to EXE
771258 INFO: Copying 0 resources to EXE
771258 INFO: Embedding manifest in EXE
771481 INFO: Appending PKG archive to EXE
774801 INFO: Fixing EXE headers
1021508 INFO: Building EXE from EXE-00.toc completed successfully.
1021631 INFO: Build complete! The results are available in: \\wsl.localhost\Debian\home\hugo_reyes\dev\CloudRecoveryAS\dist
Build OK: \\wsl.localhost\Debian\home\hugo_reyes\dev\CloudRecoveryAS\dist\CloudRestoreAS.exe
Distribuir solo el .exe; al ejecutar crea config/ automaticamente.
===WINDOWS_BUILD_EXIT=0===

113
build.ps1
View File

@@ -1,72 +1,63 @@
# Script de build para PyInstaller # Build CloudRestoreAS — PyInstaller onefile portable
# Genera un ejecutable único de la aplicación $ErrorActionPreference = "Stop"
$Root = $PSScriptRoot
Write-Host "===============================================" -ForegroundColor Cyan Write-Host "===============================================" -ForegroundColor Cyan
Write-Host "CloudRestoreAS - Build Script" -ForegroundColor Cyan Write-Host "CloudRestoreAS - Build Windows (onefile)" -ForegroundColor Cyan
Write-Host "===============================================" -ForegroundColor Cyan Write-Host "===============================================" -ForegroundColor Cyan
Write-Host ""
# Verificar que PyInstaller esté instalado & "$Root\packaging\scripts\download-bundled-deps.ps1"
Write-Host "Verificando PyInstaller..." -ForegroundColor Yellow
$pyinstallerCheck = pip show pyinstaller 2>$null # Python de Windows (evitar venv creado desde WSL)
if (-not $pyinstallerCheck) { $PythonCandidates = @(
Write-Host "PyInstaller no está instalado. Instalando..." -ForegroundColor Yellow "$env:LOCALAPPDATA\Programs\Python\Python313\python.exe",
pip install pyinstaller "$env:LOCALAPPDATA\Programs\Python\Python312\python.exe",
"$env:LOCALAPPDATA\Programs\Python\Python311\python.exe"
)
$Python = $PythonCandidates | Where-Object { Test-Path $_ } | Select-Object -First 1
if (-not $Python) {
throw "No se encontró Python 3.11+ en Windows. Instale desde python.org"
} }
Write-Host "Python: $Python" -ForegroundColor Gray
# Crear directorio de build $venvDir = Join-Path $Root "venv-windows"
$buildDir = "build" $venvPython = Join-Path $venvDir "Scripts\python.exe"
$distDir = "dist" if (-not (Test-Path $venvPython)) {
if (Test-Path $venvDir) { Remove-Item -Recurse -Force $venvDir }
Write-Host "" & $Python -m venv $venvDir
Write-Host "Limpiando builds anteriores..." -ForegroundColor Yellow
if (Test-Path $buildDir) {
Remove-Item -Recurse -Force $buildDir
}
if (Test-Path $distDir) {
Remove-Item -Recurse -Force $distDir
}
if (Test-Path "*.spec") {
Remove-Item -Force *.spec
} }
& $venvPython -m pip install --upgrade pip -q
& $venvPython -m pip install -r (Join-Path $Root "requirements-windows.txt") -q
Write-Host "" $distDir = Join-Path $Root "dist"
Write-Host "Ejecutando PyInstaller..." -ForegroundColor Yellow $isUncRoot = $Root -like "\\*"
Write-Host "" # PyInstaller no puede usar workpath en UNC (WSL); usar carpeta local en Windows
if ($isUncRoot) {
# Ejecutar PyInstaller $buildDir = Join-Path $env:LOCALAPPDATA "Temp\cloudrestore-build"
pyinstaller --name CloudRestoreAS `
--onefile `
--windowed `
--icon=NONE `
--add-data "app;app" `
--hidden-import PySide6 `
--hidden-import pyodbc `
--hidden-import win32crypt `
--collect-all PySide6 `
runner.py
if ($LASTEXITCODE -eq 0) {
Write-Host ""
Write-Host "===============================================" -ForegroundColor Green
Write-Host "Build completado exitosamente!" -ForegroundColor Green
Write-Host "===============================================" -ForegroundColor Green
Write-Host ""
Write-Host "El ejecutable se encuentra en:" -ForegroundColor Cyan
Write-Host " $PWD\dist\CloudRestoreAS.exe" -ForegroundColor White
Write-Host ""
Write-Host "IMPORTANTE:" -ForegroundColor Yellow
Write-Host " - Copia las carpetas 'data' y 'logs' junto al .exe" -ForegroundColor White
Write-Host " - Asegúrate de tener 7-Zip instalado" -ForegroundColor White
Write-Host " - Asegúrate de tener ODBC Driver 17 for SQL Server" -ForegroundColor White
Write-Host ""
} else { } else {
Write-Host "" $buildDir = Join-Path $Root "build"
Write-Host "===============================================" -ForegroundColor Red }
Write-Host "Error durante el build" -ForegroundColor Red if (Test-Path $buildDir) {
Write-Host "===============================================" -ForegroundColor Red Remove-Item -Recurse -Force $buildDir -ErrorAction SilentlyContinue
Write-Host "" }
New-Item -ItemType Directory -Force -Path $distDir, $buildDir | Out-Null
if (Test-Path (Join-Path $distDir "CloudRestoreAS.exe")) {
Remove-Item -Force (Join-Path $distDir "CloudRestoreAS.exe") -ErrorAction SilentlyContinue
} }
# Pausar para ver resultados Write-Host "Ejecutando PyInstaller (onefile)..." -ForegroundColor Yellow
Read-Host "Presiona Enter para continuar..." Write-Host "Workpath: $buildDir" -ForegroundColor Gray
& $venvPython -m PyInstaller (Join-Path $Root "packaging\CloudRestoreAS.spec") `
--clean --noconfirm `
--distpath $distDir `
--workpath $buildDir
$exe = Join-Path $Root "dist\CloudRestoreAS.exe"
if (Test-Path $exe) {
Write-Host ""
Write-Host "Build OK: $exe" -ForegroundColor Green
Write-Host "Distribuir solo el .exe; al ejecutar crea config/ automaticamente." -ForegroundColor Cyan
} else {
Write-Host "Error: no se genero el ejecutable" -ForegroundColor Red
exit 1
}

43
build.sh Executable file
View File

@@ -0,0 +1,43 @@
#!/usr/bin/env bash
# Build CloudRestoreAS — PyInstaller onefile portable (Linux)
set -euo pipefail
ROOT="$(cd "$(dirname "$0")" && pwd)"
cd "$ROOT"
echo "==============================================="
echo "CloudRestoreAS - Build Linux (onefile)"
echo "==============================================="
bash "$ROOT/packaging/scripts/download-bundled-deps.sh"
VENV="$ROOT/venv-linux"
if [[ ! -x "$VENV/bin/python" ]]; then
rm -rf "$VENV"
python3 -m venv "$VENV"
fi
"$VENV/bin/pip" install --upgrade pip -q
"$VENV/bin/pip" install -r requirements.txt -q
BUILD_DIR="${CLOUDRESTORE_BUILD_DIR:-/tmp/cloudrestore-build}"
DIST_DIR="${CLOUDRESTORE_DIST_DIR:-$ROOT/dist}"
rm -rf "$BUILD_DIR"
mkdir -p "$BUILD_DIR" "$DIST_DIR"
rm -f "$DIST_DIR/CloudRestoreAS"
echo "Ejecutando PyInstaller (onefile)..."
"$VENV/bin/python" -m PyInstaller packaging/CloudRestoreAS.spec \
--clean --noconfirm \
--distpath "$DIST_DIR" \
--workpath "$BUILD_DIR"
BIN="$ROOT/dist/CloudRestoreAS"
if [[ -f "$BIN" ]]; then
chmod +x "$BIN"
echo ""
echo "Build OK: $BIN"
echo "Distribuir solo el binario; al ejecutar crea config/ automaticamente."
else
echo "Error: no se genero el binario" >&2
exit 1
fi

103
install.sh Executable file
View File

@@ -0,0 +1,103 @@
#!/usr/bin/env bash
# Instalador Linux de CloudRestoreAS.
#
# NO instala ni descarga NADA del sistema: el binario es 100% autocontenido
# (Qt/xcb, driver ODBC + Kerberos/OpenSSL, y 7-Zip van embebidos). Este script solo
# coloca el binario, marca permisos, hace el bootstrap de config/, y opcionalmente
# registra el arranque automático (servicio systemd headless o autostart de escritorio).
#
# Uso:
# sudo ./install.sh --service # 24/7 headless vía systemd (recomendado en servidor)
# ./install.sh --desktop # autostart de escritorio (requiere sesión gráfica)
# ./install.sh # solo instala; sin arranque automático
#
# Variables:
# PREFIX=/opt/cloudrestoreas # carpeta destino (default)
# SERVICE_USER=<usuario> # usuario del servicio systemd (default: quien invoca)
set -euo pipefail
MODE="none"
for arg in "$@"; do
case "$arg" in
--service) MODE="service" ;;
--desktop) MODE="desktop" ;;
-h|--help)
grep '^#' "$0" | grep -v '^#!' | sed 's/^# \{0,1\}//'; exit 0 ;;
*) echo "Opción desconocida: $arg" >&2; exit 2 ;;
esac
done
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PREFIX="${PREFIX:-/opt/cloudrestoreas}"
BIN_NAME="CloudRestoreAS"
# Localizar el binario: dist/CloudRestoreAS, dist/CloudRestoreAS-linux, o junto al script.
SRC=""
for cand in \
"$SCRIPT_DIR/dist/$BIN_NAME" \
"$SCRIPT_DIR/dist/${BIN_NAME}-linux" \
"$SCRIPT_DIR/$BIN_NAME" \
"$SCRIPT_DIR/${BIN_NAME}-linux"; do
if [[ -f "$cand" ]]; then SRC="$cand"; break; fi
done
if [[ -z "$SRC" ]]; then
echo "ERROR: no se encontró el binario ($BIN_NAME). Ejecuta ./build.sh primero" >&2
exit 1
fi
echo "==============================================="
echo "CloudRestoreAS - Instalación Linux"
echo " Binario : $SRC"
echo " Destino : $PREFIX"
echo " Modo : $MODE"
echo "==============================================="
mkdir -p "$PREFIX"
install -m 0755 "$SRC" "$PREFIX/$BIN_NAME"
echo "Binario instalado en $PREFIX/$BIN_NAME"
# Bootstrap de config/: el propio binario crea config/, .env y carpetas de trabajo
# al inicio de su arranque. Se corre una vez en modo offscreen (sin display) y se
# corta con timeout; así el usuario ya puede editar config/.env antes de habilitar
# el servicio.
echo "Inicializando config/ (bootstrap)..."
( cd "$PREFIX" && QT_QPA_PLATFORM=offscreen timeout 20 "$PREFIX/$BIN_NAME" --headless >/dev/null 2>&1 || true )
if [[ -f "$PREFIX/config/.env" ]]; then
echo "config/.env creado."
else
echo "NOTA: config/.env se creará en la primera ejecución."
fi
case "$MODE" in
service)
SERVICE_USER="${SERVICE_USER:-${SUDO_USER:-$(id -un)}}"
UNIT_SRC="$SCRIPT_DIR/packaging/linux/cloudrestoreas.service"
UNIT_DST="/etc/systemd/system/cloudrestoreas.service"
if [[ ! -w "$(dirname "$UNIT_DST")" ]]; then
echo "ERROR: se requiere sudo para instalar el servicio systemd" >&2
exit 1
fi
sed -e "s|__USER__|$SERVICE_USER|g" \
-e "s|__WORKDIR__|$PREFIX|g" \
-e "s|__EXEC__|$PREFIX/$BIN_NAME|g" \
"$UNIT_SRC" > "$UNIT_DST"
systemctl daemon-reload
systemctl enable --now cloudrestoreas.service
echo "Servicio systemd instalado y arrancado (usuario: $SERVICE_USER)."
echo " Estado : systemctl status cloudrestoreas"
echo " Logs : journalctl -u cloudrestoreas -f"
;;
desktop)
echo "Modo escritorio: la app registra su autostart .desktop al iniciarse."
echo "Ejecuta '$PREFIX/$BIN_NAME' en tu sesión gráfica."
;;
none)
echo "Instalación sin arranque automático."
echo "Ejecuta: QT_QPA_PLATFORM=offscreen $PREFIX/$BIN_NAME --start-engine --headless"
;;
esac
echo ""
echo "Siguiente paso: edita $PREFIX/config/.env (CLOUDRESTORE_PANEL_*) y reinicia."
[[ "$MODE" == "service" ]] && echo " Tras editar: sudo systemctl restart cloudrestoreas"
echo "Listo."

View File

@@ -0,0 +1,113 @@
# -*- mode: python ; coding: utf-8 -*-
"""PyInstaller spec onefile portable (ejecutar en Windows o Linux según destino)."""
import sys
from pathlib import Path
block_cipher = None
ROOT = Path(SPECPATH).resolve().parent
platform = "windows" if sys.platform == "win32" else "linux"
bundled = ROOT / "packaging" / "bundled" / platform
_assets = ROOT / "packaging" / "assets"
datas = [
(str(ROOT / "packaging" / "templates" / "env.default"), "packaging/templates"),
]
if _assets.is_dir():
for _icon in _assets.iterdir():
if _icon.is_file() and _icon.suffix.lower() in (".png", ".ico"):
datas.append((str(_icon), "packaging/assets"))
binaries = []
seven_dir = bundled / "7zip"
if seven_dir.is_dir():
for item in seven_dir.iterdir():
if item.is_file():
binaries.append((str(item), "bundled/7zip"))
odbc_dir = bundled / "odbc"
if odbc_dir.is_dir():
for item in odbc_dir.rglob("*"):
if not item.is_file():
continue
if item.suffix.lower() in (".msi", ".deb"):
continue
rel = item.relative_to(odbc_dir)
dest = f"bundled/odbc/{rel.parent}".replace("\\", "/")
if str(rel.parent) == ".":
dest = "bundled/odbc"
binaries.append((str(item), dest))
# Libs de sistema Qt (cluster xcb/X11 + EGL) que el plugin xcb carga por dlopen.
# Van JUNTO a las librerías Qt (PySide6/Qt/lib): libQt6XcbQpa.so.6 tiene RPATH
# $ORIGIN, así que resuelve libxcb-cursor.so.0 y el resto en su propio directorio
# sin depender de LD_LIBRARY_PATH. (El stack ODBC va con el driver en bundled/odbc,
# con RPATH=$ORIGIN aplicado por download-bundled-deps.sh.) Solo aplica en Linux.
qt_sys_lib = bundled / "qt" / "lib"
if qt_sys_lib.is_dir():
for item in qt_sys_lib.iterdir():
if item.is_file() and not item.is_symlink():
binaries.append((str(item), "PySide6/Qt/lib"))
hiddenimports = [
"PySide6",
"pyodbc",
"requests",
"paramiko",
"dotenv",
"cryptography.fernet",
]
if platform == "windows":
hiddenimports.append("win32crypt")
from PyInstaller.utils.hooks import collect_all
pyside_datas, pyside_binaries, pyside_hidden = collect_all("PySide6")
datas += pyside_datas
binaries += pyside_binaries
hiddenimports += pyside_hidden
a = Analysis(
[str(ROOT / "runner.py")],
pathex=[str(ROOT)],
binaries=binaries,
datas=datas,
hiddenimports=hiddenimports,
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
# Icono del ejecutable (solo aplica en Windows; en Linux se ignora sin error).
_icon_file = ROOT / "packaging" / "assets" / "tray-icon.ico"
_icon = str(_icon_file) if _icon_file.is_file() else None
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name="CloudRestoreAS",
icon=_icon,
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)

40
packaging/LEEME.txt Normal file
View File

@@ -0,0 +1,40 @@
CloudRestoreAS — despliegue portable
=====================================
El ejecutable es AUTOCONTENIDO: no requiere Python, ni 7-Zip, ni driver ODBC, ni
librerias Qt instaladas en el equipo destino. Todo va embebido dentro del binario.
Windows: copie CloudRestoreAS.exe a la carpeta deseada.
Linux: copie CloudRestoreAS y ejecute chmod +x CloudRestoreAS si hace falta.
1. Ejecute el programa (doble clic o ./CloudRestoreAS).
- Se abre la ventana principal y un icono en la bandeja del sistema.
- El motor arranca automaticamente (CLOUDRESTORE_AUTO_START=true por defecto).
2. Se crean automaticamente: config/, Entrada/, Procesados/, Fallados/, Temp/
3. Edite config/.env con la URL, token e instancia del servicio de bases.
4. Reinicie la aplicacion si cambio la configuracion.
Cerrar la ventana (X) minimiza a la bandeja; la app sigue en ejecucion.
Para salir por completo: clic derecho en el icono de bandeja -> Salir,
o menu Archivo -> Salir.
-------------------------------------
Linux en SERVIDOR SIN ESCRITORIO (headless)
-------------------------------------
En un servidor sin pantalla (sin DISPLAY), la app arranca automaticamente en modo
"offscreen": no muestra ventana pero el motor de restauracion trabaja igual. No hay
que instalar nada del sistema.
Opcion A - servicio 24/7 (recomendado en servidor SQL):
sudo ./install.sh --service
(instala una unit systemd que corre el binario con --start-engine --headless;
ver estado: systemctl status cloudrestoreas ; logs: journalctl -u cloudrestoreas -f)
Opcion B - manual:
QT_QPA_PLATFORM=offscreen ./CloudRestoreAS --start-engine --headless
Opcion C - escritorio Linux (con pantalla):
./install.sh --desktop (registra autostart .desktop; requiere sesion grafica)
El servidor SQL destino y su carpeta de datos (data_folder) se toman del PANEL;
con SQL Server sobre Linux use rutas POSIX (p. ej. /var/opt/mssql/data).

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1,9 @@
{
"seven_zip": "26.01",
"seven_zip_windows_extra": "https://github.com/ip7z/7zip/releases/download/26.01/7z2601-extra.7z",
"seven_zip_windows_7zr": "https://github.com/ip7z/7zip/releases/download/26.01/7zr.exe",
"seven_zip_linux_x64": "https://github.com/ip7z/7zip/releases/download/26.01/7z2601-linux-x64.tar.xz",
"msodbcsql18_msi": "https://go.microsoft.com/fwlink/?linkid=2281204",
"msodbcsql18_deb": "https://packages.microsoft.com/ubuntu/22.04/prod/pool/main/m/msodbcsql18/msodbcsql18_18.5.1.1-1_amd64.deb",
"unixodbc_deb": "http://archive.ubuntu.com/ubuntu/pool/main/u/unixodbc/unixodbc_2.3.12-1ubuntu0.24.04.1_amd64.deb"
}

View File

@@ -0,0 +1,19 @@
[Unit]
Description=CloudRestoreAS - Restauración automática SQL Server
After=network-online.target mssql-server.service
Wants=network-online.target
[Service]
Type=simple
# __USER__ y __EXEC__ los sustituye install.sh al instalar.
User=__USER__
WorkingDirectory=__WORKDIR__
# Servidor headless: plataforma Qt offscreen (sin display) y motor auto-inicio.
Environment=QT_QPA_PLATFORM=offscreen
ExecStart=__EXEC__ --start-engine --headless
Restart=always
RestartSec=5
# El binario es autocontenido (Qt, ODBC, 7-Zip embebidos): no necesita libs del sistema.
[Install]
WantedBy=multi-user.target

6
packaging/scripts/build-all.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env bash
# Wrapper de compatibilidad: el orquestador canónico vive en la raíz del repo.
# Ejecuta ./build-all.sh (desde WSL) para generar Windows + Linux + paquetes.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
exec bash "$ROOT/build-all.sh" "$@"

View File

@@ -0,0 +1,28 @@
#!/usr/bin/env bash
# Build Linux onefile dentro de Docker con dependencias Qt del sistema.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
docker run --rm \
-v "$ROOT:/app" \
-w /app \
ubuntu:22.04 bash -c '
set -e
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
# Herramientas de build + libs de sistema para que PyInstaller EMBEBA el cierre
# completo de dependencias (plugin Qt xcb y driver MS ODBC). Cinturón y tirantes
# junto con el bundle explícito de download-bundled-deps.sh.
apt-get install -y -qq \
python3 python3-venv python3-pip curl dpkg-dev binutils apt-utils patchelf \
unixodbc libodbcinst2 libltdl7 libkrb5-3 libgssapi-krb5-2 libssl3 \
libglib2.0-0 libdbus-1-3 libfontconfig1 libfreetype6 \
libxkbcommon0 libxkbcommon-x11-0 libegl1 libgl1 libglvnd0 \
libx11-6 libx11-xcb1 libxext6 libxrender1 libxi6 libxfixes3 libxtst6 \
libxcomposite1 libxdamage1 libxrandr2 libxkbfile1 \
libxcb1 libxcb-cursor0 libxcb-icccm4 libxcb-util1 libxcb-image0 \
libxcb-keysyms1 libxcb-render-util0 libxcb-render0 libxcb-shape0 \
libxcb-xkb1 libxcb-glx0 libxcb-randr0 libxcb-shm0 libxcb-sync1 \
> /dev/null
chmod +x build.sh packaging/scripts/download-bundled-deps.sh
./build.sh
'

View File

@@ -0,0 +1,93 @@
# Descarga 7-Zip Extra y ODBC para empaquetar en CloudRestoreAS.exe
$ErrorActionPreference = "Stop"
$Root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
$Versions = Get-Content (Join-Path $Root "packaging\bundled-versions.json") | ConvertFrom-Json
$Bundled = Join-Path $Root "packaging\bundled\windows"
$Seven = Join-Path $Bundled "7zip"
$Odbc = Join-Path $Bundled "odbc"
New-Item -ItemType Directory -Force -Path $Seven, $Odbc | Out-Null
Write-Host "==> 7-Zip Extra ($($Versions.seven_zip))" -ForegroundColor Cyan
if (-not (Test-Path (Join-Path $Seven "7z.exe"))) {
$tmp = Join-Path $env:TEMP "cloudrestore-7z-$(Get-Random)"
New-Item -ItemType Directory -Path $tmp | Out-Null
$sevenR = Join-Path $tmp "7zr.exe"
$extra = Join-Path $tmp "7z-extra.7z"
Invoke-WebRequest -Uri $Versions.seven_zip_windows_7zr -OutFile $sevenR
Invoke-WebRequest -Uri $Versions.seven_zip_windows_extra -OutFile $extra
& $sevenR x $extra ("-o" + $Seven) -y | Out-Null
$x64Exe = Join-Path $Seven "x64\7za.exe"
$x64Dll = Join-Path $Seven "x64\7za.dll"
if (Test-Path $x64Exe) {
Copy-Item $x64Exe (Join-Path $Seven "7z.exe") -Force
Copy-Item $x64Exe (Join-Path $Seven "7za.exe") -Force
}
if (Test-Path $x64Dll) {
Copy-Item $x64Dll (Join-Path $Seven "7z.dll") -Force
Copy-Item $x64Dll (Join-Path $Seven "7za.dll") -Force
}
Remove-Item -Recurse -Force $tmp
Write-Host " 7z.exe en $Seven"
} else {
Write-Host " 7-Zip ya existe"
}
Write-Host "==> ODBC Driver 18 (extraer DLLs del MSI)" -ForegroundColor Cyan
$Msi = Join-Path $Odbc "msodbcsql18.msi"
if (-not (Test-Path (Join-Path $Odbc "msodbcsql18.dll"))) {
if (-not (Test-Path $Msi)) {
Invoke-WebRequest -Uri $Versions.msodbcsql18_msi -OutFile $Msi
}
$extract = Join-Path $Odbc "_msi_extract"
if (Test-Path $extract) { Remove-Item -Recurse -Force $extract }
New-Item -ItemType Directory -Path $extract | Out-Null
$msiArgs = @("/a", "`"$Msi`"", "/qn", "TARGETDIR=`"$extract`"", "IACCEPTMSODBCSQLLICENSETERMS=YES")
$proc = Start-Process msiexec.exe -ArgumentList $msiArgs -Wait -PassThru
if ($proc.ExitCode -ne 0) {
Write-Host " msiexec /a fallo (codigo $($proc.ExitCode)); intentando instalacion silenciosa local..." -ForegroundColor Yellow
$local = Join-Path $Odbc "driver_install"
New-Item -ItemType Directory -Force -Path $local | Out-Null
Start-Process msiexec.exe -ArgumentList "/i", "`"$Msi`"", "/qn", "IACCEPTMSODBCSQLLICENSETERMS=YES", "ADDLOCAL=ALL" -Wait
$sysOdbc = "${env:ProgramFiles}\Microsoft SQL Server\Client SDK\ODBC\170\Tools\Binn"
if (Test-Path $sysOdbc) {
Get-ChildItem $sysOdbc -Filter "*.dll" | Copy-Item -Destination $Odbc -Force
}
$driverDir = "${env:ProgramFiles}\Microsoft SQL Server\Client SDK\ODBC\180\Tools\Binn"
if (Test-Path $driverDir) {
Get-ChildItem $driverDir -Filter "*.dll" | Copy-Item -Destination $Odbc -Force
}
foreach ($dll in @("msodbcsql18.dll", "msodbcsql17.dll")) {
$sys = Join-Path $env:SystemRoot "System32\$dll"
if (Test-Path $sys) { Copy-Item $sys $Odbc -Force }
}
} else {
Get-ChildItem -Path $extract -Recurse -Filter "msodbcsql*.dll" | ForEach-Object {
Copy-Item $_.FullName $Odbc -Force
}
Get-ChildItem -Path $extract -Recurse -Filter "*.dll" | Where-Object {
$_.Name -match "mso|odbc|ssl|crypto|bcp"
} | ForEach-Object { Copy-Item $_.FullName $Odbc -Force -ErrorAction SilentlyContinue }
Remove-Item -Recurse -Force $extract -ErrorAction SilentlyContinue
}
Write-Host " DLLs ODBC en $Odbc"
} else {
Write-Host " ODBC ya existe"
}
if (-not (Test-Path (Join-Path $Odbc "odbcinst.ini"))) {
@"
[ODBC Driver 18 for SQL Server]
Description=Microsoft ODBC Driver 18 for SQL Server
Driver=msodbcsql18.dll
UsageCount=1
[ODBC Driver 17 for SQL Server]
Description=Microsoft ODBC Driver 17 for SQL Server
Driver=msodbcsql17.dll
UsageCount=1
"@ | Set-Content (Join-Path $Odbc "odbcinst.ini") -Encoding ASCII
"[ODBC Data Sources]" | Set-Content (Join-Path $Odbc "odbc.ini") -Encoding ASCII
}
Write-Host "Listo: $Bundled" -ForegroundColor Green

View File

@@ -0,0 +1,156 @@
#!/usr/bin/env bash
# Descarga y EMBEBE (build-time) todo lo que el binario Linux necesita en runtime:
# - 7zz (7-Zip para Linux)
# - Driver MS ODBC 18 + unixODBC
# - Libs de sistema de Qt (cluster xcb/X11 + EGL) y del stack ODBC
# (libltdl, Kerberos/GSSAPI, OpenSSL) que PySide6/pyodbc cargan por dlopen
# y que NO vienen dentro del wheel.
# Objetivo: que el onefile corra en un servidor Linux "pelado" sin instalar nada.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
VERSIONS="$ROOT/packaging/bundled-versions.json"
BUNDLED="$ROOT/packaging/bundled/linux"
SEVEN="$BUNDLED/7zip"
ODBC="$BUNDLED/odbc/lib"
QTLIB="$BUNDLED/qt/lib" # libs de sistema extra a embeder en la raíz de _MEIPASS
mkdir -p "$SEVEN" "$ODBC" "$QTLIB"
read_url() {
python3 -c "import json; print(json.load(open('$VERSIONS'))['$1'])"
}
# Descarga uno o más paquetes .deb (y NO sus dependencias), extrae sus .so y los
# copia al directorio destino con el NOMBRE DE SONAME (lo que busca el loader:
# libfoo.so.N), dereferenciando symlinks para no dejar enlaces colgantes.
# Requiere apt-get/dpkg-deb.
fetch_pkg_libs() {
local destdir="$1"; shift
if ! command -v apt-get >/dev/null 2>&1; then
echo " (apt-get no disponible; se omite descarga de $* — el entorno de build debe proveerlas)"
return 0
fi
local tmp; tmp=$(mktemp -d)
(
cd "$tmp"
apt-get download "$@" >/dev/null 2>&1 || {
# Reintento tras refrescar índices (build env con red).
apt-get update -qq >/dev/null 2>&1 || true
apt-get download "$@" >/dev/null 2>&1 || true
}
for deb in *.deb; do
[[ -f "$deb" ]] || continue
dpkg-deb -x "$deb" x
done
[[ -d x ]] || exit 0
# 1) Symlinks SONAME del propio paquete (libfoo.so.N -> libfoo.so.N.M.P):
# copiar el contenido REAL bajo el nombre del symlink (= SONAME exacto).
while IFS= read -r link; do
cp -Lf "$link" "$destdir/$(basename "$link")" 2>/dev/null || true
done < <(find x -type l -name '*.so.*')
# 2) Archivos reales sin symlink: derivar SONAME por truncación de versión
# (libfoo.so.N.M.P -> libfoo.so.N) para no dejarlos con nombre versionado.
while IFS= read -r f; do
local soname
soname=$(basename "$f" | sed -E 's/(\.so\.[0-9]+)\..*/\1/')
[[ -e "$destdir/$soname" ]] || cp -Lf "$f" "$destdir/$soname" 2>/dev/null || true
done < <(find x -type f -name '*.so.*')
)
rm -rf "$tmp"
}
echo "==> 7-Zip Linux (7zz)"
LINUX_7Z_URL="$(read_url seven_zip_linux_x64)"
if [[ ! -f "$SEVEN/7zz" ]]; then
TMP=$(mktemp -d)
curl -fsSL -o "$TMP/7z.tar.xz" "$LINUX_7Z_URL"
tar -xJf "$TMP/7z.tar.xz" -C "$TMP"
cp "$TMP/7zz" "$SEVEN/7zz"
chmod +x "$SEVEN/7zz"
rm -rf "$TMP"
echo " 7zz instalado"
else
echo " 7zz ya existe"
fi
echo "==> ODBC Driver 18 + unixODBC"
MSODBC_DEB="$(read_url msodbcsql18_deb)"
if [[ -z "$(ls -A "$ODBC" 2>/dev/null || true)" ]]; then
TMP=$(mktemp -d)
cd "$TMP"
curl -fsSL -o msodbcsql18.deb "$MSODBC_DEB"
if command -v apt-get >/dev/null 2>&1; then
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq || true
apt-get install -y -qq unixodbc dpkg-dev > /dev/null 2>&1 || true
if dpkg -l unixodbc 2>/dev/null | grep -q ^ii; then
# -L: dereferencia symlinks para no dejar enlaces colgantes en el destino.
find /usr/lib -name 'libodbc*.so*' -exec cp -Lf {} "$ODBC/" \; 2>/dev/null || true
find /usr/lib -name 'libodbcinst*.so*' -exec cp -Lf {} "$ODBC/" \; 2>/dev/null || true
fi
fi
dpkg-deb -x msodbcsql18.deb msodbcsql
# Copiar SOLO el archivo real versionado del driver (evita el symlink -> /opt).
find msodbcsql -type f -name 'libmsodbcsql-18*.so*' -exec cp -Lf {} "$ODBC/" \;
cd "$ROOT"
rm -rf "$TMP"
# Stack de dependencias del driver MS ODBC 18, JUNTO al driver, para que un
# servidor pelado (sin unixODBC/krb5/openssl) pueda cargarlo. El driver se
# patchelf-ea a RPATH=$ORIGIN (más abajo) para resolverlas en su propio dir.
ODBC_DEP_PKGS=(
libltdl7 libkrb5-3 libgssapi-krb5-2 libk5crypto3 libkrb5support0
libcom-err2 libkeyutils1 libssl3 libodbc2 libodbcinst2
)
fetch_pkg_libs "$ODBC" "${ODBC_DEP_PKGS[@]}" || true
fetch_pkg_libs "$ODBC" unixodbc || true
echo " Bibliotecas ODBC + deps en $ODBC"
else
echo " ODBC ya existe"
fi
# RPATH=$ORIGIN en TODAS las .so del cluster ODBC: así el driver y sus deps
# (libkrb5 -> libk5crypto/libkrb5support/libkeyutils, libgssapi, libltdl,
# libodbcinst, openssl) se resuelven entre sí en su propio directorio una vez
# copiados a config/odbc/lib, sin depender de LD_LIBRARY_PATH.
if command -v patchelf >/dev/null 2>&1; then
for so in "$ODBC"/*.so*; do
[[ -f "$so" && ! -L "$so" ]] || continue
patchelf --set-rpath '$ORIGIN' "$so" 2>/dev/null || true
done
echo " RPATH=\$ORIGIN aplicado a todo el cluster ODBC"
else
echo " ADVERTENCIA: patchelf no disponible; el driver ODBC podría no resolver" >&2
echo " sus deps en un servidor sin unixODBC/krb5. Instálalo en el entorno de build." >&2
fi
echo "==> Libs de sistema Qt (cluster xcb/X11 + EGL) para la GUI con display"
if [[ -z "$(ls -A "$QTLIB" 2>/dev/null || true)" ]]; then
# Plataforma Qt xcb + su cierre X11/xcb + EGL. Van junto a las libs Qt en el
# onefile (PySide6/Qt/lib) para que libQt6XcbQpa las encuentre por $ORIGIN.
QT_XCB_PKGS=(
libxcb-cursor0 libxcb-icccm4 libxcb-util1 libxcb-image0 libxcb-keysyms1
libxcb-render-util0 libxcb-render0 libxcb-shape0 libxcb-xkb1
libxkbcommon-x11-0 libxkbcommon0 libxtst6 libxcomposite1 libxdamage1
libxrandr2 libxkbfile1 libxcb1 libx11-6 libx11-xcb1 libxext6 libxrender1
libxi6 libxfixes3 libxcb-glx0 libxcb-randr0 libxcb-shm0 libxcb-sync1
libxcb-present0 libxcb-dri2-0 libxcb-dri3-0 libxcb-xfixes0 libxau6
libxdmcp6 libbsd0 libsm6 libice6
)
QT_GL_PKGS=(libglvnd0 libegl1 libgl1 libopengl0 libglx0 libgles2)
fetch_pkg_libs "$QTLIB" "${QT_XCB_PKGS[@]}"
fetch_pkg_libs "$QTLIB" "${QT_GL_PKGS[@]}"
COUNT=$(find "$QTLIB" -type f -name '*.so*' 2>/dev/null | wc -l)
echo " $COUNT libs Qt embebidas en $QTLIB"
if [[ "$COUNT" -eq 0 ]]; then
echo " ADVERTENCIA: no se embebió ninguna lib Qt. El entorno de build" >&2
echo " (docker-build-linux.sh) debe traerlas para que PyInstaller las incluya." >&2
fi
else
echo " Libs Qt ya existen ($(find "$QTLIB" -type f -name '*.so*' | wc -l) archivos)"
fi
echo "Listo: $BUNDLED"

View File

@@ -0,0 +1,155 @@
#!/usr/bin/env python3
"""Genera el icono de marca de CloudRestoreAS sin dependencias externas.
Dibuja una "C" (anillo con apertura a la derecha) sobre un cuadro redondeado azul
(#0078D7), con anti-aliasing por supersampling. Codifica los PNG a mano (zlib + CRC)
y ensambla un .ico multi-tamano (entradas PNG, soportadas por Windows Vista+).
Salida:
packaging/assets/tray-icon.png (256x256)
packaging/assets/tray-icon.ico (16,32,48,64,128,256)
Uso: python3 packaging/scripts/make-icons.py
Solo usa la libreria estandar; corre con cualquier Python 3.x.
"""
import math
import struct
import zlib
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent.parent
ASSETS = ROOT / "packaging" / "assets"
ICO_SIZES = (16, 32, 48, 64, 128, 256)
PNG_SIZE = 256
# Colores
BG = (0, 120, 215) # azul #0078D7
FG = (255, 255, 255) # blanco
SS = 4 # factor de supersampling (anti-aliasing)
# Geometria (fracciones del lado)
CORNER = 0.22 # radio de esquina del cuadro
RING_OUTER = 0.34 # radio externo de la "C"
RING_INNER = 0.20 # radio interno de la "C"
GAP_HALF_DEG = 40.0 # medio angulo de la apertura de la "C" (a la derecha)
def _render_rgba(size: int) -> bytes:
"""Renderiza el icono a un buffer RGBA de size*size (4 bytes por pixel)."""
cx = cy = size / 2.0
half = size / 2.0
rc = CORNER * size
inner = RING_INNER * size
outer = RING_OUTER * size
cos_gap = math.cos(math.radians(GAP_HALF_DEG))
inv = 1.0 / (SS * SS)
out = bytearray(size * size * 4)
k = 0
for py in range(size):
for px in range(size):
r_acc = g_acc = b_acc = a_acc = 0.0
for sy in range(SS):
y = py + (sy + 0.5) / SS
dyc = y - cy
for sx in range(SS):
x = px + (sx + 0.5) / SS
# Cuadro redondeado (SDF): dentro si d <= 0
bx = abs(x - cx) - (half - rc)
by = abs(y - cy) - (half - rc)
mx = bx if bx > 0.0 else 0.0
my = by if by > 0.0 else 0.0
d = math.hypot(mx, my) + min(max(bx, by), 0.0) - rc
if d > 0.0:
continue # fuera del cuadro -> transparente
# Dentro del cuadro: base azul
dxc = x - cx
rr = math.hypot(dxc, dyc)
in_ring = inner <= rr <= outer
in_gap = dxc > rr * cos_gap # apertura a la derecha
if in_ring and not in_gap:
r_acc += FG[0]; g_acc += FG[1]; b_acc += FG[2]
else:
r_acc += BG[0]; g_acc += BG[1]; b_acc += BG[2]
a_acc += 255.0
out[k] = int(r_acc * inv + 0.5)
out[k + 1] = int(g_acc * inv + 0.5)
out[k + 2] = int(b_acc * inv + 0.5)
out[k + 3] = int(a_acc * inv + 0.5)
k += 4
return bytes(out)
def _png_chunk(tag: bytes, data: bytes) -> bytes:
return (
struct.pack(">I", len(data))
+ tag
+ data
+ struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF)
)
def _encode_png(size: int, rgba: bytes) -> bytes:
"""Codifica un buffer RGBA a bytes PNG (8 bits, color type 6)."""
ihdr = struct.pack(">IIBBBBB", size, size, 8, 6, 0, 0, 0)
stride = size * 4
raw = bytearray()
for row in range(size):
raw.append(0) # filtro None por scanline
raw += rgba[row * stride:(row + 1) * stride]
idat = zlib.compress(bytes(raw), 9)
return (
b"\x89PNG\r\n\x1a\n"
+ _png_chunk(b"IHDR", ihdr)
+ _png_chunk(b"IDAT", idat)
+ _png_chunk(b"IEND", b"")
)
def _build_ico(pngs: dict) -> bytes:
"""Ensambla un .ico con entradas PNG. pngs: {size: png_bytes}."""
sizes = sorted(pngs)
count = len(sizes)
header = struct.pack("<HHH", 0, 1, count)
offset = 6 + count * 16
entries = bytearray()
body = bytearray()
for size in sizes:
data = pngs[size]
entries += struct.pack(
"<BBBBHHII",
size if size < 256 else 0, # ancho (0 == 256)
size if size < 256 else 0, # alto
0, # paleta
0, # reservado
1, # planos
32, # bits por pixel
len(data),
offset,
)
body += data
offset += len(data)
return bytes(header + entries + body)
def main() -> None:
ASSETS.mkdir(parents=True, exist_ok=True)
pngs = {}
for size in ICO_SIZES:
rgba = _render_rgba(size)
pngs[size] = _encode_png(size, rgba)
print(f" render {size}x{size} -> {len(pngs[size])} bytes PNG")
png_path = ASSETS / "tray-icon.png"
png_path.write_bytes(pngs[PNG_SIZE] if PNG_SIZE in pngs else _encode_png(PNG_SIZE, _render_rgba(PNG_SIZE)))
print(f"OK {png_path}")
ico_path = ASSETS / "tray-icon.ico"
ico_path.write_bytes(_build_ico(pngs))
print(f"OK {ico_path} ({len(ICO_SIZES)} tamanos)")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,47 @@
#!/usr/bin/env bash
# Empaqueta artefactos en dist/release/
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
RELEASE="$ROOT/dist/release"
mkdir -p "$RELEASE"
[[ -f "$ROOT/dist/CloudRestoreAS.exe" ]] && cp -f "$ROOT/dist/CloudRestoreAS.exe" "$RELEASE/"
[[ -f "$ROOT/dist/CloudRestoreAS" ]] && cp -f "$ROOT/dist/CloudRestoreAS" "$RELEASE/CloudRestoreAS-linux" && chmod +x "$RELEASE/CloudRestoreAS-linux"
cp -f "$ROOT/packaging/LEEME.txt" "$RELEASE/"
# Paquete Windows: carpeta raíz CloudRestoreAS/ con .exe + install.ps1 + LEEME
# (paridad con el .tar.gz de Linux).
if [[ -f "$RELEASE/CloudRestoreAS.exe" ]]; then
ROOT="$ROOT" RELEASE="$RELEASE" python3 <<'PY'
import os, zipfile
from pathlib import Path
root = Path(os.environ["ROOT"]); release = Path(os.environ["RELEASE"])
items = [(release / "CloudRestoreAS.exe", "CloudRestoreAS/CloudRestoreAS.exe"),
(root / "packaging" / "LEEME.txt", "CloudRestoreAS/LEEME.txt")]
if (root / "install.ps1").exists():
items.append((root / "install.ps1", "CloudRestoreAS/install.ps1"))
with zipfile.ZipFile(release / "CloudRestoreAS-win.zip", "w", zipfile.ZIP_DEFLATED) as z:
for src, arc in items:
z.write(src, arc)
print(" zip Windows:", release / "CloudRestoreAS-win.zip")
PY
fi
if [[ -f "$RELEASE/CloudRestoreAS-linux" ]]; then
# Paquete de despliegue completo: binario + instalador + unit systemd + LEEME,
# bajo una carpeta raíz para que extraiga limpio. install.sh encuentra el binario
# (CloudRestoreAS-linux) y la unit (packaging/linux/) relativos a su ubicación.
STAGE="$RELEASE/CloudRestoreAS-linux"
PKG="$RELEASE/pkg-linux/CloudRestoreAS"
rm -rf "$RELEASE/pkg-linux"
mkdir -p "$PKG/packaging/linux"
cp -f "$STAGE" "$PKG/CloudRestoreAS-linux"; chmod +x "$PKG/CloudRestoreAS-linux"
cp -f "$ROOT/install.sh" "$PKG/install.sh"; chmod +x "$PKG/install.sh"
cp -f "$ROOT/packaging/linux/cloudrestoreas.service" "$PKG/packaging/linux/"
cp -f "$ROOT/packaging/LEEME.txt" "$PKG/LEEME.txt"
tar czf "$RELEASE/CloudRestoreAS-linux.tar.gz" -C "$RELEASE/pkg-linux" CloudRestoreAS
rm -rf "$RELEASE/pkg-linux"
fi
echo "Release en: $RELEASE"
ls -lh "$RELEASE"

View File

@@ -0,0 +1,19 @@
# CloudRestoreAS — configuración local (editar y reiniciar la app)
# Arranca el motor al abrir. La ventana SIEMPRE se muestra al iniciar; cerrar (X) la
# minimiza a la bandeja del sistema (START_MINIMIZED ya no oculta la ventana).
CLOUDRESTORE_AUTO_START=true
CLOUDRESTORE_START_MINIMIZED=false
CLOUDRESTORE_REGISTER_AUTOSTART=true
# Servicio PANEL_BASES_ANEXO24
CLOUDRESTORE_PANEL_API_URL=
CLOUDRESTORE_PANEL_API_TOKEN=
CLOUDRESTORE_PANEL_INSTANCE_KEY=
CLOUDRESTORE_PANEL_VERIFY_SSL=false
# Carpetas (se crean automáticamente junto al ejecutable)
CLOUDRESTORE_INPUT_FOLDER={APP_DIR}/Entrada
CLOUDRESTORE_PROCESSED_FOLDER={APP_DIR}/Procesados
CLOUDRESTORE_FAILED_FOLDER={APP_DIR}/Fallados
CLOUDRESTORE_EXTRACT_FOLDER={APP_DIR}/Temp

2
requirements-windows.txt Normal file
View File

@@ -0,0 +1,2 @@
-r requirements.txt
pywin32>=306

View File

@@ -1,21 +1,13 @@
# CloudRestoreAS - Dependencias # CloudRestoreAS - Dependencias base (Windows y Linux)
# PySide6 para la interfaz gráfica
PySide6>=6.6.0 PySide6>=6.6.0
# pyodbc para SQL Server
pyodbc>=5.0.0 pyodbc>=5.0.0
# pywin32 para DPAPI (cifrado de passwords)
pywin32>=306
# requests para consultar al PANEL el servidor de restauración de cada base
requests>=2.31.0 requests>=2.31.0
# paramiko para transferir el .bak por SFTP/SSH a los servidores SQL externos
paramiko>=3.4.0 paramiko>=3.4.0
python-dotenv>=1.0.0
cryptography>=42.0.0
# Para empaquetado (opcional) # Empaquetado (opcional, solo build)
pyinstaller>=6.0.0 pyinstaller>=6.0.0
# Pruebas (opcional) # Pruebas (opcional)

173
runner.py
View File

@@ -1,53 +1,186 @@
"""Punto de entrada principal de la aplicación.""" """Punto de entrada principal de la aplicación."""
import argparse
import os
import sys import sys
import traceback
from pathlib import Path from pathlib import Path
# Agregar el directorio raíz al path ROOT_DIR = Path(__file__).parent.resolve()
ROOT_DIR = Path(__file__).parent if not getattr(sys, "frozen", False):
sys.path.insert(0, str(ROOT_DIR)) sys.path.insert(0, str(ROOT_DIR))
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(description="CloudRestoreAS")
parser.add_argument(
"--minimized",
action="store_true",
help="Inicia minimizado en la bandeja del sistema",
)
parser.add_argument(
"--start-engine",
action="store_true",
help="Inicia el motor de restauración automáticamente",
)
parser.add_argument(
"--headless",
action="store_true",
help="Fuerza el modo sin interfaz (Qt offscreen) para servidores sin display",
)
return parser.parse_args(argv)
def _ensure_qt_platform(headless: bool = False) -> str:
"""
Selecciona el plugin de plataforma Qt en Linux. En un servidor headless (sin
DISPLAY/WAYLAND_DISPLAY) o con --headless, usa 'offscreen' para que la app
arranque y el motor trabaje sin X; con display usa el default ('xcb'). No toca
nada si el usuario ya fijó QT_QPA_PLATFORM, ni en Windows/macOS.
Devuelve la plataforma forzada ("offscreen") o "" si se deja el default.
"""
if sys.platform in ("win32", "darwin"):
return ""
if os.environ.get("QT_QPA_PLATFORM"):
return os.environ["QT_QPA_PLATFORM"]
has_display = bool(os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY"))
if headless or not has_display:
os.environ["QT_QPA_PLATFORM"] = "offscreen"
return "offscreen"
return ""
def _crash_log_targets() -> list[Path]:
"""Ubicaciones candidatas para el crash log, de más a menos accesible."""
targets: list[Path] = []
try:
targets.append(Path(sys.executable).resolve().parent / "CloudRestoreAS-crash.log")
except Exception:
pass
base = os.environ.get("LOCALAPPDATA") or os.environ.get("APPDATA") or os.environ.get("HOME")
if base:
targets.append(Path(base) / "CloudRestoreAS" / "crash.log")
import tempfile
targets.append(Path(tempfile.gettempdir()) / "CloudRestoreAS-crash.log")
return targets
def _write_crash_log(text: str) -> Path | None:
"""Escribe el detalle del crash en la primera ubicación escribible."""
for path in _crash_log_targets():
try:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text, encoding="utf-8")
return path
except Exception:
continue
return None
def _show_fatal(exc: BaseException) -> None:
"""Hace VISIBLE un fallo de arranque: crash log + diálogo (o stderr)."""
tb = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))
detail = f"CloudRestoreAS no pudo iniciar.\n\n{type(exc).__name__}: {exc}\n\n{tb}"
crash_path = _write_crash_log(detail)
try:
sys.stderr.write(detail)
except Exception:
pass
try:
# Sin display, forzar offscreen para que crear la QApplication no falle
# también aquí (el diálogo no se verá, pero el crash log ya quedó escrito).
_ensure_qt_platform()
from PySide6.QtWidgets import QApplication, QMessageBox
app = QApplication.instance() or QApplication(sys.argv)
box = QMessageBox()
box.setIcon(QMessageBox.Icon.Critical)
box.setWindowTitle("CloudRestoreAS — Error al iniciar")
box.setText("La aplicación no pudo iniciar.")
info = str(exc) or type(exc).__name__
if crash_path:
info += f"\n\nDetalle guardado en:\n{crash_path}"
box.setInformativeText(info)
box.setDetailedText(tb)
box.exec()
except Exception:
# Sin GUI disponible: el crash log y stderr ya quedaron escritos.
pass
def _run() -> int:
"""Arranque real. Los imports están diferidos para que main() capture cualquier fallo."""
# Bootstrap y ODBC antes de importar módulos que usan pyodbc.
from app.config.bootstrap import ensure_runtime_layout
from app.config.odbc_setup import configure_odbc_environment
ensure_runtime_layout()
configure_odbc_environment()
from PySide6.QtWidgets import QApplication from PySide6.QtWidgets import QApplication
from PySide6.QtCore import Qt from PySide6.QtCore import Qt
from app.config import get_launch_options, initialize_env, is_panel_configured
from app.ui.main_window import MainWindow from app.ui.main_window import MainWindow
from app.ui.tray_assets import load_tray_icon
from app.utils.logger import app_logger from app.utils.logger import app_logger
from app.constants import DATA_DIR, LOGS_DIR
args = parse_args()
qt_platform = _ensure_qt_platform(headless=args.headless)
launch = get_launch_options()
initialize_env()
def main(): panel_ok = is_panel_configured()
"""Función principal.""" start_engine = args.start_engine or launch.start_engine
# Crear directorios necesarios
DATA_DIR.mkdir(parents=True, exist_ok=True)
LOGS_DIR.mkdir(parents=True, exist_ok=True)
app_logger.info("=" * 80) app_logger.info("=" * 80)
app_logger.info("CloudRestoreAS - Iniciando aplicación") app_logger.info("CloudRestoreAS - Iniciando aplicación")
app_logger.info(f"Directorio app: {ROOT_DIR}")
if qt_platform == "offscreen":
app_logger.info("Modo: sin display → plataforma Qt 'offscreen' (motor headless)")
else:
app_logger.info("Modo: ventana siempre visible; cerrar minimiza a bandeja")
if start_engine:
app_logger.info("Modo: motor auto-inicio")
if not panel_ok:
app_logger.warning(
"Servicio PANEL_BASES_ANEXO24 no configurado en config/.env — "
"complete CLOUDRESTORE_PANEL_* para enrutamiento automático."
)
app_logger.info("=" * 80) app_logger.info("=" * 80)
# Configurar aplicación Qt
app = QApplication(sys.argv) app = QApplication(sys.argv)
app.setApplicationName("CloudRestoreAS") app.setApplicationName("CloudRestoreAS")
app.setOrganizationName("Aduanasoft") app.setOrganizationName("Aduanasoft")
# Habilitar high DPI scaling
app.setAttribute(Qt.ApplicationAttribute.AA_EnableHighDpiScaling) app.setAttribute(Qt.ApplicationAttribute.AA_EnableHighDpiScaling)
app.setQuitOnLastWindowClosed(False)
app.setWindowIcon(load_tray_icon())
try: window = MainWindow(
# Crear y mostrar ventana principal start_engine=start_engine,
window = MainWindow() panel_configured=panel_ok,
)
# La ventana SIEMPRE se muestra al iniciar; cerrar (X) la manda a la bandeja.
window.show() window.show()
window.activateWindow()
window.raise_()
app_logger.info("Ventana principal mostrada") app_logger.info("Ventana principal mostrada")
# Ejecutar aplicación
exit_code = app.exec() exit_code = app.exec()
app_logger.info(f"Aplicación finalizada con código: {exit_code}") app_logger.info(f"Aplicación finalizada con código: {exit_code}")
sys.exit(exit_code) return exit_code
except Exception as e:
app_logger.critical(f"Error fatal en la aplicación: {e}", exc_info=True) def main():
"""Punto de entrada: ejecuta _run() y hace visible cualquier fallo de arranque."""
try:
sys.exit(_run())
except SystemExit:
raise
except BaseException as exc: # capturamos TODO en el arranque: nunca morir en silencio
_show_fatal(exc)
sys.exit(1) sys.exit(1)

79
tests/test_bootstrap.py Normal file
View File

@@ -0,0 +1,79 @@
"""Pruebas de bootstrap y carga de .env."""
import os
from pathlib import Path
import pytest
from app.config.bootstrap import ensure_runtime_layout
from app.config.env_loader import (
apply_env_overrides,
get_launch_options,
is_panel_configured,
render_env_template,
)
from app.constants import CONFIG_DIR, DEFAULT_CONFIG, DIR_ENTRADA, ENV_PATH
def test_render_env_template_contains_app_dir(tmp_path: Path):
text = render_env_template(tmp_path)
assert str(tmp_path) in text
assert "CLOUDRESTORE_PANEL_API_URL" in text
def test_ensure_runtime_layout_creates_structure(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
monkeypatch.setattr("app.config.bootstrap.APP_DIR", tmp_path)
monkeypatch.setattr("app.config.bootstrap.CONFIG_DIR", tmp_path / "config")
monkeypatch.setattr("app.config.bootstrap.DATA_DIR", tmp_path / "config" / "data")
monkeypatch.setattr("app.config.bootstrap.LOGS_DIR", tmp_path / "config" / "logs")
monkeypatch.setattr("app.config.bootstrap.ODBC_DIR", tmp_path / "config" / "odbc")
monkeypatch.setattr("app.config.bootstrap.SEVEN_ZIP_DIR", tmp_path / "config" / "7zip")
monkeypatch.setattr("app.config.bootstrap.ENV_PATH", tmp_path / "config" / ".env")
monkeypatch.setattr("app.config.bootstrap.DIR_ENTRADA", tmp_path / "Entrada")
monkeypatch.setattr("app.config.bootstrap.DIR_PROCESADOS", tmp_path / "Procesados")
monkeypatch.setattr("app.config.bootstrap.DIR_FALLADOS", tmp_path / "Fallados")
monkeypatch.setattr("app.config.bootstrap.DIR_TEMP", tmp_path / "Temp")
monkeypatch.setattr("app.constants.DB_PATH", tmp_path / "config" / "data" / "app.db")
ensure_runtime_layout()
assert (tmp_path / "config" / ".env").is_file()
assert (tmp_path / "Entrada").is_dir()
assert (tmp_path / "config" / "data" / "app.db").is_file()
def test_is_panel_configured(monkeypatch: pytest.MonkeyPatch):
monkeypatch.delenv("CLOUDRESTORE_PANEL_API_URL", raising=False)
monkeypatch.delenv("CLOUDRESTORE_PANEL_API_TOKEN", raising=False)
monkeypatch.delenv("CLOUDRESTORE_PANEL_INSTANCE_KEY", raising=False)
assert not is_panel_configured()
monkeypatch.setenv("CLOUDRESTORE_PANEL_API_URL", "https://panel:3000")
monkeypatch.setenv("CLOUDRESTORE_PANEL_API_TOKEN", "secret")
monkeypatch.setenv("CLOUDRESTORE_PANEL_INSTANCE_KEY", "srv1")
assert is_panel_configured()
def test_get_launch_options_auto_start_does_not_minimize(monkeypatch: pytest.MonkeyPatch):
monkeypatch.setenv("CLOUDRESTORE_AUTO_START", "true")
monkeypatch.delenv("CLOUDRESTORE_START_MINIMIZED", raising=False)
opts = get_launch_options()
assert opts.start_engine is True
assert opts.minimized is False
def test_get_launch_options_start_minimized(monkeypatch: pytest.MonkeyPatch):
monkeypatch.setenv("CLOUDRESTORE_START_MINIMIZED", "true")
opts = get_launch_options()
assert opts.minimized is True
def test_render_env_template_documents_start_minimized(tmp_path: Path):
text = render_env_template(tmp_path)
assert "CLOUDRESTORE_START_MINIMIZED=false" in text
def test_apply_env_overrides_paths(monkeypatch: pytest.MonkeyPatch):
monkeypatch.setenv("CLOUDRESTORE_INPUT_FOLDER", "/tmp/in")
cfg = apply_env_overrides(DEFAULT_CONFIG.copy())
assert cfg["paths"]["input_folder"] == "/tmp/in"