3.1.0: se instala solo en su sitio, manual dentro y el puerto para una sola copia
- instalar.py: instalar un paquete lo copia a la carpeta de programas del usuario (%LOCALAPPDATA%\Programs\OrganiLAB) con sus datos y su ffmpeg, lo da de alta desde ahí y lo vuelve a abrir desde su sitio. Del origen no se borra nada: queda instalado-en.txt, que además hace que abrir esa copia abra la instalada. Un destino ocupado no se pisa, y si ya hay un OrganiLAB instalado lo dice en vez de poner otro encima. - static/index.html: vista «Manual», con cada función del programa, qué hace y cómo lo hace (41 entradas, un apartado por pestaña). - organizador.py: ServidorLocal pide el puerto en exclusiva. Con «reutilizar dirección», en Windows dos copias escuchaban a la vez en el mismo puerto y se contestaban la una por la otra sin que nada fallara a la vista. - Pruebas: probar.py 19, prueba_funcional.py 26 de 26, prueba_ventana.py 18 desde el código y 17 con el paquete, incluida la instalación entera en un equipo de ensayo (ORGANILAB_ENSAYO) que no toca el Windows de verdad. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Este commit está contenido en:
152
instalar.py
152
instalar.py
@@ -1,11 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r"""
|
||||
OrganiLAB como un programa más de Windows: su acceso directo en el menú Inicio y en el Escritorio, y su
|
||||
línea en Configuración › Aplicaciones para quitarlo.
|
||||
OrganiLAB como un programa más de Windows: en su sitio, con su acceso directo en el menú Inicio y en el
|
||||
Escritorio, y con su línea en Configuración › Aplicaciones para quitarlo.
|
||||
|
||||
No copia nada: el programa se queda en la carpeta donde está y esto solo lo da de alta, todo dentro del
|
||||
usuario y sin pedir permisos de administrador. Quitarlo borra los accesos directos y la línea de
|
||||
Aplicaciones; la carpeta del programa y los datos no se tocan.
|
||||
Instalar el paquete lo lleva a su sitio definitivo, la carpeta de programas del usuario
|
||||
(%LOCALAPPDATA%\Programs\OrganiLAB): de Descargas, que es donde se suele descomprimir, se limpia por
|
||||
descuido. Se copia el programa con sus datos y su ffmpeg; del origen no se borra ni se mueve nada, y allí
|
||||
queda una nota que, además, hace que abrir esa copia abra la instalada. Todo dentro del usuario, sin
|
||||
permisos de administrador. Desde el código (sin .exe) solo se da de alta donde está.
|
||||
|
||||
Quitarlo borra los accesos directos y la línea de Aplicaciones; la carpeta del programa y los datos no se
|
||||
tocan.
|
||||
|
||||
Los accesos directos llevan la misma identidad que la ventana (ventana.IDENTIDAD). Así el acceso anclado
|
||||
a la barra de tareas y la ventana abierta son, para Windows, la misma cosa: un solo botón, con su icono.
|
||||
@@ -15,15 +20,26 @@ a la barra de tareas y la ventana abierta son, para Windows, la misma cosa: un s
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import ctypes
|
||||
import shutil
|
||||
import winreg
|
||||
import sqlite3
|
||||
from ctypes import wintypes as W
|
||||
|
||||
import ventana as V
|
||||
|
||||
NOMBRE = "OrganiLAB"
|
||||
DESCRIPCION = "OrganiLAB · ordena y aligera el material de video"
|
||||
CLAVE = r"Software\Microsoft\Windows\CurrentVersion\Uninstall\OrganiLAB"
|
||||
# Para probar la instalación entera sin tocar el Windows de verdad: con ORGANILAB_ENSAYO=<carpeta>, el
|
||||
# «equipo» es esa carpeta (sus programas, su menú Inicio y su Escritorio) y la línea de Aplicaciones va a
|
||||
# una clave aparte del registro. Lo usa prueba_ventana.py.
|
||||
ENSAYO = os.environ.get("ORGANILAB_ENSAYO", "")
|
||||
CLAVE = r"Software\OrganiLAB-ensayo\Alta" if ENSAYO else r"Software\Microsoft\Windows\CurrentVersion\Uninstall\OrganiLAB"
|
||||
MARCA_MUDANZA = "instalado-en.txt" # en la carpeta de origen: adónde se instaló lo que había aquí
|
||||
INVENTARIO = "inventario.sqlite"
|
||||
NO_VIAJA = {"actualizacion", "__pycache__", MARCA_MUDANZA} # del primer nivel: lo que es de paso
|
||||
NO_VIAJA_DATOS = {"sesion.json", "ventana"} # de datos\: la sesión abierta y el perfil del motor, que se rehace solo
|
||||
EN_INICIO = "{A77F5D77-2E2B-44C3-A6A2-ABA601054A51}" # la carpeta Programas del menú Inicio
|
||||
EN_ESCRITORIO = "{B4BFCC3A-DB2C-424C-B029-7FE99A87C641}" # el Escritorio (puede estar en OneDrive)
|
||||
CLSID_ACCESO, IID_ACCESO = "{00021401-0000-0000-C000-000000000046}", "{000214F9-0000-0000-C000-000000000046}"
|
||||
@@ -132,9 +148,106 @@ def leer_acceso(ruta):
|
||||
|
||||
def _accesos():
|
||||
"""Las dos rutas donde van los accesos directos: (menú Inicio, Escritorio)."""
|
||||
if ENSAYO:
|
||||
carpetas = os.path.join(ENSAYO, "Inicio"), os.path.join(ENSAYO, "Escritorio")
|
||||
for c in carpetas:
|
||||
os.makedirs(c, exist_ok=True)
|
||||
return tuple(os.path.join(c, NOMBRE + ".lnk") for c in carpetas)
|
||||
return os.path.join(carpeta_de(EN_INICIO), NOMBRE + ".lnk"), os.path.join(carpeta_de(EN_ESCRITORIO), NOMBRE + ".lnk")
|
||||
|
||||
|
||||
def _misma(a, b):
|
||||
return os.path.normcase(os.path.abspath(a)) == os.path.normcase(os.path.abspath(b))
|
||||
|
||||
|
||||
def sitio_definitivo():
|
||||
"""Donde Windows pone los programas de cada usuario. De ahí no se limpia nada por descuido."""
|
||||
programas = os.path.join(ENSAYO, "Programs") if ENSAYO else os.path.join(
|
||||
os.environ.get("LOCALAPPDATA") or os.path.expanduser("~\\AppData\\Local"), "Programs")
|
||||
return os.path.join(programas, NOMBRE)
|
||||
|
||||
|
||||
def hay_que_mudar(base):
|
||||
"""True si instalar significa llevar el programa a su sitio: es un paquete (trae su .exe) y no está ya allí."""
|
||||
return os.path.isfile(os.path.join(base, "OrganizadorLabstream.exe")) and not _misma(base, sitio_definitivo())
|
||||
|
||||
|
||||
def mudado(base):
|
||||
"""Si desde esta carpeta ya se instaló el programa en otra, el .exe de esa otra; si no, ""."""
|
||||
try:
|
||||
with open(os.path.join(base, MARCA_MUDANZA), encoding="utf-8") as fh:
|
||||
destino = fh.readline().strip()
|
||||
except OSError:
|
||||
return ""
|
||||
exe = os.path.join(destino, "OrganizadorLabstream.exe")
|
||||
return exe if destino and not _misma(destino, base) and os.path.isfile(exe) else ""
|
||||
|
||||
|
||||
def _lo_que_viaja(base):
|
||||
"""(ruta relativa, bytes) de cada archivo que se lleva la mudanza. No viaja lo que es de la sesión abierta
|
||||
ni lo que se rehace solo; el inventario va aparte, con la copia segura de SQLite."""
|
||||
out = []
|
||||
for raiz, dirs, files in os.walk(base):
|
||||
rel = os.path.relpath(raiz, base)
|
||||
if rel == ".":
|
||||
dirs[:] = [d for d in dirs if d not in NO_VIAJA]
|
||||
files = [f for f in files if f not in NO_VIAJA]
|
||||
elif rel == "datos":
|
||||
dirs[:] = [d for d in dirs if d not in NO_VIAJA_DATOS]
|
||||
files = [f for f in files if f not in NO_VIAJA_DATOS and not f.startswith(INVENTARIO) and not f.endswith(".tmp")]
|
||||
for f in files:
|
||||
p = os.path.join(raiz, f)
|
||||
out.append((os.path.relpath(p, base), os.path.getsize(p)))
|
||||
return out
|
||||
|
||||
|
||||
def mudar(base, destino=None, avance=None):
|
||||
"""Copia el programa de `base`, con sus datos y su ffmpeg, a su sitio definitivo. Del origen no borra ni
|
||||
mueve nada: deja allí la nota de la mudanza. Si falla a medias, quita lo copiado y el origen sigue como
|
||||
estaba. `avance(0..1)` va diciendo cuánto lleva. Devuelve el destino."""
|
||||
destino = destino or sitio_definitivo()
|
||||
if os.path.isdir(destino) and os.listdir(destino):
|
||||
raise FileExistsError(f"la carpeta {destino} ya existe y no está vacía")
|
||||
cosas = _lo_que_viaja(base)
|
||||
inventario = os.path.join(base, "datos", INVENTARIO)
|
||||
total = sum(t for _, t in cosas) + (os.path.getsize(inventario) if os.path.exists(inventario) else 0) or 1
|
||||
os.makedirs(os.path.dirname(destino), exist_ok=True)
|
||||
if shutil.disk_usage(os.path.dirname(destino)).free < total * 1.05 + (50 << 20):
|
||||
raise OSError(f"no hay sitio en el disco: hacen falta {total / 1e6:.0f} MB")
|
||||
hechos = 0
|
||||
try:
|
||||
for rel, tam in cosas:
|
||||
a = os.path.join(destino, rel)
|
||||
os.makedirs(os.path.dirname(a), exist_ok=True)
|
||||
shutil.copy2(os.path.join(base, rel), a)
|
||||
hechos += tam
|
||||
if avance:
|
||||
avance(hechos / total)
|
||||
if os.path.exists(inventario): # puede estar en uso: SQLite sabe copiarse entero y coherente
|
||||
os.makedirs(os.path.join(destino, "datos"), exist_ok=True)
|
||||
origen, copia = sqlite3.connect(inventario), sqlite3.connect(os.path.join(destino, "datos", INVENTARIO))
|
||||
try:
|
||||
origen.backup(copia)
|
||||
finally:
|
||||
copia.close()
|
||||
origen.close()
|
||||
except Exception:
|
||||
shutil.rmtree(destino, ignore_errors=True)
|
||||
raise
|
||||
with open(os.path.join(base, MARCA_MUDANZA), "w", encoding="utf-8") as fh:
|
||||
fh.write(destino + "\n\nOrganiLAB se instaló en la carpeta de arriba el " + time.strftime("%d/%m/%Y")
|
||||
+ ", con sus datos.\nEsta copia ya no se usa: si la abres, se abre la instalada.\n"
|
||||
"Puedes borrar esta carpeta entera cuando quieras.\n")
|
||||
return destino
|
||||
|
||||
|
||||
class YaInstalado(Exception):
|
||||
"""Ya hay un OrganiLAB instalado en este equipo, en otra carpeta."""
|
||||
def __init__(self, otro):
|
||||
super().__init__(f"ya hay un OrganiLAB instalado en {otro['base']} (versión {otro['version']})")
|
||||
self.otro = otro
|
||||
|
||||
|
||||
def _peso_kb(base):
|
||||
total = 0
|
||||
for raiz, dirs, files in os.walk(base):
|
||||
@@ -181,6 +294,18 @@ def instalar(base, version, solo_los_que_hay=False):
|
||||
return {"base": base, "accesos": hechos, "orden": f'"{exe}" {args}'.strip()}
|
||||
|
||||
|
||||
def poner(base, version, avance=None):
|
||||
"""Instalar de verdad: si el programa es un paquete que no está en su sitio definitivo, se copia allí; y
|
||||
se da de alta en Windows desde donde quede. El resultado dice dónde quedó y si hubo mudanza."""
|
||||
if not hay_que_mudar(base):
|
||||
return {**instalar(base, version), "mudado": False}
|
||||
otro = instalado()
|
||||
if otro and not _misma(otro["base"], base) and os.path.isfile(os.path.join(otro["base"], "OrganizadorLabstream.exe")):
|
||||
raise YaInstalado(otro)
|
||||
destino = mudar(base, avance=avance)
|
||||
return {**instalar(destino, version), "mudado": True}
|
||||
|
||||
|
||||
def al_dia(base, version):
|
||||
"""Tras una actualización: si este programa está dado de alta, Windows tiene que enseñar la versión
|
||||
nueva y los accesos directos tienen que apuntar a lo que ahora lo abre."""
|
||||
@@ -222,9 +347,13 @@ def principal(que, base, version, silencioso=False):
|
||||
return V.IDOK if silencioso else u.MessageBoxW(None, texto, NOMBRE, botones)
|
||||
try:
|
||||
if que == "instalar":
|
||||
instalar(base, version)
|
||||
decir("OrganiLAB ya está en el menú Inicio y en el Escritorio.\n\n"
|
||||
"Para quitarlo: Configuración › Aplicaciones › Aplicaciones instaladas.")
|
||||
r = poner(base, version)
|
||||
if r["mudado"]:
|
||||
decir(f"OrganiLAB quedó instalado en:\n\n{r['base']}\n\nÁbrelo desde el menú Inicio o el Escritorio. "
|
||||
"La carpeta desde la que lo instalaste ya no hace falta: puedes borrarla.")
|
||||
else:
|
||||
decir("OrganiLAB ya está en el menú Inicio y en el Escritorio.\n\n"
|
||||
"Para quitarlo: Configuración › Aplicaciones › Aplicaciones instaladas.")
|
||||
return 0
|
||||
if not silencioso and decir("¿Quitar OrganiLAB del menú Inicio, del Escritorio y de la lista de aplicaciones?\n\n"
|
||||
"La carpeta del programa y tus datos (inventario, historial y ajustes) no se borran.",
|
||||
@@ -233,6 +362,11 @@ def principal(que, base, version, silencioso=False):
|
||||
desinstalar(base)
|
||||
decir(f"Quitado. La carpeta del programa sigue en:\n\n{base}\n\nSi ya no la quieres, bórrala a mano.")
|
||||
return 0
|
||||
except YaInstalado as ex:
|
||||
decir(f"Ya hay un OrganiLAB instalado en este equipo (versión {ex.otro['version']}), en:\n\n{ex.otro['base']}\n\n"
|
||||
"Ábrelo desde el menú Inicio. Para ponerlo al día, usa «Buscar actualizaciones» dentro del programa.",
|
||||
V.MB_OK | V.MB_ICONINFORMATION)
|
||||
return 1
|
||||
except Exception as ex: # noqa
|
||||
decir(f"No se pudo: {ex}", V.MB_OK | V.MB_ICONWARNING)
|
||||
return 1
|
||||
|
||||
Referencia en una nueva incidencia
Block a user