# -*- coding: utf-8 -*- """ transfer.py — Адаптивный перенос архива (scp/rsync/fallback) """ import os import json from core.color import info, success, warn, error as cerror, prompt, confirm, step from core import state from core.runner import run, exists def do_transfer(): st = state.load_state() archive_path = st.get("archive_path") if not archive_path or not os.path.isfile(archive_path): raise RuntimeError("Архив не найден. Сначала выполните сборку (Source → Pack).") host = st.get("target_host") user = st.get("target_user") port = st.get("target_port", 22) key_path = st.get("ssh_key") password = st.get("ssh_password") if not host or host.lower() in ("none", "", "localhost"): warn("Target host не указан. Перенос невозможен.") state.set_stage("TRANSFER_SKIPPED") return if not user or user.lower() == "none": user = "root" warn(f"SSH user не указан, используем default: {user}") step(5, "ПЕРЕНОС АРХИВА") info(f"Target: {user}@{host}:{port}") # Определяем размер size = os.path.getsize(archive_path) size_mb = size / 1024 / 1024 info(f"Размер архива: {size_mb:.2f} MB") # Путь назначения remote_dir = "/tmp/docker-migrate-incoming" remote_path = os.path.join(remote_dir, os.path.basename(archive_path)) # Создаём remote_dir через ssh перед scp/rsync ssh_base = f"ssh -p {port} -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new" if key_path and os.path.isfile(key_path): ssh_base += f" -i '{key_path}'" info(f"Используем SSH-ключ: {key_path}") elif password: if not exists("sshpass"): raise RuntimeError("sshpass не найден, но нужен для пароля. Установите: apt-get install -y sshpass") ssh_base = f"sshpass -p '{password}' {ssh_base}" info("Используем пароль через sshpass") else: ssh_base += " -o BatchMode=yes" info("Пробуем ключевую аутентификацию (BatchMode)") # Проверим что target отвечает (mkdir) info("Проверяем доступ к target ...") r0 = run(f"{ssh_base} {user}@{host} 'mkdir -p {remote_dir}'", check=False, timeout=30) if r0.returncode != 0: cerror(f"SSH не удался: {r0.stderr.strip()[:200]}") state.set_error("TRANSFER", r0.stdout, r0.stderr, suggestion="Проверьте пароль/ключ и доступ SSH. После исправления: docker-migrate --resume") raise RuntimeError(f"SSH-доступ не работает. Код: {r0.returncode}") # Выбираем метод if size_mb < 50 and exists("scp"): method = "scp" elif size_mb >= 50 and exists("rsync"): method = "rsync" else: if exists("rsync"): method = "rsync" elif exists("scp"): method = "scp" else: raise RuntimeError("Ни rsync, ни scp не найдены. Установите один из них.") info(f"Выбран метод переноса: {method}") if method == "scp": # scp: заглавная -P для порта scp_opts = f"-P {port} -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new" if key_path and os.path.isfile(key_path): scp_opts += f" -i '{key_path}'" info("Копируем архив через scp ...") r = run(f"scp {scp_opts} {archive_path} {user}@{host}:{remote_path}", check=False, timeout=60) elif password: # sshpass оборачивает всю команду scp info("Копируем архив через scp + sshpass ...") r = run(f"sshpass -p '{password}' scp {scp_opts} {archive_path} {user}@{host}:{remote_path}", check=False, timeout=60) else: scp_opts += " -o BatchMode=yes" info("Копируем архив через scp ...") r = run(f"scp {scp_opts} {archive_path} {user}@{host}:{remote_path}", check=False, timeout=60) else: # rsync через ssh_base info("Копируем архив через rsync ...") rsync_ssh = ssh_base.replace("ssh ", "") # убираем 'ssh ' для rsync -e r = run(f"rsync -avz --progress -e 'ssh {rsync_ssh}' {archive_path} {user}@{host}:{remote_path}", check=False, timeout=60) if r.returncode != 0: cerror(f"Передача не удалась: {r.stderr.strip()[:200]}") state.set_error( step="TRANSFER", stdout=r.stdout, stderr=r.stderr, suggestion=f"Проверьте SSH-доступ к {user}@{host}:{port}. После исправления: docker-migrate --resume" ) raise RuntimeError(f"Передача файла не удалась. Код возврата: {r.returncode}") success(f"Архив передан на {host}:{remote_path}") # Распаковка на target info("Распаковываем архив на target ...") r2 = run(f"{ssh_base} {user}@{host} 'tar xzf {remote_path} -C {remote_dir}'", check=False, timeout=60) if r2.returncode != 0: warn(f"Не удалось распаковать: {r2.stderr.strip()[:200]}") state.set_error("TRANSFER_UNPACK", r2.stdout, r2.stderr, suggestion="Проверьте tar на target") raise RuntimeError("Распаковка не удалась") # Сохраняем remote_dir в state для target state.set_stage("TRANSFER", target_remote_dir=remote_dir) # Предлагаем запустить target-режим удалённо if confirm("Сразу запустить восстановление на новом сервере", default="y"): info("Запускаем target-mode удалённо ...") project_local = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) run(f"{ssh_base} {user}@{host} 'mkdir -p /opt/docker-migrate-tool && tar czf - -C {project_local} . | tar xzf - -C /opt/docker-migrate-tool'", check=False, timeout=60) run(f"{ssh_base} {user}@{host} 'cd /opt/docker-migrate-tool && python3 core/main.py --mode=target --remote-dir={remote_dir}'", check=False, timeout=300) else: success(f"Архив передан. Запустите на target: python3 core/main.py --mode=target --remote-dir={remote_dir}") state.mark_completed("TRANSFER") state.set_stage("DONE")