diff --git a/Models-Backup.json b/Models-Backup.json new file mode 100644 index 0000000..fb1f16d --- /dev/null +++ b/Models-Backup.json @@ -0,0 +1 @@ +{"id":"c500f3f0-4711-4aa1-8a72-d7aded65e375","revision":0,"last_node_id":2,"last_link_id":1,"nodes":[{"id":2,"type":"ShowText|pysssss","pos":[1184.7082734890591,382.55434679367886],"size":[310.7533638412674,125.03695102793529],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"text","name":"text","type":"STRING","link":1}],"outputs":[{"localized_name":"STRING","name":"STRING","shape":6,"type":"STRING","links":null}],"properties":{"ue_properties":{"widget_ue_connectable":{},"input_ue_unconnectable":{}},"cnr_id":"comfyui-custom-scripts","ver":"1.2.5","Node name for S&R":"ShowText|pysssss"}},{"id":1,"type":"WorkflowBackup","pos":[811.6121734958897,382.5543467936786],"size":[313.91953125,130],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"workflows_path","name":"workflows_path","type":"STRING","widget":{"name":"workflows_path"},"link":null},{"localized_name":"backup_destination","name":"backup_destination","type":"STRING","widget":{"name":"backup_destination"},"link":null},{"localized_name":"mode","name":"mode","type":"COMBO","widget":{"name":"mode"},"link":null},{"localized_name":"language","name":"language","type":"COMBO","widget":{"name":"language"},"link":null}],"outputs":[{"localized_name":"status_report","name":"status_report","type":"STRING","links":[1]}],"properties":{"ue_properties":{"widget_ue_connectable":{},"input_ue_unconnectable":{}},"Node name for S&R":"WorkflowBackup"},"widgets_values":["Paste your path here","D:/ComfyUI_Backup","ANALYSIS_ONLY","English"]}],"links":[[1,1,0,2,0,"STRING"]],"groups":[],"config":{},"extra":{"workflowRendererVersion":"LG","ue_links":[],"ds":{"scale":1.4641000000000006,"offset":[-318.1349519946265,-112.59061053932456]}},"version":0.4} \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..5cc5764 --- /dev/null +++ b/__init__.py @@ -0,0 +1,11 @@ +from .nodes import WorkflowBackupNode + +NODE_CLASS_MAPPINGS = { + "WorkflowBackup": WorkflowBackupNode +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "WorkflowBackup": "📦 ComfyUI Backup & Packager" +} + +__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] \ No newline at end of file diff --git a/nodes.py b/nodes.py new file mode 100644 index 0000000..94f5605 --- /dev/null +++ b/nodes.py @@ -0,0 +1,192 @@ +import os +import json +import shutil +import folder_paths +from pathlib import Path +from comfy.utils import ProgressBar # <--- NUEVO: Importamos la barra de progreso + +class WorkflowBackupNode: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "workflows_path": ("STRING", {"default": "Paste your path here", "multiline": False}), + "backup_destination": ("STRING", {"default": "D:/ComfyUI_Backup", "multiline": False}), + "mode": (["ANALYSIS_ONLY", "EXECUTE_BACKUP"],), + "language": (["English", "Spanish"],), + }, + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("status_report",) + FUNCTION = "process_backup" + OUTPUT_NODE = True + CATEGORY = "utils/backup" + + def process_backup(self, workflows_path, backup_destination, mode, language): + # 1. Path Cleaning + w_path = Path(workflows_path.strip('"').strip("'")) + dest_path = Path(backup_destination.strip('"').strip("'")) + + # 2. Setup Messages + msgs = { + "English": { + "err_path": "❌ ERROR: The workflows folder does not exist: ", + "err_no_json": "⚠️ No .json files found in ", + "analyzing": "🔍 Analyzing {count} workflows in: {path}", + "err_read": "⚠️ Error reading {file}: {error}", + "found": "✅ Found {count} unique models required.", + "size": "📦 Estimated Total Size: {size:.2f} GB", + "err_dest": "❌ Error creating destination folder: {path}", + "copying": "Copying: {name}...", + "err_copy": "Error copying {src}: {error}", + "done": "🎉 BACKUP COMPLETED: {count} new models copied.", + "location": "📁 Location: {path}", + "analysis_mode": "ℹ️ ANALYSIS MODE: No files were copied. Switch mode to EXECUTE_BACKUP to perform the copy." + }, + "Spanish": { + "err_path": "❌ ERROR: La carpeta de workflows no existe: ", + "err_no_json": "⚠️ No se encontraron archivos .json en ", + "analyzing": "🔍 Analizando {count} workflows en: {path}", + "err_read": "⚠️ Error leyendo {file}: {error}", + "found": "✅ Se encontraron {count} modelos únicos necesarios.", + "size": "📦 Tamaño total estimado: {size:.2f} GB", + "err_dest": "❌ Error creando carpeta destino: {path}", + "copying": "Copiando: {name}...", + "err_copy": "Error copiando {src}: {error}", + "done": "🎉 RESPALDO COMPLETADO: {count} modelos nuevos copiados.", + "location": "📁 Ubicación: {path}", + "analysis_mode": "ℹ️ MODO ANÁLISIS: No se copiaron archivos. Cambia el modo a EXECUTE_BACKUP para copiar." + } + } + + T = msgs[language] + + # 3. ComfyUI Root Detection + comfy_root = Path(folder_paths.base_path) + models_root = comfy_root / "models" + + if not w_path.exists(): + return (f"{T['err_path']}{workflows_path}",) + + # 4. Logic + workflows_found = list(w_path.rglob("*.json")) + if not workflows_found: + return (f"{T['err_no_json']}{workflows_path}",) + + report = [] + report.append(T['analyzing'].format(count=len(workflows_found), path=w_path)) + + files_to_copy = {} # {source_path: category} + + # Categories Map + CATEGORIES_MAP = { + 'checkpoints': ['ckpt', 'checkpoint', 'model_name', 'load_checkpoint'], + 'loras': ['lora'], + 'vae': ['vae'], + 'controlnet': ['control', 'controlnet'], + 'upscale_models': ['upscale', 'esrgan', 'realesrgan'], + 'clip_vision': ['clip_vision'], + 'embeddings': ['embedding'], + 'ipadapter': ['ipadapter'], + 'unet': ['unet'], + 'clip': ['clip'] + } + + # --- ANALYSIS PHASE --- + for wf_file in workflows_found: + try: + with open(wf_file, 'r', encoding='utf-8') as f: + data = json.load(f) + + nodes = data.get('nodes', []) + if not nodes and isinstance(data, dict): + nodes = list(data.values()) + + for node in nodes: + if not isinstance(node, dict): continue + + valores = [] + widgets = node.get('widgets_values', []) + inputs = node.get('inputs', {}) + + if isinstance(widgets, list): valores.extend(widgets) + if isinstance(inputs, dict): valores.extend(inputs.values()) + + for val in valores: + if not isinstance(val, str): continue + + if any(ext in val.lower() for ext in ['.safetensors', '.pt', '.pth', '.ckpt', '.bin']): + ruta_real = self.buscar_archivo(models_root, val) + if ruta_real: + cat_detectada = Path(ruta_real).parent.name + files_to_copy[ruta_real] = cat_detectada + except Exception as e: + report.append(T['err_read'].format(file=wf_file.name, error=str(e))) + + total_files = len(files_to_copy) + total_size = sum(os.path.getsize(f) for f in files_to_copy.keys()) / (1024**3) + + report.append(T['found'].format(count=total_files)) + report.append(T['size'].format(size=total_size)) + + # --- EXECUTION PHASE --- + if mode == "EXECUTE_BACKUP": + if not dest_path.exists(): + try: + dest_path.mkdir(parents=True) + except: + return (f"{T['err_dest'].format(path=dest_path)}",) + + # 1. Backup Workflows + wf_dest = dest_path / "workflows_backup" + wf_dest.mkdir(exist_ok=True) + for wf in workflows_found: + try: + shutil.copy2(wf, wf_dest) + except: pass + + # 2. Backup Models (CON BARRA DE PROGRESO) + copied_count = 0 + + # Inicializamos la barra de progreso + pbar = ProgressBar(total_files) # <--- NUEVO + + for src, cat in files_to_copy.items(): + try: + final_dest_folder = dest_path / "models" / cat + final_dest_folder.mkdir(parents=True, exist_ok=True) + + src_path = Path(src) + final_dest_file = final_dest_folder / src_path.name + + if not final_dest_file.exists(): + print(T['copying'].format(name=src_path.name)) + shutil.copy2(src_path, final_dest_file) + copied_count += 1 + + # Actualizamos la barra + pbar.update(1) # <--- NUEVO: Avanza 1 paso en la barra azul + + except Exception as e: + print(T['err_copy'].format(src=src, error=str(e))) + + report.append(T['done'].format(count=copied_count)) + report.append(T['location'].format(path=dest_path)) + + else: + report.append(T['analysis_mode']) + + return ("\n".join(report),) + + def buscar_archivo(self, root_path, filename): + possible_path = root_path / filename + if possible_path.exists(): return str(possible_path) + + clean_name = Path(filename).name + for file in root_path.rglob(clean_name): + return str(file) + return None \ No newline at end of file