- firma.py: RSA-2048 PKCS#1 v1.5 SHA-256 sin dependencias; empaquetar.py firma el manifiesto y actualizador.py exige la firma (CLAVE_PUBLICA incrustada). - Barra de tareas: tiempo restante estimado a partir del avance. - Corregido: APLICANDO nunca llegaba al total y «Moviendo» quedaba fijo, bloqueando desconectar la carpeta. - probar.py: reglas, firma y recorrido completo sobre una copia temporal. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
147 líneas
8.0 KiB
Python
147 líneas
8.0 KiB
Python
# -*- coding: utf-8 -*-
|
|
r"""
|
|
Construye la versión portátil y los paquetes de actualización.
|
|
Uso: py -3.12 empaquetar.py [ruta\a\ffmpeg\bin]
|
|
Resultado:
|
|
dist\OrganizadorLabstream\ carpeta completa (con ffmpeg)
|
|
OrganizadorLabstream-portatil-<v>.zip primera instalación (con ffmpeg, ~190 MB)
|
|
OrganizadorLabstream-app-<v>.zip actualización (sin ffmpeg ni datos, ~15 MB)
|
|
..\labstream-os\public\herramientas\organizador.json manifiesto para el OS (versión, tamaños, sha256)
|
|
Después: subir los dos zips al NAS (storage/herramientas/) y desplegar el OS.
|
|
"""
|
|
import os
|
|
import re
|
|
import sys
|
|
import glob
|
|
import json
|
|
import shutil
|
|
import hashlib
|
|
import zipfile
|
|
import datetime
|
|
import subprocess
|
|
|
|
AQUI = os.path.dirname(os.path.abspath(__file__))
|
|
os.chdir(AQUI)
|
|
VERSION = re.search(r'^VERSION = "([^"]+)"', open("organizador.py", encoding="utf-8").read(), re.M).group(1)
|
|
print("versión", VERSION)
|
|
|
|
ffbin = sys.argv[1] if len(sys.argv) > 1 else ""
|
|
if not ffbin:
|
|
for c in glob.glob(os.path.expandvars(r"%LOCALAPPDATA%\Microsoft\WinGet\Packages\Gyan.FFmpeg*\ffmpeg-*\bin")):
|
|
ffbin = c
|
|
if not ffbin and shutil.which("ffmpeg"):
|
|
ffbin = os.path.dirname(shutil.which("ffmpeg"))
|
|
print("ffmpeg desde:", ffbin or "(no encontrado: el zip irá sin ffmpeg)")
|
|
|
|
for d in ("build", "dist"):
|
|
shutil.rmtree(d, ignore_errors=True)
|
|
subprocess.check_call([sys.executable, "-m", "PyInstaller", "--noconfirm", "--clean", "--onedir", "--windowed",
|
|
"--name", "OrganizadorLabstream", "--icon", os.path.join("static", "organilab.ico"),
|
|
"--hidden-import", "resolve_bridge", "--hidden-import", "reglas",
|
|
"--hidden-import", "asistente", "--hidden-import", "actualizador", "--hidden-import", "firma",
|
|
"--collect-all", "webview", "--collect-all", "clr_loader", "--collect-all", "pythonnet",
|
|
"--exclude-module", "tkinter", "organizador.py"])
|
|
dest = os.path.join("dist", "OrganizadorLabstream")
|
|
shutil.copytree("static", os.path.join(dest, "static"))
|
|
os.makedirs(os.path.join(dest, "datos"), exist_ok=True)
|
|
for f in ("config.json", "diccionario.json", "plantillas.json"):
|
|
src = os.path.join("datos", f)
|
|
if os.path.exists(src):
|
|
shutil.copy(src, os.path.join(dest, "datos", f))
|
|
# la config del paquete sale limpia: sin el recorrido ya visto ni raíces de prueba
|
|
cfg_p = os.path.join(dest, "datos", "config.json")
|
|
if os.path.exists(cfg_p):
|
|
cfg = json.load(open(cfg_p, encoding="utf-8"))
|
|
cfg.pop("tour_visto", None)
|
|
cfg["raices"] = [] # el paquete sale SIN carpetas: cada equipo añade las suyas y se escanean al añadirlas
|
|
json.dump(cfg, open(cfg_p, "w", encoding="utf-8"), ensure_ascii=False, indent=2)
|
|
shutil.copy("README.md", dest)
|
|
BAT = ('@echo off\r\nchcp 65001 >nul\r\ncd /d "%~dp0"\r\nif "%~1"=="" (\r\n echo Arrastra una carpeta de cliente encima de este archivo.\r\n pause\r\n exit /b\r\n)\r\n'
|
|
'"%~dp0OrganizadorLabstream.exe" {cmd} "%~1"\r\n')
|
|
with open(os.path.join(dest, "Organizar carpeta (arrastra aqui).bat"), "w", encoding="cp1252") as fh:
|
|
fh.write(BAT.format(cmd="asistente"))
|
|
with open(os.path.join(dest, "Apartar stock Envato viejo (arrastra aqui).bat"), "w", encoding="cp1252") as fh:
|
|
fh.write(BAT.format(cmd="stock"))
|
|
with open(os.path.join(dest, "LEEME.txt"), "w", encoding="utf-8") as fh:
|
|
fh.write(f"OrganiLAB · Organizador Labstream {VERSION} (portátil)\n\n"
|
|
"ORDENAR UNA CARPETA: arrastra la carpeta del cliente encima de «Organizar carpeta (arrastra aqui).bat»\n"
|
|
"(o del propio .exe). Se escanea con barra de progreso, sale el diagnóstico y el asistente pregunta paso a paso.\n"
|
|
"Por consola: OrganizadorLabstream.exe asistente \"D:\\Clientes\\Carpeta\" [--simular] [--si]\n"
|
|
"Más comandos: OrganizadorLabstream.exe ayuda\n\n"
|
|
"1. Doble clic en OrganizadorLabstream.exe abre OrganiLAB en su propia ventana (sin navegador).\n"
|
|
"2. No hace falta instalar nada: Python y ffmpeg van dentro.\n3. Para convertir BRAW hace falta DaVinci Resolve Studio abierto en un proyecto.\n"
|
|
"4. La app NUNCA borra: lo que sobra va a _Para borrar y lo repetido a subcarpetas Duplicados, para que una persona lo revise.\n"
|
|
"5. Todo lo que aprende y la configuración queda en la carpeta datos\\ (llévala contigo si cambias de PC).\n"
|
|
"6. Se actualiza solo desde Labstream OS (os.labstreamsas.com): avisa cuando hay versión nueva.\n"
|
|
"Si Windows SmartScreen avisa, «Más información» → «Ejecutar de todas formas».\n")
|
|
|
|
# ── paquete APP (actualización): todo menos ffmpeg y datos ──
|
|
app_zip = f"OrganizadorLabstream-app-{VERSION}.zip"
|
|
with zipfile.ZipFile(app_zip, "w", zipfile.ZIP_DEFLATED) as z:
|
|
for raiz, dirs, files in os.walk(dest):
|
|
rel_raiz = os.path.relpath(raiz, dest)
|
|
primero = rel_raiz.split(os.sep)[0]
|
|
if primero in ("ffmpeg", "datos"):
|
|
continue
|
|
for f in files:
|
|
p = os.path.join(raiz, f)
|
|
z.write(p, "OrganizadorLabstream/" + os.path.relpath(p, dest).replace(os.sep, "/"))
|
|
|
|
# ── paquete COMPLETO (primera instalación): con ffmpeg ──
|
|
if ffbin:
|
|
os.makedirs(os.path.join(dest, "ffmpeg"), exist_ok=True)
|
|
for exe in ("ffmpeg.exe", "ffprobe.exe"):
|
|
shutil.copy(os.path.join(ffbin, exe), os.path.join(dest, "ffmpeg", exe))
|
|
lic = os.path.join(os.path.dirname(ffbin), "LICENSE")
|
|
if os.path.exists(lic):
|
|
shutil.copy(lic, os.path.join(dest, "ffmpeg", "LICENSE.txt"))
|
|
for viejo in glob.glob("OrganizadorLabstream-portatil*.zip") + [z for z in glob.glob("OrganizadorLabstream-app-*.zip") if z != app_zip]:
|
|
os.remove(viejo)
|
|
full_zip = f"OrganizadorLabstream-portatil-{VERSION}.zip"
|
|
shutil.make_archive(full_zip[:-4], "zip", "dist", "OrganizadorLabstream")
|
|
|
|
|
|
def sha(p):
|
|
h = hashlib.sha256()
|
|
with open(p, "rb") as fh:
|
|
for b in iter(lambda: fh.read(1 << 20), b""):
|
|
h.update(b)
|
|
return h.hexdigest()
|
|
|
|
|
|
notas = os.environ.get("NOTAS", "")
|
|
manifiesto = {
|
|
"nombre": "OrganiLAB",
|
|
"version": VERSION,
|
|
"builtAt": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z"),
|
|
"archivo": full_zip, "size": os.path.getsize(full_zip), "sha256": sha(full_zip),
|
|
"app": {"archivo": app_zip, "size": os.path.getsize(app_zip), "sha256": sha(app_zip), "url": f"/api/herramientas/{app_zip}"},
|
|
"notes": notas,
|
|
}
|
|
import firma
|
|
if os.environ.get("FIRMA") == "no":
|
|
print("AVISO: manifiesto sin firmar (FIRMA=no); los .exe 1.6.2+ no lo aceptarán")
|
|
else:
|
|
if not os.path.exists(firma.RUTA_PRIVADA):
|
|
sys.exit(f"falta la clave de firma en {firma.RUTA_PRIVADA}: corre py -3.12 firma.py generar (o FIRMA=no para saltarlo)")
|
|
_priv = firma.cargar_privada()
|
|
manifiesto["firma"] = {"alg": "rsa2048-pkcs1v15-sha256", "valor": firma.firmar(firma.texto_manifiesto(manifiesto), _priv)}
|
|
import actualizador
|
|
if not firma.verificar(firma.texto_manifiesto(manifiesto), manifiesto["firma"]["valor"], actualizador.CLAVE_PUBLICA):
|
|
sys.exit("la clave privada no corresponde a CLAVE_PUBLICA de actualizador.py; el manifiesto no serviría")
|
|
print("manifiesto firmado")
|
|
ruta_os = os.path.join(os.path.dirname(AQUI), "labstream-os", "public", "herramientas", "organizador.json")
|
|
if os.path.isdir(os.path.dirname(ruta_os)):
|
|
previo = {}
|
|
try:
|
|
previo = json.load(open(ruta_os, encoding="utf-8"))
|
|
except Exception: # noqa
|
|
pass
|
|
if not notas:
|
|
manifiesto["notes"] = previo.get("notes", "") if previo.get("version") == VERSION else ""
|
|
json.dump(manifiesto, open(ruta_os, "w", encoding="utf-8"), ensure_ascii=False, indent=2)
|
|
print("manifiesto escrito en", ruta_os)
|
|
json.dump(manifiesto, open("manifiesto.json", "w", encoding="utf-8"), ensure_ascii=False, indent=2)
|
|
print(f"listo: {full_zip} ({os.path.getsize(full_zip) / 1e6:.0f} MB) · {app_zip} ({os.path.getsize(app_zip) / 1e6:.0f} MB)")
|
|
print("Sube los dos zips a storage/herramientas/ del NAS y despliega el OS.")
|