diff --git a/client/core/controllers/coreController.cpp b/client/core/controllers/coreController.cpp index 36136f71a..7287070d9 100644 --- a/client/core/controllers/coreController.cpp +++ b/client/core/controllers/coreController.cpp @@ -135,7 +135,7 @@ void CoreController::initControllers() new SettingsController(m_serversModel, m_containersModel, m_languageModel, m_sitesModel, m_appSplitTunnelingModel, m_settings)); m_engine->rootContext()->setContextProperty("SettingsController", m_settingsController.get()); - m_serversBackupController.reset(new ServersBackupController(m_settings)); + m_serversBackupController.reset(new ServersBackupController(m_settings, m_serversModel.get())); m_engine->rootContext()->setContextProperty("ServersBackupController", m_serversBackupController.get()); m_sitesController.reset(new SitesController(m_settings, m_vpnConnection, m_sitesModel)); diff --git a/client/ui/controllers/serversBackupController.cpp b/client/ui/controllers/serversBackupController.cpp index d8dfd71d4..430107281 100644 --- a/client/ui/controllers/serversBackupController.cpp +++ b/client/ui/controllers/serversBackupController.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #ifdef Q_OS_ANDROID #include #include "platforms/android/android_controller.h" @@ -18,15 +19,22 @@ #include "containers/containers_defs.h" #include "core/networkUtilities.h" #include "systemController.h" +#include "ui/models/servers_model.h" +#include "ui/models/containers_model.h" -ServersBackupController::ServersBackupController(std::shared_ptr settings, QObject *parent) +ServersBackupController::ServersBackupController(std::shared_ptr settings, ServersModel *serversModel, QObject *parent) : QObject(parent) , m_settings(settings) + , m_serversModel(serversModel) , m_serverController(new ServerController(settings, this)) , m_status(Idle) , m_backupDir("/var/backups/amnezia") , m_restoreReplaceMode(false) , m_tempUploadFile(nullptr) + , m_containerRetryCount(0) + , m_autoRestoreAfterUpload(false) + , m_autoDownloadAfterCreate(false) + , m_autoDeleteAfterDownload(false) { } @@ -52,35 +60,35 @@ void ServersBackupController::createBackup(const ServerCredentials &credentials) m_currentOutput.clear(); m_currentError.clear(); - // Получаем IP адрес сервера + // Get server IP address QString serverIp = NetworkUtilities::getIPAddress(credentials.hostName); if (serverIp.isEmpty()) { serverIp = credentials.hostName; } - // Форматируем IP: заменяем точки на подчеркивания + // Format IP: replace dots with underscores QString ipFormatted = serverIp; ipFormatted.replace(".", "_"); - - // Получаем bash скрипт для backup с IP адресом + + // Get bash script for backup with IP address QString script = getBackupScript(ipFormatted); - // Callback для обработки stdout + // Callback for handling stdout auto cbStdOut = [this](const QString &data, libssh::Client &client) -> ErrorCode { Q_UNUSED(client); return handleStdOut(data, m_currentOutput); }; - // Callback для обработки stderr + // Callback for handling stderr auto cbStdErr = [this](const QString &data, libssh::Client &client) -> ErrorCode { Q_UNUSED(client); return handleStdErr(data, m_currentError); }; - // Запускаем скрипт на сервере + // Run script on server ErrorCode error = m_serverController->runHostScript(credentials, script, cbStdOut, cbStdErr); if (error == ErrorCode::NoError) { - // Парсим имя созданного backup из вывода: формат "IP_ADDRESS - DD-MM-YYYY_HH-MM-SS.tgz" + // Parse created backup name from output: format "IP_ADDRESS - DD-MM-YYYY_HH-MM-SS.tgz" QRegularExpression re("([\\d_]+)\\s+-\\s+(\\d{2}-\\d{2}-\\d{4})_(\\d{2}-\\d{2}-\\d{2})\\.tgz"); QRegularExpressionMatch match = re.match(m_currentOutput); @@ -88,7 +96,18 @@ void ServersBackupController::createBackup(const ServerCredentials &credentials) QString backupFilename = match.captured(1) + " - " + match.captured(2) + "_" + match.captured(3) + ".tgz"; setStatus(Success); setProgress(100, tr("Backup created successfully")); - emit backupCreated(backupFilename); + + // Save filename for later deletion + m_lastCreatedBackupFilename = backupFilename; + + // If auto-download enabled, start it + if (m_autoDownloadAfterCreate) { + qDebug() << "Auto-downloading backup after creation"; + m_autoDownloadAfterCreate = false; // Reset flag + downloadBackup(credentials, backupFilename, backupFilename); + } else { + emit backupCreated(backupFilename); + } } else { setStatus(Failed); emit errorOccurred(tr("Failed to parse backup filename from output: %1").arg(m_currentOutput.left(200)), ErrorCode::InternalError); @@ -123,12 +142,12 @@ void ServersBackupController::createContainerBackup(const ServerCredentials &cre m_currentOutput.clear(); m_currentError.clear(); - // Получаем IP адрес сервера + // Get server IP address QString serverIp = NetworkUtilities::getIPAddress(credentials.hostName); if (serverIp.isEmpty()) { serverIp = credentials.hostName; } - // Форматируем IP: заменяем точки на подчеркивания + // Format IP: replace dots with underscores QString ipFormatted = serverIp; ipFormatted.replace(".", "_"); @@ -147,7 +166,7 @@ void ServersBackupController::createContainerBackup(const ServerCredentials &cre ErrorCode error = m_serverController->runHostScript(credentials, script, cbStdOut, cbStdErr); if (error == ErrorCode::NoError) { - // Парсим имя созданного backup из вывода: формат "IP_ADDRESS - DD-MM-YYYY_HH-MM-SS.tgz" + // Parse created backup name from output: format "IP_ADDRESS - DD-MM-YYYY_HH-MM-SS.tgz" QRegularExpression re("([\\d_]+)\\s+-\\s+(\\d{2}-\\d{2}-\\d{4})_(\\d{2}-\\d{2}-\\d{2})\\.tgz"); QRegularExpressionMatch match = re.match(m_currentOutput); @@ -179,12 +198,12 @@ void ServersBackupController::createContainersBackup(const ServerCredentials &cr m_currentOutput.clear(); m_currentError.clear(); - // Получаем IP адрес сервера + // Get server IP address QString serverIp = NetworkUtilities::getIPAddress(credentials.hostName); if (serverIp.isEmpty()) { serverIp = credentials.hostName; } - // Форматируем IP: заменяем точки на подчеркивания + // Format IP: replace dots with underscores QString ipFormatted = serverIp; ipFormatted.replace(".", "_"); @@ -203,7 +222,7 @@ void ServersBackupController::createContainersBackup(const ServerCredentials &cr ErrorCode error = m_serverController->runHostScript(credentials, script, cbStdOut, cbStdErr); if (error == ErrorCode::NoError) { - // Парсим имя созданного backup из вывода: формат "IP_ADDRESS - DD-MM-YYYY_HH-MM-SS.tgz" + // Parse created backup name from output: format "IP_ADDRESS - DD-MM-YYYY_HH-MM-SS.tgz" QRegularExpression re("([\\d_]+)\\s+-\\s+(\\d{2}-\\d{2}-\\d{4})_(\\d{2}-\\d{2}-\\d{2})\\.tgz"); QRegularExpressionMatch match = re.match(m_currentOutput); @@ -291,14 +310,14 @@ void ServersBackupController::restoreBackup(const ServerCredentials &credentials ErrorCode error = m_serverController->runHostScript(credentials, script, cbStdOut, cbStdErr); - // Проверяем вывод на наличие ошибок, даже если скрипт завершился с кодом 0 + // Check output for errors, even if script exited with code 0 bool hasError = m_currentOutput.contains("[ERROR]") || m_currentOutput.contains("Failed to extract backup") || m_currentError.contains("[ERROR]") || m_currentError.contains("Failed to extract backup"); if (error == ErrorCode::NoError && !hasError) { - // Проверяем, что восстановление действительно завершилось успешно + // Check that restore actually completed successfully if (m_currentOutput.contains("Restore completed successfully")) { setStatus(Success); setProgress(100, tr("Backup restored successfully")); @@ -383,7 +402,7 @@ void ServersBackupController::downloadBackup(const ServerCredentials &credential // If only filename provided (no directory), use appropriate folder if (!pathInfo.isAbsolute() || pathInfo.dir().path() == ".") { #ifdef Q_OS_ANDROID - // На Android используем публичную папку Download (через JNI) + // On Android use public Download folder (via JNI) QJniObject mediaDir = QJniObject::callStaticObjectMethod( "android/os/Environment", "getExternalStoragePublicDirectory", @@ -395,7 +414,7 @@ void ServersBackupController::downloadBackup(const ServerCredentials &credential QString tempPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation); actualLocalPath = QDir(tempPath).filePath(backupFilename); #else - // На Desktop используем Documents (как обычный backup) + // On Desktop use Documents (as regular backup) QString documentsPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); if (documentsPath.isEmpty()) { documentsPath = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); @@ -430,6 +449,15 @@ void ServersBackupController::downloadBackup(const ServerCredentials &credential setStatus(Success); setProgress(100, tr("Backup downloaded successfully")); + + // If auto-delete from server enabled, start it + if (m_autoDeleteAfterDownload && !m_lastCreatedBackupFilename.isEmpty()) { + qDebug() << "Auto-deleting backup from server after download:" << m_lastCreatedBackupFilename; + m_autoDeleteAfterDownload = false; // Reset flag + deleteBackup(credentials, m_lastCreatedBackupFilename); + m_lastCreatedBackupFilename.clear(); + } + emit backupDownloaded(actualLocalPath); } else { setStatus(Failed); @@ -446,31 +474,31 @@ void ServersBackupController::uploadBackup(const ServerCredentials &credentials, return; } - // Сохраняем режим восстановления для последующего использования + // Save restore mode for later use m_restoreReplaceMode = replaceMode; QString actualLocalPath = localPath; QString filename; #ifdef Q_OS_ANDROID - // Для Android URI нужно получить имя файла и использовать файловый дескриптор + // For Android URI need to get filename and use file descriptor if (localPath.startsWith("content://")) { - // Получаем имя файла из URI + // Get filename from URI filename = AndroidController::instance()->getFileName(localPath); if (filename.isEmpty()) { - // Fallback: извлекаем имя из URI + // Fallback: extract name from URI QStringList parts = localPath.split('/'); if (!parts.isEmpty()) { filename = parts.last(); - // Декодируем URL-кодированные символы + // Decode URL-encoded characters if (filename.contains('%')) { filename = QUrl::fromPercentEncoding(filename.toUtf8()); } } } - // Для Android URI используем файловый дескриптор через SystemController::readFile - // Но scpFileCopy требует путь к файлу, поэтому нужно скопировать файл во временную директорию + // For Android URI use file descriptor via SystemController::readFile + // But scpFileCopy requires file path, so need to copy file to temp directory QByteArray fileData; qDebug() << "Reading Android URI:" << localPath; if (!SystemController::readFile(localPath, fileData)) { @@ -480,14 +508,14 @@ void ServersBackupController::uploadBackup(const ServerCredentials &credentials, } qDebug() << "Read" << fileData.size() << "bytes from Android URI"; - // Удаляем предыдущий временный файл, если он существует + // Delete previous temp file if exists if (m_tempUploadFile) { delete m_tempUploadFile; m_tempUploadFile = nullptr; } - // Создаем временный файл (сохраняем в член класса, чтобы не удалялся) - // Используем setAutoRemove(false) чтобы файл не удалялся автоматически + // Create temp file (save to class member so it doesn't get deleted) + // Use setAutoRemove(false) so file isn't automatically deleted m_tempUploadFile = new QTemporaryFile(this); m_tempUploadFile->setAutoRemove(false); if (!m_tempUploadFile->open()) { @@ -500,13 +528,13 @@ void ServersBackupController::uploadBackup(const ServerCredentials &credentials, qint64 written = m_tempUploadFile->write(fileData); m_tempUploadFile->flush(); - // НЕ закрываем файл - он должен оставаться открытым для SCP + // DON'T close file - it must stay open for SCP // m_tempUploadFile->close(); actualLocalPath = m_tempUploadFile->fileName(); qDebug() << "Created temp file:" << actualLocalPath << "written:" << written << "bytes, size:" << QFileInfo(actualLocalPath).size(); - // Проверяем, что файл существует и доступен для чтения + // Check that file exists and is readable QFileInfo tempFileInfo(actualLocalPath); if (!tempFileInfo.exists()) { qDebug() << "Temp file does not exist after creation!"; @@ -516,7 +544,7 @@ void ServersBackupController::uploadBackup(const ServerCredentials &credentials, return; } - // Если имя файла пустое, используем имя из временного файла + // If filename empty, use name from temp file if (filename.isEmpty()) { filename = QFileInfo(actualLocalPath).fileName(); } @@ -529,7 +557,7 @@ void ServersBackupController::uploadBackup(const ServerCredentials &credentials, filename = localFileInfo.fileName(); } #else - // Для других платформ используем обычную проверку + // For other platforms use regular check QFileInfo localFileInfo(localPath); if (!localFileInfo.exists()) { emit errorOccurred(tr("Local file does not exist: %1").arg(localPath), ErrorCode::InternalError); @@ -562,7 +590,16 @@ void ServersBackupController::uploadBackup(const ServerCredentials &credentials, setProgress(100, tr("Backup uploaded successfully")); emit backupUploaded(remotePath); - // Удаляем временный файл после успешной загрузки + // If auto restore enabled, start it + if (m_autoRestoreAfterUpload) { + m_autoRestoreAfterUpload = false; // Reset flag + + qDebug() << "Auto-starting restore after upload"; + QString backupFilename = remotePath.split('/').last(); + restoreBackup(m_pendingRestoreCredentials, backupFilename, QStringList(), m_restoreReplaceMode); + } + + // Delete temp file after successful upload if (m_tempUploadFile) { qDebug() << "Removing temp file:" << m_tempUploadFile->fileName(); m_tempUploadFile->remove(); @@ -574,7 +611,7 @@ void ServersBackupController::uploadBackup(const ServerCredentials &credentials, qDebug() << "Upload failed with error code:" << static_cast(error); emit errorOccurred(tr("Failed to upload backup: error code %1").arg(static_cast(error)), error); - // Удаляем временный файл при ошибке + // Delete temp file on error if (m_tempUploadFile) { m_tempUploadFile->remove(); delete m_tempUploadFile; @@ -583,14 +620,14 @@ void ServersBackupController::uploadBackup(const ServerCredentials &credentials, } } -// Перегруженный метод для setup wizard с отдельными параметрами credentials +// Overloaded method for setup wizard with separate credential parameters void ServersBackupController::uploadBackupWithStrings(const QString &hostname, const QString &username, const QString &secretData, const QString &localPath, bool replaceMode) { - // Создаем ServerCredentials из строк + // Create ServerCredentials from strings ServerCredentials credentials; credentials.hostName = hostname; credentials.userName = username; @@ -599,7 +636,7 @@ void ServersBackupController::uploadBackupWithStrings(const QString &hostname, qDebug() << "uploadBackupWithStrings called with hostname:" << hostname << "username:" << username; - // Вызываем основной метод + // Call main method uploadBackup(credentials, localPath, replaceMode); } @@ -609,11 +646,11 @@ QStringList ServersBackupController::scanBackupForContainers(const QString &loca qDebug() << "Scanning backup file for containers:" << localPath; - // Для Android URI или обычного пути используем tar для просмотра содержимого + // For Android URI or regular path use tar to view contents #ifdef Q_OS_ANDROID QString actualPath = localPath; if (localPath.startsWith("content://")) { - // Для Android URI нужно сначала прочитать файл + // For Android URI need to read file first int fd = AndroidController::instance()->getFd(localPath); if (fd < 0) { qWarning() << "Failed to get file descriptor for Android URI"; @@ -631,7 +668,7 @@ QStringList ServersBackupController::scanBackupForContainers(const QString &loca file.close(); AndroidController::instance()->closeFd(); - // Сохраняем во временный файл + // Save to temporary file actualPath = QDir::temp().filePath("backup_scan_temp.tgz"); QFile tempFile(actualPath); if (!tempFile.open(QIODevice::WriteOnly)) { @@ -645,7 +682,7 @@ QStringList ServersBackupController::scanBackupForContainers(const QString &loca QString actualPath = localPath; #endif - // Выполняем команду tar для просмотра содержимого + // Execute tar command to view contents QProcess process; process.start("tar", QStringList() << "-tzf" << actualPath); process.waitForFinished(5000); @@ -658,11 +695,11 @@ QStringList ServersBackupController::scanBackupForContainers(const QString &loca QString output = process.readAllStandardOutput(); QStringList lines = output.split('\n', Qt::SkipEmptyParts); - // Ищем директории контейнеров (amnezia-*) + // Find container directories (amnezia-*) QSet foundContainers; for (const QString &line : lines) { if (line.contains("amnezia-")) { - // Извлекаем имя контейнера из пути + // Extract container name from path QStringList parts = line.split('/'); for (const QString &part : parts) { if (part.startsWith("amnezia-")) { @@ -690,9 +727,9 @@ void ServersBackupController::deleteBackup(const ServerCredentials &credentials, setStatus(InProgress); setProgress(0, tr("Deleting backup...")); - // Экранируем имя файла для безопасного использования в bash + // Escape filename for safe use in bash QString escapedFilename = backupFilename; - escapedFilename.replace("'", "'\\''"); // Экранируем одинарные кавычки + escapedFilename.replace("'", "'\\''"); // Escape single quotes QString script = QString("sudo rm -f '%1/%2'").arg(m_backupDir).arg(escapedFilename); m_currentOutput.clear(); @@ -720,13 +757,13 @@ void ServersBackupController::deleteBackup(const ServerCredentials &credentials, } // ============================================================================ -// ВСТРОЕННЫЕ BASH СКРИПТЫ +// EMBEDDED BASH SCRIPTS // ============================================================================ QString ServersBackupController::getBackupScript(const QString &ipAddress) const { - // Упрощенная версия bash скрипта, встроенная в C++ - // Формат имени файла: IP_ADDRESS - DD-MM-YYYY_HH-MM-SS.tgz + // Simplified bash script version, embedded in C++ + // Filename format: IP_ADDRESS - DD-MM-YYYY_HH-MM-SS.tgz return QString(R"( #!/bin/bash set -e @@ -740,10 +777,10 @@ BACKUP_SUBDIR="$BACKUP_DIR/backup_temp_$$" echo "[INFO] Starting backup..." -# Создание директории +# Create directory mkdir -p "$BACKUP_SUBDIR" -# Список контейнеров Amnezia +# List of Amnezia containers CONTAINERS=( "amnezia-awg" "amnezia-awg2" @@ -755,21 +792,21 @@ CONTAINERS=( "amnezia-shadowsocks" ) -# Backup каждого контейнера (включая остановленные) +# Backup each container (including stopped) for container in "${CONTAINERS[@]}"; do if sudo docker ps -a --format '{{.Names}}' | grep -q "^$container$"; then echo "[INFO] Backing up $container..." mkdir -p "$BACKUP_SUBDIR/$container" - # Копируем /opt/amnezia + # Copy /opt/amnezia sudo docker cp "$container:/opt/amnezia" "$BACKUP_SUBDIR/$container/" 2>/dev/null || true - # Сохраняем метаданные + # Save metadata sudo docker inspect "$container" > "$BACKUP_SUBDIR/$container/container_inspect.json" 2>/dev/null || true fi done -# Создание архива +# Create archive cd "$BACKUP_DIR" tar -czf "$BACKUP_FILENAME" -C "$BACKUP_SUBDIR" . 2>/dev/null rm -rf "$BACKUP_SUBDIR" @@ -782,8 +819,8 @@ QString ServersBackupController::getContainerBackupScript(DockerContainer contai { QString containerName = ContainerProps::containerToString(container); - // Backup конкретного контейнера напрямую через docker cp - // Формат имени файла: IP_ADDRESS - DD-MM-YYYY_HH-MM-SS.tgz + // Backup specific container directly via docker cp + // Filename format: IP_ADDRESS - DD-MM-YYYY_HH-MM-SS.tgz return QString(R"( #!/bin/bash set -e @@ -798,35 +835,35 @@ BACKUP_SUBDIR="$BACKUP_DIR/backup_temp_$$" echo "[INFO] Starting backup for container: $CONTAINER_NAME..." -# Проверка существования контейнера +# Check container exists if ! sudo docker ps -a --format '{{.Names}}' | grep -q "^$CONTAINER_NAME$"; then echo "[ERROR] Container $CONTAINER_NAME does not exist" exit 1 fi -# Создание директории +# Create directory mkdir -p "$BACKUP_SUBDIR/$CONTAINER_NAME" -# Backup конфигураций из контейнера напрямую +# Backup configurations from container directly echo "[INFO] Copying /opt/amnezia from container..." sudo docker cp "$CONTAINER_NAME:/opt/amnezia" "$BACKUP_SUBDIR/$CONTAINER_NAME/" 2>/dev/null || { echo "[WARN] Failed to copy /opt/amnezia, trying alternative paths..." - # Альтернативные пути для разных типов контейнеров + # Alternative paths for different container types sudo docker cp "$CONTAINER_NAME:/etc/openvpn" "$BACKUP_SUBDIR/$CONTAINER_NAME/" 2>/dev/null || true sudo docker cp "$CONTAINER_NAME:/etc/wireguard" "$BACKUP_SUBDIR/$CONTAINER_NAME/" 2>/dev/null || true sudo docker cp "$CONTAINER_NAME:/etc/ipsec.d" "$BACKUP_SUBDIR/$CONTAINER_NAME/" 2>/dev/null || true sudo docker cp "$CONTAINER_NAME:/etc/xray" "$BACKUP_SUBDIR/$CONTAINER_NAME/" 2>/dev/null || true } -# Сохранение метаданных контейнера +# Save container metadata echo "[INFO] Saving container metadata..." sudo docker inspect "$CONTAINER_NAME" > "$BACKUP_SUBDIR/$CONTAINER_NAME/container_inspect.json" 2>/dev/null || true -# Сохранение конфигурации сети +# Save network configuration sudo docker network inspect $(sudo docker inspect -f '{{range $k, $v := .NetworkSettings.Networks}}{{$k}} {{end}}' "$CONTAINER_NAME" | awk '{print $1}') \ > "$BACKUP_SUBDIR/$CONTAINER_NAME/network_config.json" 2>/dev/null || true -# Создание архива +# Create archive cd "$BACKUP_DIR" tar -czf "$BACKUP_FILENAME" -C "$BACKUP_SUBDIR" . 2>/dev/null rm -rf "$BACKUP_SUBDIR" @@ -843,7 +880,7 @@ QString ServersBackupController::getContainersBackupScript(const QList/dev/null || { echo "[WARN] Failed to copy /opt/amnezia from $container, trying alternative paths..." sudo docker cp "$container:/etc/openvpn" "$BACKUP_SUBDIR/$container/" 2>/dev/null || true @@ -878,14 +915,14 @@ for container in "${CONTAINERS[@]}"; do sudo docker cp "$container:/etc/xray" "$BACKUP_SUBDIR/$container/" 2>/dev/null || true } - # Сохраняем метаданные + # Save metadata sudo docker inspect "$container" > "$BACKUP_SUBDIR/$container/container_inspect.json" 2>/dev/null || true else echo "[WARN] Container $container does not exist, skipping..." fi done -# Создание архива +# Create archive cd "$BACKUP_DIR" tar -czf "$BACKUP_FILENAME" -C "$BACKUP_SUBDIR" . 2>/dev/null rm -rf "$BACKUP_SUBDIR" @@ -898,11 +935,11 @@ QString ServersBackupController::getRestoreScript(const QString &backupFilename, const QStringList &containers, bool replaceMode) const { - Q_UNUSED(containers); // TODO: Использовать для выборочного восстановления + Q_UNUSED(containers); // TODO: Use for selective restore - // Экранируем имя файла для безопасного использования в bash + // Escape filename for safe use in bash QString escapedFilename = backupFilename; - escapedFilename.replace("'", "'\\''"); // Экранируем одинарные кавычки + escapedFilename.replace("'", "'\\''"); // Escape single quotes return QString(R"( #!/bin/bash @@ -920,13 +957,13 @@ else echo "[INFO] Using add mode: data will be added to existing containers" fi -# Проверка существования файла backup +# Check backup file exists if [ ! -f "$BACKUP_DIR/$BACKUP_FILE" ]; then echo "[ERROR] Backup file not found: $BACKUP_DIR/$BACKUP_FILE" exit 1 fi -# Извлечение backup +# Extract backup mkdir -p "$TEMP_DIR" EXTRACT_OUTPUT=$(tar -xzf "$BACKUP_DIR/$BACKUP_FILE" -C "$TEMP_DIR" 2>&1) EXTRACT_EXIT_CODE=$? @@ -936,12 +973,12 @@ if [ $EXTRACT_EXIT_CODE -ne 0 ]; then exit 1 fi -# Ищем директорию с контейнерами (может быть backup_temp_* или просто контейнеры напрямую) +# Find directory with containers (may be backup_temp_* or containers directly) BACKUP_SUBDIR=$(ls -d "$TEMP_DIR"/backup_* 2>/dev/null | head -1) -# Если не нашли backup_*, проверяем, есть ли директории контейнеров напрямую +# If didn't find backup_*, check if container directories exist directly if [ -z "$BACKUP_SUBDIR" ]; then - # Проверяем, есть ли директории контейнеров (amnezia-*) напрямую в TEMP_DIR + # Check if container directories (amnezia-*) exist directly in TEMP_DIR if ls -d "$TEMP_DIR"/amnezia-* 2>/dev/null | head -1 > /dev/null; then BACKUP_SUBDIR="$TEMP_DIR" else @@ -953,7 +990,7 @@ if [ -z "$BACKUP_SUBDIR" ]; then fi fi -# Восстановление каждого контейнера +# Restore each container for container_dir in "$BACKUP_SUBDIR"/*; do if [ ! -d "$container_dir" ]; then continue @@ -964,34 +1001,34 @@ for container_dir in "$BACKUP_SUBDIR"/*; do if sudo docker ps -a --format '{{.Names}}' | grep -q "^$container_name$"; then echo "[INFO] Restoring $container_name..." - # Остановка контейнера + # Stop container sudo docker stop "$container_name" 2>/dev/null || true - # Режим замены: очистка контейнера перед восстановлением + # Replace mode: clear container before restore if [ "$REPLACE_MODE" = "1" ]; then echo "[INFO] Clearing container $container_name before restore..." - # Создаем пустую директорию для очистки /opt/amnezia + # Create empty directory to clear /opt/amnezia TEMP_CLEAR_DIR="/tmp/clear_amnezia_$$" mkdir -p "$TEMP_CLEAR_DIR/amnezia" - # Копируем пустую директорию, что удалит старое содержимое + # Copy empty directory, which will delete old contents sudo docker cp "$TEMP_CLEAR_DIR/amnezia" "$container_name:/opt/" 2>/dev/null || true - # Удаляем временную директорию + # Delete temporary directory rm -rf "$TEMP_CLEAR_DIR" fi - # Восстановление /opt/amnezia + # Restore /opt/amnezia if [ -d "$container_dir/amnezia" ]; then sudo docker cp "$container_dir/amnezia" "$container_name:/opt/" 2>/dev/null || true fi - # Запуск контейнера + # Start container sudo docker start "$container_name" 2>/dev/null || true echo "[INFO] $container_name restored" fi done -# Очистка +# Cleanup rm -rf "$TEMP_DIR" echo "[INFO] Restore completed successfully" @@ -1007,12 +1044,12 @@ BACKUP_DIR=%1 echo "Backup directory: $BACKUP_DIR" -# Проверка наличия backup +# Check backup availability BACKUPS=$(ls -t "$BACKUP_DIR"/backup_*.tar.gz 2>/dev/null | wc -l) echo "Total backups: $BACKUPS" if [ "$BACKUPS" -gt 0 ]; then - # Информация о последнем backup + # Last backup information LATEST=$(ls -t "$BACKUP_DIR"/backup_*.tar.gz 2>/dev/null | head -1) if [ -n "$LATEST" ]; then echo "Latest backup: $(basename "$LATEST")" @@ -1021,7 +1058,7 @@ if [ "$BACKUPS" -gt 0 ]; then fi fi -# Проверка запущенных контейнеров +# Check running containers echo "Running containers:" sudo docker ps --filter "name=amnezia-" --format '{{.Names}}' 2>/dev/null @@ -1040,17 +1077,13 @@ ls -lht "$BACKUP_DIR"/backup_*.tar.gz 2>/dev/null || echo "No backups found" )").arg(m_backupDir); } -// ============================================================================ -// ПАРСИНГ ВЫВОДА -// ============================================================================ - QList ServersBackupController::parseBackupList(const QString &output) { QList backups; QStringList lines = output.split('\n', Qt::SkipEmptyParts); - // Парсим вывод ls -lht + // Parse ls -lht output QRegularExpression re("^-.*\\s+(\\d+)\\s+\\w+\\s+\\d+\\s+([\\d:]+)\\s+(.+backup_(\\d{8}_\\d{6})\\.tar\\.gz)$"); for (const QString &line : lines) { @@ -1061,7 +1094,7 @@ QList ServersBackupController::parseBackupL info.filename = QFileInfo(match.captured(3)).fileName(); info.fullPath = match.captured(3); - // Парсим дату из имени файла + // Parse date from filename QString dateStr = match.captured(4); info.createdAt = QDateTime::fromString(dateStr, "yyyyMMdd_HHmmss"); info.isValid = true; @@ -1077,18 +1110,18 @@ QJsonObject ServersBackupController::parseBackupStatus(const QString &output) { QJsonObject status; - // Парсим текстовый вывод + // Parse text output status["raw_output"] = output; status["has_backups"] = output.contains("Total backups:"); - // Извлекаем количество backup + // Extract backup count QRegularExpression reTotal("Total backups: (\\d+)"); QRegularExpressionMatch matchTotal = reTotal.match(output); if (matchTotal.hasMatch()) { status["total_backups"] = matchTotal.captured(1).toInt(); } - // Извлекаем информацию о последнем backup + // Extract last backup information QRegularExpression reLatest("Latest backup: (.+)"); QRegularExpressionMatch matchLatest = reLatest.match(output); if (matchLatest.hasMatch()) { @@ -1099,7 +1132,7 @@ QJsonObject ServersBackupController::parseBackupStatus(const QString &output) } // ============================================================================ -// ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ +// HELPER METHODS // ============================================================================ ErrorCode ServersBackupController::handleStdOut(const QString &data, QString &output) @@ -1107,13 +1140,13 @@ ErrorCode ServersBackupController::handleStdOut(const QString &data, QString &ou output += data; qDebug().noquote() << "[BACKUP]" << data; - // Проверяем на ошибки в выводе + // Check for errors in output if (data.contains("[ERROR]") || data.contains("ERROR")) { - // Ошибка обнаружена в stdout, но это не критично для handleStdOut - // Основная проверка будет в restoreBackup после выполнения скрипта + // Error detected in stdout, but not critical for handleStdOut + // Main check will be in restoreBackup after script execution } - // Обновляем прогресс на основе вывода + // Update progress based on output if (data.contains("Starting backup")) { setProgress(10, tr("Starting backup...")); } else if (data.contains("Backing up")) { @@ -1151,3 +1184,283 @@ void ServersBackupController::setProgress(int percent, const QString &message) emit progressChanged(percent, message); } +void ServersBackupController::createBackupWithDownload(bool downloadToDevice, bool deleteFromServer) +{ + qDebug() << "createBackupWithDownload: download=" << downloadToDevice << "delete=" << deleteFromServer; + + if (!m_serversModel) { + emit errorOccurred(tr("ServersModel is not available"), ErrorCode::InternalError); + return; + } + + int serverIndex = m_serversModel->getProcessedServerIndex(); + if (serverIndex < 0) { + emit errorOccurred(tr("No server selected"), ErrorCode::InternalError); + return; + } + + ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex); + + // Set flags for automatic download and delete + m_autoDownloadAfterCreate = downloadToDevice; + m_autoDeleteAfterDownload = deleteFromServer; + + // Create backup + createBackup(credentials); +} + +QVariantMap ServersBackupController::getBackupFileInfo(const QString &backupFilePath) +{ + QVariantMap result; + + // Get filename + QString fileName; + +#ifdef Q_OS_ANDROID + if (backupFilePath.startsWith("content://")) { + fileName = AndroidController::instance()->getFileName(backupFilePath); + } +#endif +#ifdef Q_OS_IOS + if (backupFilePath.startsWith("file://")) { + fileName = IosController::getFileName(backupFilePath); + } +#endif + + if (fileName.isEmpty()) { + QFileInfo fileInfo(backupFilePath); + fileName = fileInfo.fileName(); + } + + // If filename is empty, use fallback + if (fileName.isEmpty()) { + QStringList pathParts = backupFilePath.split('/'); + if (!pathParts.isEmpty()) { + fileName = pathParts.last(); + } + } + + if (fileName.isEmpty()) { + fileName = "backup.tgz"; + } + + // Extract IP from filename (format: 38_99_23_227 - DD-MM-YYYY_HH-MM-SS.tgz) + QString serverIp; + QRegularExpression ipRegex("^([\\d_]+)\\s*-"); + QRegularExpressionMatch match = ipRegex.match(fileName); + if (match.hasMatch()) { + serverIp = match.captured(1).replace('_', '.'); + } + + // If failed to extract IP, use empty string + // QML will decide what to use + + result["fileName"] = fileName; + result["serverIp"] = serverIp; + + qDebug() << "getBackupFileInfo:" << backupFilePath; + qDebug() << " fileName:" << fileName; + qDebug() << " serverIp:" << serverIp; + + return result; +} + +void ServersBackupController::prepareRestoreFromBackup(const QString &backupFilePath, + const QString &hostname, + const QString &username, + const QString &secretData) +{ + qDebug() << "Preparing restore from backup:" << backupFilePath; + qDebug() << " hostname:" << hostname; + qDebug() << " username:" << username; + + // 1. Scan backup to determine containers + QStringList containers = scanBackupForContainers(backupFilePath); + + if (containers.isEmpty()) { + qWarning() << "No containers found in backup"; + emit errorOccurred(tr("No containers found in backup file"), ErrorCode::InternalError); + return; + } + + qDebug() << "Found containers in backup:" << containers; + + // 2. Extract information from filename + QString fileName; +#ifdef Q_OS_ANDROID + if (backupFilePath.startsWith("content://")) { + fileName = AndroidController::instance()->getFileName(backupFilePath); + } +#endif +#ifdef Q_OS_IOS + if (backupFilePath.startsWith("file://")) { + fileName = IosController::getFileName(backupFilePath); + } +#endif + + if (fileName.isEmpty()) { + QFileInfo fileInfo(backupFilePath); + fileName = fileInfo.fileName(); + } + + // Extract IP from filename (format: 38_99_23_227 - DD-MM-YYYY_HH-MM-SS.tgz) + QString serverIp = hostname; // Default to hostname + QRegularExpression ipRegex("^([\\d_]+)\\s*-"); + QRegularExpressionMatch match = ipRegex.match(fileName); + if (match.hasMatch()) { + serverIp = match.captured(1).replace('_', '.'); + } + + qDebug() << "Extracted info - fileName:" << fileName << "serverIp:" << serverIp; + + // 3. Send signal with data for container installation + // QML will receive this signal and install containers via InstallController + emit readyForRestore(backupFilePath, hostname, username, secretData, serverIp, fileName); +} + +void ServersBackupController::startRestore(bool isFromSetupWizard, + const QString &backupFilePath, + bool replaceMode, + const QString &wizardHostname, + const QString &wizardUsername, + const QString &wizardSecretData) +{ + qDebug() << "Starting restore: isFromSetupWizard=" << isFromSetupWizard + << "replaceMode=" << replaceMode + << "backupFilePath=" << backupFilePath; + + // Enable auto restore flag after upload + m_autoRestoreAfterUpload = true; + + // If this is setup wizard with wizard credentials, use them directly + if (isFromSetupWizard && !wizardHostname.isEmpty()) { + qDebug() << "Setup wizard mode, using uploadBackupWithStrings"; + qDebug() << " hostname:" << wizardHostname; + qDebug() << " username:" << wizardUsername; + + // Save credentials for subsequent restore + m_pendingRestoreCredentials.hostName = wizardHostname; + m_pendingRestoreCredentials.userName = wizardUsername; + m_pendingRestoreCredentials.secretData = wizardSecretData; + + uploadBackupWithStrings(wizardHostname, wizardUsername, wizardSecretData, backupFilePath, replaceMode); + } else { + // Regular mode - get credentials from ServersModel + qDebug() << "Regular mode, getting credentials from ServersModel"; + + if (!m_serversModel) { + qWarning() << "ServersModel is null"; + emit errorOccurred(tr("Internal error: ServersModel is not available"), ErrorCode::InternalError); + return; + } + + int serverIndex = m_serversModel->getProcessedServerIndex(); + qDebug() << " ProcessedServerIndex:" << serverIndex; + qDebug() << " ServersCount:" << m_serversModel->getServersCount(); + + if (serverIndex < 0) { + qWarning() << "No processed server selected, trying default server"; + serverIndex = m_serversModel->getDefaultServerIndex(); + qDebug() << " DefaultServerIndex:" << serverIndex; + + if (serverIndex < 0) { + qWarning() << "No default server either"; + emit errorOccurred(tr("No server selected"), ErrorCode::InternalError); + return; + } + } + + qDebug() << " Using server index:" << serverIndex; + ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex); + + // Save credentials for subsequent restore + m_pendingRestoreCredentials = credentials; + + uploadBackup(credentials, backupFilePath, replaceMode); + } +} + +bool ServersBackupController::setDefaultServerAfterRestore(bool isFromSetupWizard) +{ + if (!m_serversModel) { + qWarning() << "ServersModel is null, cannot set default server"; + return false; + } + + if (m_serversModel->getServersCount() == 0) { + qWarning() << "No servers in model"; + return false; + } + + // For setup wizard, set last added server as default + if (isFromSetupWizard) { + int serverIdx = m_serversModel->getServersCount() - 1; + qDebug() << "Setting default server after restore:" << serverIdx; + m_serversModel->setDefaultServerIndex(serverIdx); + m_serversModel->setProcessedServerIndex(serverIdx); + + // Reset retry counter + m_containerRetryCount = 0; + + // Start timer to set default container + QTimer::singleShot(500, this, &ServersBackupController::trySetDefaultContainer); + + return true; + } + + return false; +} + +void ServersBackupController::trySetDefaultContainer() +{ + if (!m_serversModel) { + qWarning() << "ServersModel is null"; + emit defaultServerAndContainerSet(); + return; + } + + int serverIdx = m_serversModel->getServersCount() - 1; + qDebug() << "Timer: Setting default container (attempt" << m_containerRetryCount + 1 << "/" << m_maxContainerRetries << ")"; + + // Get server configuration + QJsonObject serverConfig = m_serversModel->getServerConfig(serverIdx); + QJsonArray containers = serverConfig.value(config_key::containers).toArray(); + + qDebug() << " Total containers:" << containers.size(); + + if (containers.size() > 0) { + // Find first installed container + for (int i = 0; i < containers.size(); i++) { + QJsonObject containerObj = containers.at(i).toObject(); + + // Check if container has any data (meaning it's installed) + DockerContainer container = ContainerProps::containerFromString(containerObj.value(config_key::container).toString()); + if (container != DockerContainer::None && !containerObj.isEmpty()) { + qDebug() << " Setting default container at index:" << i << "for server:" << serverIdx; + m_serversModel->setDefaultContainer(serverIdx, i); + + // Successfully set - send signal + qDebug() << " Default server and container set successfully"; + emit defaultServerAndContainerSet(); + return; + } + } + + // Containers exist, but none match - proceed anyway + qDebug() << " No suitable containers found, but model has data, proceeding"; + emit defaultServerAndContainerSet(); + return; + } + + // Model empty - try again + m_containerRetryCount++; + if (m_containerRetryCount < m_maxContainerRetries) { + qDebug() << " Model is empty, will retry..."; + QTimer::singleShot(500, this, &ServersBackupController::trySetDefaultContainer); + } else { + qDebug() << " Max retries reached, proceeding anyway"; + m_containerRetryCount = 0; + emit defaultServerAndContainerSet(); + } +} + diff --git a/client/ui/controllers/serversBackupController.h b/client/ui/controllers/serversBackupController.h index 4ade4ae76..f253d452b 100644 --- a/client/ui/controllers/serversBackupController.h +++ b/client/ui/controllers/serversBackupController.h @@ -9,6 +9,7 @@ #include class QTemporaryFile; +class ServersModel; #include "core/controllers/serverController.h" #include "core/defs.h" @@ -17,24 +18,24 @@ class QTemporaryFile; using namespace amnezia; /** - * @brief Контроллер для управления backup конфигураций Amnezia VPN + * @brief Controller for managing Amnezia VPN configuration backups * - * Использует существующий ServerController и libssh::Client из Amnezia - * Bash скрипты встроены напрямую в C++ код - * Поддерживает backup конкретных контейнеров напрямую через docker cp + * Uses existing ServerController and libssh::Client from Amnezia + * Bash scripts are embedded directly in C++ code + * Supports direct container backup via docker cp * - * Полностью кроссплатформенный: Windows, macOS, Linux, iOS, Android + * Fully cross-platform: Windows, macOS, Linux, iOS, Android */ class ServersBackupController : public QObject { Q_OBJECT public: - explicit ServersBackupController(std::shared_ptr settings, QObject *parent = nullptr); + explicit ServersBackupController(std::shared_ptr settings, ServersModel *serversModel, QObject *parent = nullptr); ~ServersBackupController(); /** - * @brief Информация о backup + * @brief Backup information */ struct BackupInfo { QString filename; @@ -55,44 +56,52 @@ public: public slots: /** - * @brief Создать backup на сервере (всех контейнеров) - * @param credentials Учетные данные сервера + * @brief Create backup on server (all containers) + * @param credentials Server credentials */ void createBackup(const ServerCredentials &credentials); + + /** + * @brief Create backup and automatically download to device (for QML) + * @param downloadToDevice Download to device after creation? + * @param deleteFromServer Delete from server after download? + */ + Q_INVOKABLE void createBackupWithDownload(bool downloadToDevice = true, + bool deleteFromServer = true); /** - * @brief Создать backup конкретного контейнера - * @param credentials Учетные данные сервера - * @param container Тип контейнера для backup + * @brief Create backup of specific container + * @param credentials Server credentials + * @param container Container type for backup */ void createContainerBackup(const ServerCredentials &credentials, DockerContainer container); /** - * @brief Создать backup конкретного контейнера по имени - * @param credentials Учетные данные сервера - * @param containerName Имя контейнера (например "amnezia-awg") + * @brief Create backup of specific container by name + * @param credentials Server credentials + * @param containerName Container name (e.g. "amnezia-awg") */ void createBackupByName(const ServerCredentials &credentials, const QString &containerName); /** - * @brief Создать backup нескольких контейнеров - * @param credentials Учетные данные сервера - * @param containers Список контейнеров для backup + * @brief Create backup of multiple containers + * @param credentials Server credentials + * @param containers List of containers for backup */ void createContainersBackup(const ServerCredentials &credentials, const QList &containers); /** - * @brief Получить список backup с сервера - * @param credentials Учетные данные сервера + * @brief Get list of backups from server + * @param credentials Server credentials */ void fetchBackupList(const ServerCredentials &credentials); /** - * @brief Восстановить из backup - * @param credentials Учетные данные сервера - * @param backupFilename Имя файла backup - * @param containers Список контейнеров (пустой = все) - * @param replaceMode Если true - сначала очищает контейнер, затем восстанавливает. Если false - добавляет данные поверх существующих + * @brief Restore from backup + * @param credentials Server credentials + * @param backupFilename Backup file name + * @param containers List of containers (empty = all) + * @param replaceMode If true - clears container first, then restores. If false - adds data on top of existing */ void restoreBackup(const ServerCredentials &credentials, const QString &backupFilename, @@ -100,32 +109,32 @@ public slots: bool replaceMode = false); /** - * @brief Проверить состояние backup на сервере - * @param credentials Учетные данные сервера + * @brief Check backup status on server + * @param credentials Server credentials */ void checkBackupStatus(const ServerCredentials &credentials); /** - * @brief Скачать backup на локальную машину - * @param credentials Учетные данные сервера - * @param backupFilename Имя файла backup - * @param localPath Путь для сохранения + * @brief Download backup to local machine + * @param credentials Server credentials + * @param backupFilename Backup file name + * @param localPath Save path */ void downloadBackup(const ServerCredentials &credentials, const QString &backupFilename, const QString &localPath); /** - * @brief Загрузить backup на сервер - * @param credentials Учетные данные сервера - * @param localPath Путь к локальному файлу - * @param replaceMode Режим восстановления (true = замена, false = добавление). Сохраняется для последующего использования в restoreBackup + * @brief Upload backup to server + * @param credentials Server credentials + * @param localPath Path to local file + * @param replaceMode Restore mode (true = replace, false = add). Saved for later use in restoreBackup */ void uploadBackup(const ServerCredentials &credentials, const QString &localPath, bool replaceMode = false); - // Перегруженный метод для setup wizard с отдельными параметрами credentials + // Overloaded method for setup wizard with separate credential parameters Q_INVOKABLE void uploadBackupWithStrings(const QString &hostname, const QString &username, const QString &secretData, @@ -133,154 +142,247 @@ public slots: bool replaceMode = false); /** - * @brief Сканировать backup файл и определить какие контейнеры в нем есть - * @param localPath Путь к локальному backup файлу - * @return Список имен контейнеров найденных в backup + * @brief Universal method to start restore (from QML) + * Automatically selects correct path depending on parameters + * @param isFromSetupWizard Restore from setup wizard? + * @param backupFilePath Path to local backup file + * @param replaceMode Restore mode (true = replace, false = add) + * @param wizardHostname Hostname for setup wizard (optional) + * @param wizardUsername Username for setup wizard (optional) + * @param wizardSecretData Secret data for setup wizard (optional) + */ + Q_INVOKABLE void startRestore(bool isFromSetupWizard, + const QString &backupFilePath, + bool replaceMode, + const QString &wizardHostname = QString(), + const QString &wizardUsername = QString(), + const QString &wizardSecretData = QString()); + + /** + * @brief Prepare restore information from backup file + * Parses filename, extracts IP, prepares metadata + * @param backupFilePath Path to backup file + * @return QVariantMap with keys: fileName, serverIp + */ + Q_INVOKABLE QVariantMap getBackupFileInfo(const QString &backupFilePath); + + /** + * @brief Scan backup file and determine which containers it contains + * @param localPath Path to local backup file + * @return List of container names found in backup */ Q_INVOKABLE QStringList scanBackupForContainers(const QString &localPath); + + /** + * @brief Set default server and container after restore (for setup wizard) + * @param isFromSetupWizard Was restore called from setup wizard + * @return true if successful, false if no servers or containers + */ + Q_INVOKABLE bool setDefaultServerAfterRestore(bool isFromSetupWizard); + + /** + * @brief Install containers from backup on empty server (for setup wizard) + * Scans backup, adds empty server and sends signal to install containers + * @param backupFilePath Path to local backup file + * @param hostname Server hostname + * @param username Username for SSH + * @param secretData Password/key for SSH + */ + Q_INVOKABLE void prepareRestoreFromBackup(const QString &backupFilePath, + const QString &hostname, + const QString &username, + const QString &secretData); /** - * @brief Удалить backup с сервера - * @param credentials Учетные данные сервера - * @param backupFilename Имя файла backup + * @brief Delete backup from server + * @param credentials Server credentials + * @param backupFilename Backup file name */ void deleteBackup(const ServerCredentials &credentials, const QString &backupFilename); /** - * @brief Установить директорию backup на сервере + * @brief Set backup directory on server */ void setBackupDirectory(const QString &directory); /** - * @brief Получить директорию backup + * @brief Get backup directory */ QString backupDirectory() const { return m_backupDir; } signals: /** - * @brief Изменился статус операции + * @brief Operation status changed */ void statusChanged(BackupStatus status); /** - * @brief Прогресс операции (0-100) + * @brief Operation progress (0-100) */ void progressChanged(int percent, const QString &message); /** - * @brief Получен список backup + * @brief Backup list received */ void backupListReceived(const QList &backups); /** - * @brief Backup создан успешно + * @brief Backup created successfully */ void backupCreated(const QString &backupFilename); /** - * @brief Backup восстановлен успешно + * @brief Backup restored successfully */ void backupRestored(); + + /** + * @brief Need to set default server and container (for setup wizard) + * This signal is sent after backupRestored() if restore was from setup wizard + */ + void needSetDefaultServer(); + + /** + * @brief Default server and container successfully set + * Can navigate to result page + */ + void defaultServerAndContainerSet(); + + /** + * @brief All containers from backup installed + * Can proceed to data restore + * @param backupFilePath Path to backup file + * @param hostname Hostname + * @param username Username + * @param secretData Secret data + * @param serverIp IP address (for display) + * @param fileName File name (for display) + */ + void readyForRestore(const QString &backupFilePath, + const QString &hostname, + const QString &username, + const QString &secretData, + const QString &serverIp, + const QString &fileName); /** - * @brief Backup скачан + * @brief Backup downloaded */ void backupDownloaded(const QString &localPath); /** - * @brief Backup загружен на сервер + * @brief Backup uploaded to server */ void backupUploaded(const QString &serverPath); /** - * @brief Получена информация о состоянии backup + * @brief Backup status information received */ void backupStatusReceived(const QJsonObject &status); /** - * @brief Произошла ошибка + * @brief Error occurred */ void errorOccurred(const QString &errorMessage, ErrorCode errorCode); private: /** - * @brief Получить bash скрипт для создания backup всех контейнеров - * @param ipAddress IP адрес сервера в формате с подчеркиваниями (например "192_119_110_11") + * @brief Get bash script for creating backup of all containers + * @param ipAddress Server IP address in underscored format (e.g. "192_119_110_11") */ QString getBackupScript(const QString &ipAddress) const; /** - * @brief Получить bash скрипт для создания backup конкретного контейнера - * @param container Тип контейнера - * @param ipAddress IP адрес сервера в формате с подчеркиваниями (например "192_119_110_11") + * @brief Get bash script for creating backup of specific container + * @param container Container type + * @param ipAddress Server IP address in underscored format (e.g. "192_119_110_11") */ QString getContainerBackupScript(DockerContainer container, const QString &ipAddress) const; /** - * @brief Получить bash скрипт для создания backup нескольких контейнеров - * @param containers Список контейнеров - * @param ipAddress IP адрес сервера в формате с подчеркиваниями (например "192_119_110_11") + * @brief Get bash script for creating backup of multiple containers + * @param containers List of containers + * @param ipAddress Server IP address in underscored format (e.g. "192_119_110_11") */ QString getContainersBackupScript(const QList &containers, const QString &ipAddress) const; /** - * @brief Получить bash скрипт для восстановления - * @param backupFilename Имя файла backup - * @param containers Список контейнеров - * @param replaceMode Если true - сначала очищает контейнер, затем восстанавливает + * @brief Get bash script for restore + * @param backupFilename Backup file name + * @param containers List of containers + * @param replaceMode If true - clears container first, then restores */ QString getRestoreScript(const QString &backupFilename, const QStringList &containers, bool replaceMode = false) const; /** - * @brief Получить bash скрипт для проверки состояния + * @brief Get bash script for status check */ QString getCheckStatusScript() const; /** - * @brief Получить bash скрипт для списка backup + * @brief Get bash script for backup list */ QString getListBackupsScript() const; /** - * @brief Парсить список backup из вывода + * @brief Parse backup list from output */ QList parseBackupList(const QString &output); /** - * @brief Парсить статус из вывода + * @brief Parse status from output */ QJsonObject parseBackupStatus(const QString &output); /** - * @brief Обработать стандартный вывод + * @brief Handle standard output */ ErrorCode handleStdOut(const QString &data, QString &output); /** - * @brief Обработать вывод ошибок + * @brief Handle error output */ ErrorCode handleStdErr(const QString &data, QString &error); /** - * @brief Установить статус + * @brief Set status */ void setStatus(BackupStatus status); /** - * @brief Установить прогресс + * @brief Set progress */ void setProgress(int percent, const QString &message); + + /** + * @brief Attempt to set default container (called from timer) + */ + void trySetDefaultContainer(); private: std::shared_ptr m_settings; + ServersModel *m_serversModel; ServerController *m_serverController; BackupStatus m_status; QString m_backupDir; QString m_currentOutput; QString m_currentError; - bool m_restoreReplaceMode; // Сохраняем режим восстановления для использования после uploadBackup - QTemporaryFile *m_tempUploadFile; // Временный файл для Android URI (чтобы не удалялся до завершения загрузки) + bool m_restoreReplaceMode; // Save restore mode for use after uploadBackup + QTemporaryFile *m_tempUploadFile; // Temp file for Android URI (to prevent deletion before upload completes) + + // For setting default container + int m_containerRetryCount; + static constexpr int m_maxContainerRetries = 3; + + // For automatic restore after upload + ServerCredentials m_pendingRestoreCredentials; + bool m_autoRestoreAfterUpload; + + // For automatic backup download/delete + bool m_autoDownloadAfterCreate; + bool m_autoDeleteAfterDownload; + QString m_lastCreatedBackupFilename; }; #endif // SERVERSBACKUPCONTROLLER_H diff --git a/client/ui/qml/Pages2/PageSettingsServerBackup.qml b/client/ui/qml/Pages2/PageSettingsServerBackup.qml index fcdc76891..f8b4eb1d8 100644 --- a/client/ui/qml/Pages2/PageSettingsServerBackup.qml +++ b/client/ui/qml/Pages2/PageSettingsServerBackup.qml @@ -116,18 +116,9 @@ PageType { } } - // ============ Backup Functions ============ - - function getServerCredentials() { - var index = ServersModel.processedIndex - return ServersModel.getServerCredentials(index) - } - - property bool downloadAfterCreate: false - function createBackup(shouldDownload) { - // По умолчанию shouldDownload = true, если не указано - downloadAfterCreate = (shouldDownload !== undefined) ? shouldDownload : true + // Default shouldDownload = true + var downloadAfterCreate = (shouldDownload !== undefined) ? shouldDownload : true var headerText = downloadAfterCreate ? qsTr("Create backup and download to device?") : @@ -140,21 +131,15 @@ PageType { var yesButtonFunction = function() { PageController.showBusyIndicator(true) - - var credentials = getServerCredentials() - // Всегда создаем backup всех контейнеров - ServersBackupController.createBackup(credentials) + // Call C++ method that manages download and delete automatically + ServersBackupController.createBackupWithDownload(downloadAfterCreate, true) } var noButtonFunction = function() {} showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) } - property string selectedBackupForRestore: "" - function restoreBackup() { - // Для мобильных устройств используем все возможные расширения backup файлов - // Android преобразует расширения в MIME типы автоматически var filter = GC.isMobile() ? "*.gz *.tgz *.tar.gz" : "Backup files (*.tar.gz *.backup *.tgz *.gz)" var localPath = SystemController.getFileName( qsTr("Select Backup to Restore"), @@ -168,43 +153,25 @@ PageType { return } - selectedBackupForRestore = localPath + // Get file information via C++ + var fileInfo = ServersBackupController.getBackupFileInfo(localPath) + var fileName = fileInfo.fileName || "backup.tgz" + var serverIp = fileInfo.serverIp || "" - // Открываем страницу выбора режима восстановления + // If IP not found in filename, use current server + if (!serverIp || serverIp.length === 0) { + serverIp = ServersModel.getProcessedServerData("hostName") || "" + } + + var serverName = ServersModel.getProcessedServerData("name") || qsTr("Server") + + // Open restore mode selection page var parentItem = root.parent while (parentItem && parentItem.objectName !== "tabBarStackView") { parentItem = parentItem.parent } + if (parentItem && typeof parentItem.push === "function") { - // Используем SystemController для получения имени файла из пути или URI - // Это правильно обработает Android URI через ContentResolver - var fileName = SystemController.getFileNameFromPath(localPath) - - // Если имя файла пустое или undefined, используем fallback - if (!fileName || fileName === undefined || fileName.length === 0) { - var fallbackName = localPath.split('/').pop() - fileName = (fallbackName && fallbackName.length > 0) ? fallbackName : qsTr("backup.tgz") - } - - // Убеждаемся, что fileName - это строка - fileName = String(fileName) - - // Извлекаем IP адрес из имени файла (формат: IP_ADDRESS - DD-MM-YYYY_HH-MM-SS.tgz) - var serverIp = "" - var ipMatch = fileName.match(/^([\d_]+)\s*-/) - if (ipMatch && ipMatch.length > 1) { - // Заменяем подчеркивания на точки для отображения IP адреса - serverIp = ipMatch[1].replace(/_/g, ".") - } - - // Если не удалось извлечь IP из имени файла, используем IP из credentials - if (!serverIp || serverIp.length === 0) { - var credentials = getServerCredentials() - serverIp = credentials.hostName || "" - } - - var serverName = ServersModel.getProcessedServerData("name") || qsTr("Server") - parentItem.push(PageController.getPagePath(PageEnum.PageSettingsServerRestoreMode), { "backupFilePath": localPath, "backupFileName": fileName, @@ -218,50 +185,23 @@ PageType { // ============ Backup Controller Connections ============ - property string lastCreatedBackupFilename: "" - property string lastUploadedBackupFilename: "" - Connections { target: ServersBackupController function onBackupCreated(backupFilename) { - lastCreatedBackupFilename = backupFilename - - if (downloadAfterCreate) { - var credentials = getServerCredentials() - var localPath = backupFilename - PageController.showNotificationMessage(qsTr("Backup created. Downloading to device...")) - ServersBackupController.downloadBackup(credentials, backupFilename, localPath) - downloadAfterCreate = false - } else { - PageController.showBusyIndicator(false) - PageController.showNotificationMessage(qsTr("Backup created successfully: %1").arg(backupFilename)) - } + // If auto-download is not enabled, show success message + PageController.showBusyIndicator(false) + PageController.showNotificationMessage(qsTr("Backup created successfully: %1").arg(backupFilename)) } function onBackupDownloaded(localPath) { PageController.showBusyIndicator(false) console.log("Backup downloaded to:", localPath) - - if (lastCreatedBackupFilename && lastCreatedBackupFilename.length > 0) { - var credentials = getServerCredentials() - ServersBackupController.deleteBackup(credentials, lastCreatedBackupFilename) - console.log("Deleting backup from server:", lastCreatedBackupFilename) - } - PageController.showNotificationMessage(qsTr("Backup downloaded successfully!\n\nSaved to:\n%1").arg(localPath)) } - function onBackupUploaded(serverPath) { - // Этот обработчик больше не используется здесь, так как восстановление - // теперь происходит через PageSettingsServerRestoreMode - // Оставляем для совместимости, но не выполняем действий - } - function onBackupRestored() { PageController.showBusyIndicator(false) - - selectedBackupForRestore = "" PageController.showNotificationMessage(qsTr("Backup restored successfully! Containers are restarting...")) } diff --git a/client/ui/qml/Pages2/PageSettingsServerRestoreMode.qml b/client/ui/qml/Pages2/PageSettingsServerRestoreMode.qml index 17f47fa62..62a5a4ca8 100644 --- a/client/ui/qml/Pages2/PageSettingsServerRestoreMode.qml +++ b/client/ui/qml/Pages2/PageSettingsServerRestoreMode.qml @@ -20,7 +20,7 @@ PageType { property string serverIp: "" property bool isFromSetupWizard: false - // Credentials для setup wizard (когда сервер еще не добавлен в ServersModel) + // Credentials for setup wizard (when server is not yet added to ServersModel) property string wizardHostname: "" property string wizardUsername: "" property string wizardSecretData: "" @@ -71,7 +71,7 @@ PageType { Layout.rightMargin: 16 text: { - // Показываем только имя файла и IP адрес, без имени сервера + // Show only filename and IP address, without server name if (serverIp && serverIp.length > 0) { return qsTr("%1 on %2").arg(backupFileName).arg(serverIp) } @@ -95,7 +95,7 @@ PageType { rightImageSource: "qrc:/images/controls/chevron-right.svg" clickedFunction: function() { - startRestore(false) // false = режим добавления + startRestore(false) // false = add mode } } @@ -110,7 +110,7 @@ PageType { textColor: AmneziaStyle.color.vibrantRed clickedFunction: function() { - startRestore(true) // true = режим замены + startRestore(true) // true = replace mode } } } @@ -121,44 +121,17 @@ PageType { function startRestore(replaceMode) { restoreReplaceMode = replaceMode + PageController.showBusyIndicator(true) - // Если это setup wizard с wizard credentials, используем их напрямую - if (isFromSetupWizard && wizardHostname.length > 0) { - console.log("Setup wizard mode, using uploadBackupWithStrings") - PageController.showBusyIndicator(true) - ServersBackupController.uploadBackupWithStrings( - wizardHostname, - wizardUsername, - wizardSecretData, - backupFilePath, - replaceMode - ) - } else { - // Обычный режим - берем из ServersModel - PageController.showBusyIndicator(true) - var credentials = getServerCredentials() - ServersBackupController.uploadBackup(credentials, backupFilePath, replaceMode) - } - } - - function getServerCredentials() { - // Если это setup wizard, используем переданные credentials - if (isFromSetupWizard && wizardHostname.length > 0) { - console.log("Using wizard credentials:", wizardHostname, wizardUsername) - // Устанавливаем credentials в InstallController - InstallController.setProcessedServerCredentials(wizardHostname, wizardUsername, wizardSecretData) - // Получаем их обратно как C++ объект через ServersModel - // Сначала проверяем, есть ли сервер в модели - if (ServersModel.getServersCount() > 0 && ServersModel.processedIndex >= 0) { - return ServersModel.getServerCredentials(ServersModel.processedIndex) - } - // Если сервера нет, создаем временный объект (это не сработает, нужен другой подход) - console.error("Server not in model yet, cannot get credentials") - } - - // Иначе берем из ServersModel - var index = ServersModel.processedIndex - return ServersModel.getServerCredentials(index) + // Call universal C++ method that will determine how to perform restore + ServersBackupController.startRestore( + isFromSetupWizard, + backupFilePath, + replaceMode, + wizardHostname || "", + wizardUsername || "", + wizardSecretData || "" + ) } property string lastUploadedBackupFilename: "" @@ -166,37 +139,25 @@ PageType { Connections { target: ServersBackupController - function onBackupUploaded(serverPath) { - PageController.showNotificationMessage(qsTr("Backup uploaded. Restoring configuration...")) - - var backupFilename = serverPath.split('/').pop() - lastUploadedBackupFilename = backupFilename - - var credentials = getServerCredentials() - ServersBackupController.restoreBackup(credentials, backupFilename, [], restoreReplaceMode) - } - function onBackupRestored() { - console.log(" onBackupRestored, isFromSetupWizard:", isFromSetupWizard) - PageController.showBusyIndicator(false) - - // Для setup wizard устанавливаем default container и сервер - if (isFromSetupWizard && ServersModel.getServersCount() > 0) { - var serverIdx = ServersModel.getServersCount() - 1 - console.log(" Setting server as default:", serverIdx) - ServersModel.setDefaultServerIndex(serverIdx) - ServersModel.processedIndex = serverIdx - - // Запускаем timer для установки default container - // Контейнеры уже установлены через InstallController, просто ждем обновления модели - setDefaultContainerTimer.start() + // For setup wizard, call C++ method to set default server and container + if (isFromSetupWizard) { + ServersBackupController.setDefaultServerAfterRestore(true) } else { - // Для обычного режима сразу переходим + // For regular mode, navigate directly + PageController.showBusyIndicator(false) navigateToRestoredPage() } } + + function onDefaultServerAndContainerSet() { + console.log(" onDefaultServerAndContainerSet - navigating to restored page") + // C++ has set default server and container, navigate to result page + PageController.showBusyIndicator(false) + navigateToRestoredPage() + } function onErrorOccurred(errorMessage, errorCode) { PageController.showBusyIndicator(false) @@ -204,11 +165,10 @@ PageType { } } - // Удаляем Connections для scanServerFinished - больше не нужен function navigateToRestoredPage() { - // Переход на страницу успешного восстановления - // Получаем реальное имя сервера из модели + // Navigate to successful restore page + // Get actual server name from model var actualServerName = serverName if (root.isFromSetupWizard && ServersModel.getServersCount() > 0) { var serverIdx = ServersModel.getServersCount() - 1 @@ -217,13 +177,13 @@ PageType { actualServerName = ServersModel.getProcessedServerData("name") || qsTr("Server") ServersModel.processedIndex = oldProcessedIndex } else if (!serverName || serverName.length === 0) { - // Если имя не передано, получаем из processedIndex + // If name not provided, get from processedIndex actualServerName = ServersModel.getProcessedServerData("name") || qsTr("Server") } var parentItem = root.parent - // Для setup wizard используем обычный StackView + // For setup wizard use regular StackView if (root.isFromSetupWizard) { while (parentItem && typeof parentItem.push !== "function") { parentItem = parentItem.parent @@ -237,7 +197,7 @@ PageType { }) } } else { - // Для меню управления ищем tabBarStackView + // For management menu, find tabBarStackView while (parentItem && parentItem.objectName !== "tabBarStackView") { parentItem = parentItem.parent } @@ -253,49 +213,4 @@ PageType { } } } - - property int containerRetryCount: 0 - property int maxContainerRetries: 5 - - Timer { - id: setDefaultContainerTimer - interval: 1000 - repeat: false - - onTriggered: { - console.log("Timer: Searching for installed containers (attempt", containerRetryCount + 1, "/", maxContainerRetries, ")") - var serverIdx = ServersModel.getServersCount() - 1 - - // Ищем первый установленный контейнер через DefaultServerContainersModel - console.log(" Total rows:", DefaultServerContainersModel.rowCount()) - var foundInstalled = false - for (var i = 0; i < DefaultServerContainersModel.rowCount(); i++) { - var isInstalled = DefaultServerContainersModel.data(DefaultServerContainersModel.index(i, 0), 0x0012) // IsInstalledRole - if (isInstalled) { - console.log(" Setting default container:", i, "for server:", serverIdx) - ServersModel.setDefaultContainer(serverIdx, i) - foundInstalled = true - containerRetryCount = 0 // Reset counter - - // Выключаем индикатор загрузки и переходим на страницу результата - PageController.showBusyIndicator(false) - navigateToRestoredPage() - return - } - } - - containerRetryCount++ - if (containerRetryCount < maxContainerRetries) { - console.log(" No installed containers found yet, will retry...") - setDefaultContainerTimer.start() - } else { - console.log(" Max retries reached, stopping search") - containerRetryCount = 0 // Reset for next time - - // Все равно переходим на страницу результата - PageController.showBusyIndicator(false) - navigateToRestoredPage() - } - } - } } diff --git a/client/ui/qml/Pages2/PageSetupWizardCredentials.qml b/client/ui/qml/Pages2/PageSetupWizardCredentials.qml index 3c5d044fc..d633f8816 100644 --- a/client/ui/qml/Pages2/PageSetupWizardCredentials.qml +++ b/client/ui/qml/Pages2/PageSetupWizardCredentials.qml @@ -15,7 +15,6 @@ PageType { property var setupWizardEasy: null - // Сохраняем credentials здесь для использования при восстановлении backup property string savedHostname: "" property string savedUsername: "" property string savedSecretData: "" @@ -125,7 +124,6 @@ PageType { return } - // Сохраняем credentials в свойствах этой страницы root.savedHostname = _hostname root.savedUsername = _username root.savedSecretData = _secretData diff --git a/client/ui/qml/Pages2/PageSetupWizardEasy.qml b/client/ui/qml/Pages2/PageSetupWizardEasy.qml index 20086ac29..7fd154175 100644 --- a/client/ui/qml/Pages2/PageSetupWizardEasy.qml +++ b/client/ui/qml/Pages2/PageSetupWizardEasy.qml @@ -25,12 +25,51 @@ PageType { property string restoreSecretData: "" property bool waitingForServerToAdd: false - // Для установки контейнеров из backup + // For installing containers from backup property var containersToInstall: [] property int currentContainerIndex: 0 property bool isInstallingContainers: false - // Connections для отслеживания добавления сервера + // Connections for ServersBackupController + Connections { + target: ServersBackupController + + function onReadyForRestore(backupFilePath, hostname, username, secretData, serverIp, fileName) { + console.log("onReadyForRestore received from C++") + console.log(" backupFilePath:", backupFilePath) + console.log(" hostname:", hostname) + console.log(" serverIp:", serverIp) + console.log(" fileName:", fileName) + + // Scan backup to determine containers (C++ already did this, but needed for QML) + var foundContainers = ServersBackupController.scanBackupForContainers(backupFilePath) + console.log("Found containers:", foundContainers) + + if (foundContainers.length === 0) { + PageController.showErrorMessage(qsTr("No containers found in backup file")) + root.isRestoreFromBackup = false + return + } + + root.containersToInstall = foundContainers + root.currentContainerIndex = 0 + + // Now add empty server with these credentials + InstallController.setShouldCreateServer(true) + InstallController.setProcessedServerCredentials(hostname, username, secretData) + + // Set waiting flag + root.waitingForServerToAdd = true + + console.log("Backup scanned, adding server...") + // Add server (asynchronously) + InstallController.addEmptyServer() + + // Further execution will happen in onInstallServerFinished + } + } + + // Connections for tracking server addition Connections { target: InstallController @@ -39,10 +78,10 @@ PageType { console.log("Server added successfully, now installing containers from backup...") root.waitingForServerToAdd = false - // Сервер уже создан, устанавливаем флаг в false + // Server already created, set flag to false InstallController.setShouldCreateServer(false) - // Начинаем установку контейнеров + // Start installing containers root.isInstallingContainers = true installNextContainer() } @@ -52,28 +91,28 @@ PageType { if (root.isInstallingContainers) { console.log("Container installed:", finishedMessage) - // Переходим к следующему контейнеру + // Move to next container root.currentContainerIndex++ if (root.currentContainerIndex < root.containersToInstall.length) { - // Устанавливаем следующий контейнер + // Install next container installNextContainer() } else { - // Все контейнеры установлены, теперь делаем restore + // All containers installed, now do restore console.log("All containers installed, starting restore...") root.isInstallingContainers = false - // ВАЖНО: Выключаем busy indicator перед переходом + // IMPORTANT: Turn off busy indicator before navigation PageController.showBusyIndicator(false) - // Запускаем переход на страницу выбора режима restore + // Start navigation to restore mode selection page navigationTimer.start() } } } } - // Функция для установки следующего контейнера из списка + // Function to install next container from list function installNextContainer() { if (root.currentContainerIndex >= root.containersToInstall.length) { return @@ -82,7 +121,7 @@ PageType { var containerName = root.containersToInstall[root.currentContainerIndex] console.log("Installing container:", containerName, "(", root.currentContainerIndex + 1, "/", root.containersToInstall.length, ")") - // Конвертируем имя контейнера в DockerContainer enum + // Convert container name to DockerContainer enum var dockerContainer = ContainerProps.containerFromString(containerName) if (dockerContainer === 0) { // None @@ -92,33 +131,33 @@ PageType { return } - // Получаем default настройки для контейнера + // Get default settings for container var defaultProtocol = ContainerProps.defaultProtocol(dockerContainer) var defaultPort = ProtocolProps.getPortForInstall(defaultProtocol) var defaultTransport = ProtocolProps.defaultTransportProto(defaultProtocol) - // Показываем индикатор загрузки с сообщением + // Show loading indicator with message PageController.showBusyIndicator(true) PageController.showNotificationMessage(qsTr("Installing %1 (%2/%3)...") .arg(containerName) .arg(root.currentContainerIndex + 1) .arg(root.containersToInstall.length)) - // Убеждаемся что credentials установлены + // Ensure credentials are set console.log("Setting credentials for container installation...") InstallController.setProcessedServerCredentials(root.restoreHostname, root.restoreUsername, root.restoreSecretData) - // Устанавливаем индекс сервера + // Set server index var serverIdx = ServersModel.getServersCount() - 1 ServersModel.processedIndex = serverIdx - // Устанавливаем контейнер + // Install container console.log("Calling InstallController.install for docker container:", dockerContainer) ContainersModel.setProcessedContainerIndex(dockerContainer) InstallController.install(dockerContainer, defaultPort, defaultTransport) } - // Таймер для перехода на страницу выбора режима после выбора файла + // Timer for navigating to restore mode selection page after file selection Timer { id: navigationTimer interval: 500 @@ -128,7 +167,7 @@ PageType { console.log("Navigation timer triggered, going to restore mode page") console.log("Credentials available:", root.restoreHostname, root.restoreUsername, root.restoreSecretData.length > 0 ? "***" : "EMPTY") - // Получаем имя файла + // Get filename var fileName = SystemController.getFileNameFromPath(root.backupFilePath) if (!fileName || fileName === undefined || fileName.length === 0) { var fallbackName = root.backupFilePath.split('/').pop() @@ -136,7 +175,7 @@ PageType { } fileName = String(fileName) - // Извлекаем IP адрес из имени файла + // Extract IP address from filename var serverIp = "" var ipMatch = fileName.match(/^([\d_]+)\s*-/) if (ipMatch && ipMatch.length > 1) { @@ -151,30 +190,30 @@ PageType { serverName = qsTr("RestoredServer") } - // Переходим на страницу установки + // Navigate to installation page PageController.goToPage(PageEnum.PageSetupWizardInstalling) - // Сразу ищем StackView и переходим на страницу восстановления - // Сервер уже добавлен, так как мы ждали onInstallServerFinished + // Immediately find StackView and navigate to restore page + // Server already added, as we waited for onInstallServerFinished Qt.callLater(function() { var pagePath = "qrc:/ui/qml/Pages2/PageSettingsServerRestoreMode.qml" - // Находим главное окно приложения + // Find main application window var item = root while (item.parent) { item = item.parent } - // Находим StackView рекурсивно + // Find StackView recursively function findStackView(obj) { if (!obj) return null - // Проверяем, является ли объект StackView + // Check if object is StackView if (obj.toString().indexOf("StackView") !== -1 || typeof obj.push === "function") { return obj } - // Проверяем children + // Check children if (obj.children) { for (var i = 0; i < obj.children.length; i++) { var result = findStackView(obj.children[i]) @@ -182,7 +221,7 @@ PageType { } } - // Проверяем contentItem + // Check contentItem if (obj.contentItem) { return findStackView(obj.contentItem) } @@ -196,7 +235,7 @@ PageType { stackView.push(pagePath, { "backupFilePath": root.backupFilePath, "backupFileName": fileName, - "serverName": "", // Будет получено из ServersModel + "serverName": "", // Will be obtained from ServersModel "serverIp": serverIp, "isFromSetupWizard": true, "wizardHostname": root.restoreHostname, @@ -358,9 +397,7 @@ PageType { ButtonGroup.group: buttonGroup onClicked: function() { - console.log("=== Restore from backup clicked ===") - // СНАЧАЛА выбираем файл var filter = GC.isMobile() ? "*.gz *.tgz *.tar.gz" : "Backup files (*.tar.gz *.backup *.tgz *.gz)" var localPath = SystemController.getFileName( qsTr("Select Backup to Restore"), @@ -377,34 +414,20 @@ PageType { return } - // Сохраняем путь к backup файлу + // Save backup file path root.backupFilePath = localPath root.isRestoreFromBackup = true - // Сканируем backup для определения контейнеров - console.log("Scanning backup for containers...") - var foundContainers = ServersBackupController.scanBackupForContainers(localPath) - console.log("Found containers:", foundContainers) - - if (foundContainers.length === 0) { - PageController.showErrorMessage(qsTr("No containers found in backup file")) - root.isRestoreFromBackup = false - return - } - - root.containersToInstall = foundContainers - root.currentContainerIndex = 0 - - // Получаем credentials из PageSetupWizardCredentials через поиск в StackView + // Get credentials from PageSetupWizardCredentials via StackView search var credentialsPage = null var item = root - // Ищем StackView + // Find StackView while (item && !item.hasOwnProperty("depth")) { item = item.parent } - // Если нашли StackView, ищем PageSetupWizardCredentials в его истории + // If found StackView, search for PageSetupWizardCredentials in its history if (item && item.depth > 0) { for (var i = 0; i < item.depth; i++) { var page = item.get(i) @@ -421,18 +444,9 @@ PageType { root.restoreSecretData = credentialsPage.savedSecretData console.log("Got credentials from PageSetupWizardCredentials:", root.restoreHostname, root.restoreUsername) - // ТЕПЕРЬ добавляем пустой сервер с этими credentials - InstallController.setShouldCreateServer(true) - InstallController.setProcessedServerCredentials(root.restoreHostname, root.restoreUsername, root.restoreSecretData) - - // Устанавливаем флаг ожидания - root.waitingForServerToAdd = true - - console.log("Backup file selected, adding server...") - // Добавляем сервер (асинхронно) - InstallController.addEmptyServer() - - // Дальнейшее выполнение произойдет в onInstallServerFinished + // Call C++ method to prepare restore + // It will scan backup and send readyForRestore signal + ServersBackupController.prepareRestoreFromBackup(localPath, root.restoreHostname, root.restoreUsername, root.restoreSecretData) } else { console.log("WARNING: No credentials found") return