fix: add container copy optimization

This commit is contained in:
NickVs2015
2026-02-06 17:14:08 +03:00
parent c83e748def
commit 6801c00aeb
7 changed files with 722 additions and 440 deletions
+1 -1
View File
@@ -203,7 +203,7 @@ void CoreController::initControllers()
m_ipSplitTunnelingUiController = new IpSplitTunnelingUiController(m_ipSplitTunnelingController, m_ipSplitTunnelingModel, this); m_ipSplitTunnelingUiController = new IpSplitTunnelingUiController(m_ipSplitTunnelingController, m_ipSplitTunnelingModel, this);
setQmlContextProperty("IpSplitTunnelingController", m_ipSplitTunnelingUiController); setQmlContextProperty("IpSplitTunnelingController", m_ipSplitTunnelingUiController);
m_serversBackupController = new ServersBackupController(m_settings); m_serversBackupController = new ServersBackupController(m_settings, m_serversModel);
setQmlContextProperty("ServersBackupController", m_serversBackupController); setQmlContextProperty("ServersBackupController", m_serversBackupController);
m_allowedDnsUiController = new AllowedDnsUiController(m_allowedDnsController, m_allowedDnsModel, this); m_allowedDnsUiController = new AllowedDnsUiController(m_allowedDnsController, m_allowedDnsModel, this);
+417 -104
View File
@@ -8,6 +8,7 @@
#include <QUrl> #include <QUrl>
#include <QProcess> #include <QProcess>
#include <QSet> #include <QSet>
#include <QTimer>
#ifdef Q_OS_ANDROID #ifdef Q_OS_ANDROID
#include <QJniObject> #include <QJniObject>
#include "platforms/android/android_controller.h" #include "platforms/android/android_controller.h"
@@ -18,15 +19,22 @@
#include "containers/containers_defs.h" #include "containers/containers_defs.h"
#include "core/networkUtilities.h" #include "core/networkUtilities.h"
#include "systemController.h" #include "systemController.h"
#include "ui/models/servers_model.h"
#include "ui/models/containers_model.h"
ServersBackupController::ServersBackupController(std::shared_ptr<Settings> settings, QObject *parent) ServersBackupController::ServersBackupController(std::shared_ptr<Settings> settings, ServersModel *serversModel, QObject *parent)
: QObject(parent) : QObject(parent)
, m_settings(settings) , m_settings(settings)
, m_serversModel(serversModel)
, m_serverController(new ServerController(settings, this)) , m_serverController(new ServerController(settings, this))
, m_status(Idle) , m_status(Idle)
, m_backupDir("/var/backups/amnezia") , m_backupDir("/var/backups/amnezia")
, m_restoreReplaceMode(false) , m_restoreReplaceMode(false)
, m_tempUploadFile(nullptr) , 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_currentOutput.clear();
m_currentError.clear(); m_currentError.clear();
// Получаем IP адрес сервера // Get server IP address
QString serverIp = NetworkUtilities::getIPAddress(credentials.hostName); QString serverIp = NetworkUtilities::getIPAddress(credentials.hostName);
if (serverIp.isEmpty()) { if (serverIp.isEmpty()) {
serverIp = credentials.hostName; serverIp = credentials.hostName;
} }
// Форматируем IP: заменяем точки на подчеркивания // Format IP: replace dots with underscores
QString ipFormatted = serverIp; QString ipFormatted = serverIp;
ipFormatted.replace(".", "_"); ipFormatted.replace(".", "_");
// Получаем bash скрипт для backup с IP адресом // Get bash script for backup with IP address
QString script = getBackupScript(ipFormatted); QString script = getBackupScript(ipFormatted);
// Callback для обработки stdout // Callback for handling stdout
auto cbStdOut = [this](const QString &data, libssh::Client &client) -> ErrorCode { auto cbStdOut = [this](const QString &data, libssh::Client &client) -> ErrorCode {
Q_UNUSED(client); Q_UNUSED(client);
return handleStdOut(data, m_currentOutput); return handleStdOut(data, m_currentOutput);
}; };
// Callback для обработки stderr // Callback for handling stderr
auto cbStdErr = [this](const QString &data, libssh::Client &client) -> ErrorCode { auto cbStdErr = [this](const QString &data, libssh::Client &client) -> ErrorCode {
Q_UNUSED(client); Q_UNUSED(client);
return handleStdErr(data, m_currentError); return handleStdErr(data, m_currentError);
}; };
// Запускаем скрипт на сервере // Run script on server
ErrorCode error = m_serverController->runHostScript(credentials, script, cbStdOut, cbStdErr); ErrorCode error = m_serverController->runHostScript(credentials, script, cbStdOut, cbStdErr);
if (error == ErrorCode::NoError) { 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"); QRegularExpression re("([\\d_]+)\\s+-\\s+(\\d{2}-\\d{2}-\\d{4})_(\\d{2}-\\d{2}-\\d{2})\\.tgz");
QRegularExpressionMatch match = re.match(m_currentOutput); 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"; QString backupFilename = match.captured(1) + " - " + match.captured(2) + "_" + match.captured(3) + ".tgz";
setStatus(Success); setStatus(Success);
setProgress(100, tr("Backup created successfully")); setProgress(100, tr("Backup created successfully"));
// 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); emit backupCreated(backupFilename);
}
} else { } else {
setStatus(Failed); setStatus(Failed);
emit errorOccurred(tr("Failed to parse backup filename from output: %1").arg(m_currentOutput.left(200)), ErrorCode::InternalError); 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_currentOutput.clear();
m_currentError.clear(); m_currentError.clear();
// Получаем IP адрес сервера // Get server IP address
QString serverIp = NetworkUtilities::getIPAddress(credentials.hostName); QString serverIp = NetworkUtilities::getIPAddress(credentials.hostName);
if (serverIp.isEmpty()) { if (serverIp.isEmpty()) {
serverIp = credentials.hostName; serverIp = credentials.hostName;
} }
// Форматируем IP: заменяем точки на подчеркивания // Format IP: replace dots with underscores
QString ipFormatted = serverIp; QString ipFormatted = serverIp;
ipFormatted.replace(".", "_"); ipFormatted.replace(".", "_");
@@ -147,7 +166,7 @@ void ServersBackupController::createContainerBackup(const ServerCredentials &cre
ErrorCode error = m_serverController->runHostScript(credentials, script, cbStdOut, cbStdErr); ErrorCode error = m_serverController->runHostScript(credentials, script, cbStdOut, cbStdErr);
if (error == ErrorCode::NoError) { 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"); QRegularExpression re("([\\d_]+)\\s+-\\s+(\\d{2}-\\d{2}-\\d{4})_(\\d{2}-\\d{2}-\\d{2})\\.tgz");
QRegularExpressionMatch match = re.match(m_currentOutput); QRegularExpressionMatch match = re.match(m_currentOutput);
@@ -179,12 +198,12 @@ void ServersBackupController::createContainersBackup(const ServerCredentials &cr
m_currentOutput.clear(); m_currentOutput.clear();
m_currentError.clear(); m_currentError.clear();
// Получаем IP адрес сервера // Get server IP address
QString serverIp = NetworkUtilities::getIPAddress(credentials.hostName); QString serverIp = NetworkUtilities::getIPAddress(credentials.hostName);
if (serverIp.isEmpty()) { if (serverIp.isEmpty()) {
serverIp = credentials.hostName; serverIp = credentials.hostName;
} }
// Форматируем IP: заменяем точки на подчеркивания // Format IP: replace dots with underscores
QString ipFormatted = serverIp; QString ipFormatted = serverIp;
ipFormatted.replace(".", "_"); ipFormatted.replace(".", "_");
@@ -203,7 +222,7 @@ void ServersBackupController::createContainersBackup(const ServerCredentials &cr
ErrorCode error = m_serverController->runHostScript(credentials, script, cbStdOut, cbStdErr); ErrorCode error = m_serverController->runHostScript(credentials, script, cbStdOut, cbStdErr);
if (error == ErrorCode::NoError) { 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"); QRegularExpression re("([\\d_]+)\\s+-\\s+(\\d{2}-\\d{2}-\\d{4})_(\\d{2}-\\d{2}-\\d{2})\\.tgz");
QRegularExpressionMatch match = re.match(m_currentOutput); 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); 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]") || bool hasError = m_currentOutput.contains("[ERROR]") ||
m_currentOutput.contains("Failed to extract backup") || m_currentOutput.contains("Failed to extract backup") ||
m_currentError.contains("[ERROR]") || m_currentError.contains("[ERROR]") ||
m_currentError.contains("Failed to extract backup"); m_currentError.contains("Failed to extract backup");
if (error == ErrorCode::NoError && !hasError) { if (error == ErrorCode::NoError && !hasError) {
// Проверяем, что восстановление действительно завершилось успешно // Check that restore actually completed successfully
if (m_currentOutput.contains("Restore completed successfully")) { if (m_currentOutput.contains("Restore completed successfully")) {
setStatus(Success); setStatus(Success);
setProgress(100, tr("Backup restored successfully")); 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 only filename provided (no directory), use appropriate folder
if (!pathInfo.isAbsolute() || pathInfo.dir().path() == ".") { if (!pathInfo.isAbsolute() || pathInfo.dir().path() == ".") {
#ifdef Q_OS_ANDROID #ifdef Q_OS_ANDROID
// На Android используем публичную папку Download (через JNI) // On Android use public Download folder (via JNI)
QJniObject mediaDir = QJniObject::callStaticObjectMethod( QJniObject mediaDir = QJniObject::callStaticObjectMethod(
"android/os/Environment", "android/os/Environment",
"getExternalStoragePublicDirectory", "getExternalStoragePublicDirectory",
@@ -395,7 +414,7 @@ void ServersBackupController::downloadBackup(const ServerCredentials &credential
QString tempPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation); QString tempPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation);
actualLocalPath = QDir(tempPath).filePath(backupFilename); actualLocalPath = QDir(tempPath).filePath(backupFilename);
#else #else
// На Desktop используем Documents (как обычный backup) // On Desktop use Documents (as regular backup)
QString documentsPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); QString documentsPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
if (documentsPath.isEmpty()) { if (documentsPath.isEmpty()) {
documentsPath = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); documentsPath = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
@@ -430,6 +449,15 @@ void ServersBackupController::downloadBackup(const ServerCredentials &credential
setStatus(Success); setStatus(Success);
setProgress(100, tr("Backup downloaded successfully")); 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); emit backupDownloaded(actualLocalPath);
} else { } else {
setStatus(Failed); setStatus(Failed);
@@ -446,31 +474,31 @@ void ServersBackupController::uploadBackup(const ServerCredentials &credentials,
return; return;
} }
// Сохраняем режим восстановления для последующего использования // Save restore mode for later use
m_restoreReplaceMode = replaceMode; m_restoreReplaceMode = replaceMode;
QString actualLocalPath = localPath; QString actualLocalPath = localPath;
QString filename; QString filename;
#ifdef Q_OS_ANDROID #ifdef Q_OS_ANDROID
// Для Android URI нужно получить имя файла и использовать файловый дескриптор // For Android URI need to get filename and use file descriptor
if (localPath.startsWith("content://")) { if (localPath.startsWith("content://")) {
// Получаем имя файла из URI // Get filename from URI
filename = AndroidController::instance()->getFileName(localPath); filename = AndroidController::instance()->getFileName(localPath);
if (filename.isEmpty()) { if (filename.isEmpty()) {
// Fallback: извлекаем имя из URI // Fallback: extract name from URI
QStringList parts = localPath.split('/'); QStringList parts = localPath.split('/');
if (!parts.isEmpty()) { if (!parts.isEmpty()) {
filename = parts.last(); filename = parts.last();
// Декодируем URL-кодированные символы // Decode URL-encoded characters
if (filename.contains('%')) { if (filename.contains('%')) {
filename = QUrl::fromPercentEncoding(filename.toUtf8()); filename = QUrl::fromPercentEncoding(filename.toUtf8());
} }
} }
} }
// Для Android URI используем файловый дескриптор через SystemController::readFile // For Android URI use file descriptor via SystemController::readFile
// Но scpFileCopy требует путь к файлу, поэтому нужно скопировать файл во временную директорию // But scpFileCopy requires file path, so need to copy file to temp directory
QByteArray fileData; QByteArray fileData;
qDebug() << "Reading Android URI:" << localPath; qDebug() << "Reading Android URI:" << localPath;
if (!SystemController::readFile(localPath, fileData)) { if (!SystemController::readFile(localPath, fileData)) {
@@ -480,14 +508,14 @@ void ServersBackupController::uploadBackup(const ServerCredentials &credentials,
} }
qDebug() << "Read" << fileData.size() << "bytes from Android URI"; qDebug() << "Read" << fileData.size() << "bytes from Android URI";
// Удаляем предыдущий временный файл, если он существует // Delete previous temp file if exists
if (m_tempUploadFile) { if (m_tempUploadFile) {
delete m_tempUploadFile; delete m_tempUploadFile;
m_tempUploadFile = nullptr; m_tempUploadFile = nullptr;
} }
// Создаем временный файл (сохраняем в член класса, чтобы не удалялся) // Create temp file (save to class member so it doesn't get deleted)
// Используем setAutoRemove(false) чтобы файл не удалялся автоматически // Use setAutoRemove(false) so file isn't automatically deleted
m_tempUploadFile = new QTemporaryFile(this); m_tempUploadFile = new QTemporaryFile(this);
m_tempUploadFile->setAutoRemove(false); m_tempUploadFile->setAutoRemove(false);
if (!m_tempUploadFile->open()) { if (!m_tempUploadFile->open()) {
@@ -500,13 +528,13 @@ void ServersBackupController::uploadBackup(const ServerCredentials &credentials,
qint64 written = m_tempUploadFile->write(fileData); qint64 written = m_tempUploadFile->write(fileData);
m_tempUploadFile->flush(); m_tempUploadFile->flush();
// НЕ закрываем файл - он должен оставаться открытым для SCP // DON'T close file - it must stay open for SCP
// m_tempUploadFile->close(); // m_tempUploadFile->close();
actualLocalPath = m_tempUploadFile->fileName(); actualLocalPath = m_tempUploadFile->fileName();
qDebug() << "Created temp file:" << actualLocalPath << "written:" << written << "bytes, size:" << QFileInfo(actualLocalPath).size(); qDebug() << "Created temp file:" << actualLocalPath << "written:" << written << "bytes, size:" << QFileInfo(actualLocalPath).size();
// Проверяем, что файл существует и доступен для чтения // Check that file exists and is readable
QFileInfo tempFileInfo(actualLocalPath); QFileInfo tempFileInfo(actualLocalPath);
if (!tempFileInfo.exists()) { if (!tempFileInfo.exists()) {
qDebug() << "Temp file does not exist after creation!"; qDebug() << "Temp file does not exist after creation!";
@@ -516,7 +544,7 @@ void ServersBackupController::uploadBackup(const ServerCredentials &credentials,
return; return;
} }
// Если имя файла пустое, используем имя из временного файла // If filename empty, use name from temp file
if (filename.isEmpty()) { if (filename.isEmpty()) {
filename = QFileInfo(actualLocalPath).fileName(); filename = QFileInfo(actualLocalPath).fileName();
} }
@@ -529,7 +557,7 @@ void ServersBackupController::uploadBackup(const ServerCredentials &credentials,
filename = localFileInfo.fileName(); filename = localFileInfo.fileName();
} }
#else #else
// Для других платформ используем обычную проверку // For other platforms use regular check
QFileInfo localFileInfo(localPath); QFileInfo localFileInfo(localPath);
if (!localFileInfo.exists()) { if (!localFileInfo.exists()) {
emit errorOccurred(tr("Local file does not exist: %1").arg(localPath), ErrorCode::InternalError); 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")); setProgress(100, tr("Backup uploaded successfully"));
emit backupUploaded(remotePath); 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) { if (m_tempUploadFile) {
qDebug() << "Removing temp file:" << m_tempUploadFile->fileName(); qDebug() << "Removing temp file:" << m_tempUploadFile->fileName();
m_tempUploadFile->remove(); m_tempUploadFile->remove();
@@ -574,7 +611,7 @@ void ServersBackupController::uploadBackup(const ServerCredentials &credentials,
qDebug() << "Upload failed with error code:" << static_cast<int>(error); qDebug() << "Upload failed with error code:" << static_cast<int>(error);
emit errorOccurred(tr("Failed to upload backup: error code %1").arg(static_cast<int>(error)), error); emit errorOccurred(tr("Failed to upload backup: error code %1").arg(static_cast<int>(error)), error);
// Удаляем временный файл при ошибке // Delete temp file on error
if (m_tempUploadFile) { if (m_tempUploadFile) {
m_tempUploadFile->remove(); m_tempUploadFile->remove();
delete m_tempUploadFile; 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, void ServersBackupController::uploadBackupWithStrings(const QString &hostname,
const QString &username, const QString &username,
const QString &secretData, const QString &secretData,
const QString &localPath, const QString &localPath,
bool replaceMode) bool replaceMode)
{ {
// Создаем ServerCredentials из строк // Create ServerCredentials from strings
ServerCredentials credentials; ServerCredentials credentials;
credentials.hostName = hostname; credentials.hostName = hostname;
credentials.userName = username; credentials.userName = username;
@@ -599,7 +636,7 @@ void ServersBackupController::uploadBackupWithStrings(const QString &hostname,
qDebug() << "uploadBackupWithStrings called with hostname:" << hostname << "username:" << username; qDebug() << "uploadBackupWithStrings called with hostname:" << hostname << "username:" << username;
// Вызываем основной метод // Call main method
uploadBackup(credentials, localPath, replaceMode); uploadBackup(credentials, localPath, replaceMode);
} }
@@ -609,11 +646,11 @@ QStringList ServersBackupController::scanBackupForContainers(const QString &loca
qDebug() << "Scanning backup file for containers:" << localPath; 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 #ifdef Q_OS_ANDROID
QString actualPath = localPath; QString actualPath = localPath;
if (localPath.startsWith("content://")) { if (localPath.startsWith("content://")) {
// Для Android URI нужно сначала прочитать файл // For Android URI need to read file first
int fd = AndroidController::instance()->getFd(localPath); int fd = AndroidController::instance()->getFd(localPath);
if (fd < 0) { if (fd < 0) {
qWarning() << "Failed to get file descriptor for Android URI"; qWarning() << "Failed to get file descriptor for Android URI";
@@ -631,7 +668,7 @@ QStringList ServersBackupController::scanBackupForContainers(const QString &loca
file.close(); file.close();
AndroidController::instance()->closeFd(); AndroidController::instance()->closeFd();
// Сохраняем во временный файл // Save to temporary file
actualPath = QDir::temp().filePath("backup_scan_temp.tgz"); actualPath = QDir::temp().filePath("backup_scan_temp.tgz");
QFile tempFile(actualPath); QFile tempFile(actualPath);
if (!tempFile.open(QIODevice::WriteOnly)) { if (!tempFile.open(QIODevice::WriteOnly)) {
@@ -645,7 +682,7 @@ QStringList ServersBackupController::scanBackupForContainers(const QString &loca
QString actualPath = localPath; QString actualPath = localPath;
#endif #endif
// Выполняем команду tar для просмотра содержимого // Execute tar command to view contents
QProcess process; QProcess process;
process.start("tar", QStringList() << "-tzf" << actualPath); process.start("tar", QStringList() << "-tzf" << actualPath);
process.waitForFinished(5000); process.waitForFinished(5000);
@@ -658,11 +695,11 @@ QStringList ServersBackupController::scanBackupForContainers(const QString &loca
QString output = process.readAllStandardOutput(); QString output = process.readAllStandardOutput();
QStringList lines = output.split('\n', Qt::SkipEmptyParts); QStringList lines = output.split('\n', Qt::SkipEmptyParts);
// Ищем директории контейнеров (amnezia-*) // Find container directories (amnezia-*)
QSet<QString> foundContainers; QSet<QString> foundContainers;
for (const QString &line : lines) { for (const QString &line : lines) {
if (line.contains("amnezia-")) { if (line.contains("amnezia-")) {
// Извлекаем имя контейнера из пути // Extract container name from path
QStringList parts = line.split('/'); QStringList parts = line.split('/');
for (const QString &part : parts) { for (const QString &part : parts) {
if (part.startsWith("amnezia-")) { if (part.startsWith("amnezia-")) {
@@ -690,9 +727,9 @@ void ServersBackupController::deleteBackup(const ServerCredentials &credentials,
setStatus(InProgress); setStatus(InProgress);
setProgress(0, tr("Deleting backup...")); setProgress(0, tr("Deleting backup..."));
// Экранируем имя файла для безопасного использования в bash // Escape filename for safe use in bash
QString escapedFilename = backupFilename; QString escapedFilename = backupFilename;
escapedFilename.replace("'", "'\\''"); // Экранируем одинарные кавычки escapedFilename.replace("'", "'\\''"); // Escape single quotes
QString script = QString("sudo rm -f '%1/%2'").arg(m_backupDir).arg(escapedFilename); QString script = QString("sudo rm -f '%1/%2'").arg(m_backupDir).arg(escapedFilename);
m_currentOutput.clear(); m_currentOutput.clear();
@@ -720,13 +757,13 @@ void ServersBackupController::deleteBackup(const ServerCredentials &credentials,
} }
// ============================================================================ // ============================================================================
// ВСТРОЕННЫЕ BASH СКРИПТЫ // EMBEDDED BASH SCRIPTS
// ============================================================================ // ============================================================================
QString ServersBackupController::getBackupScript(const QString &ipAddress) const QString ServersBackupController::getBackupScript(const QString &ipAddress) const
{ {
// Упрощенная версия bash скрипта, встроенная в C++ // Simplified bash script version, embedded in C++
// Формат имени файла: IP_ADDRESS - DD-MM-YYYY_HH-MM-SS.tgz // Filename format: IP_ADDRESS - DD-MM-YYYY_HH-MM-SS.tgz
return QString(R"( return QString(R"(
#!/bin/bash #!/bin/bash
set -e set -e
@@ -740,10 +777,10 @@ BACKUP_SUBDIR="$BACKUP_DIR/backup_temp_$$"
echo "[INFO] Starting backup..." echo "[INFO] Starting backup..."
# Создание директории # Create directory
mkdir -p "$BACKUP_SUBDIR" mkdir -p "$BACKUP_SUBDIR"
# Список контейнеров Amnezia # List of Amnezia containers
CONTAINERS=( CONTAINERS=(
"amnezia-awg" "amnezia-awg"
"amnezia-awg2" "amnezia-awg2"
@@ -755,21 +792,21 @@ CONTAINERS=(
"amnezia-shadowsocks" "amnezia-shadowsocks"
) )
# Backup каждого контейнера (включая остановленные) # Backup each container (including stopped)
for container in "${CONTAINERS[@]}"; do for container in "${CONTAINERS[@]}"; do
if sudo docker ps -a --format '{{.Names}}' | grep -q "^$container$"; then if sudo docker ps -a --format '{{.Names}}' | grep -q "^$container$"; then
echo "[INFO] Backing up $container..." echo "[INFO] Backing up $container..."
mkdir -p "$BACKUP_SUBDIR/$container" mkdir -p "$BACKUP_SUBDIR/$container"
# Копируем /opt/amnezia # Copy /opt/amnezia
sudo docker cp "$container:/opt/amnezia" "$BACKUP_SUBDIR/$container/" 2>/dev/null || true 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 sudo docker inspect "$container" > "$BACKUP_SUBDIR/$container/container_inspect.json" 2>/dev/null || true
fi fi
done done
# Создание архива # Create archive
cd "$BACKUP_DIR" cd "$BACKUP_DIR"
tar -czf "$BACKUP_FILENAME" -C "$BACKUP_SUBDIR" . 2>/dev/null tar -czf "$BACKUP_FILENAME" -C "$BACKUP_SUBDIR" . 2>/dev/null
rm -rf "$BACKUP_SUBDIR" rm -rf "$BACKUP_SUBDIR"
@@ -782,8 +819,8 @@ QString ServersBackupController::getContainerBackupScript(DockerContainer contai
{ {
QString containerName = ContainerProps::containerToString(container); QString containerName = ContainerProps::containerToString(container);
// Backup конкретного контейнера напрямую через docker cp // Backup specific container directly via docker cp
// Формат имени файла: IP_ADDRESS - DD-MM-YYYY_HH-MM-SS.tgz // Filename format: IP_ADDRESS - DD-MM-YYYY_HH-MM-SS.tgz
return QString(R"( return QString(R"(
#!/bin/bash #!/bin/bash
set -e set -e
@@ -798,35 +835,35 @@ BACKUP_SUBDIR="$BACKUP_DIR/backup_temp_$$"
echo "[INFO] Starting backup for container: $CONTAINER_NAME..." echo "[INFO] Starting backup for container: $CONTAINER_NAME..."
# Проверка существования контейнера # Check container exists
if ! sudo docker ps -a --format '{{.Names}}' | grep -q "^$CONTAINER_NAME$"; then if ! sudo docker ps -a --format '{{.Names}}' | grep -q "^$CONTAINER_NAME$"; then
echo "[ERROR] Container $CONTAINER_NAME does not exist" echo "[ERROR] Container $CONTAINER_NAME does not exist"
exit 1 exit 1
fi fi
# Создание директории # Create directory
mkdir -p "$BACKUP_SUBDIR/$CONTAINER_NAME" mkdir -p "$BACKUP_SUBDIR/$CONTAINER_NAME"
# Backup конфигураций из контейнера напрямую # Backup configurations from container directly
echo "[INFO] Copying /opt/amnezia from container..." echo "[INFO] Copying /opt/amnezia from container..."
sudo docker cp "$CONTAINER_NAME:/opt/amnezia" "$BACKUP_SUBDIR/$CONTAINER_NAME/" 2>/dev/null || { sudo docker cp "$CONTAINER_NAME:/opt/amnezia" "$BACKUP_SUBDIR/$CONTAINER_NAME/" 2>/dev/null || {
echo "[WARN] Failed to copy /opt/amnezia, trying alternative paths..." 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/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/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/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 sudo docker cp "$CONTAINER_NAME:/etc/xray" "$BACKUP_SUBDIR/$CONTAINER_NAME/" 2>/dev/null || true
} }
# Сохранение метаданных контейнера # Save container metadata
echo "[INFO] Saving container metadata..." echo "[INFO] Saving container metadata..."
sudo docker inspect "$CONTAINER_NAME" > "$BACKUP_SUBDIR/$CONTAINER_NAME/container_inspect.json" 2>/dev/null || true 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}') \ 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 > "$BACKUP_SUBDIR/$CONTAINER_NAME/network_config.json" 2>/dev/null || true
# Создание архива # Create archive
cd "$BACKUP_DIR" cd "$BACKUP_DIR"
tar -czf "$BACKUP_FILENAME" -C "$BACKUP_SUBDIR" . 2>/dev/null tar -czf "$BACKUP_FILENAME" -C "$BACKUP_SUBDIR" . 2>/dev/null
rm -rf "$BACKUP_SUBDIR" rm -rf "$BACKUP_SUBDIR"
@@ -843,7 +880,7 @@ QString ServersBackupController::getContainersBackupScript(const QList<DockerCon
containersList += QString("\"%1\" ").arg(containerName); containersList += QString("\"%1\" ").arg(containerName);
} }
// Формат имени файла: IP_ADDRESS - DD-MM-YYYY_HH-MM-SS.tgz // Filename format: IP_ADDRESS - DD-MM-YYYY_HH-MM-SS.tgz
return QString(R"( return QString(R"(
#!/bin/bash #!/bin/bash
set -e set -e
@@ -857,19 +894,19 @@ BACKUP_SUBDIR="$BACKUP_DIR/backup_temp_$$"
echo "[INFO] Starting backup for containers..." echo "[INFO] Starting backup for containers..."
# Создание директории # Create directory
mkdir -p "$BACKUP_SUBDIR" mkdir -p "$BACKUP_SUBDIR"
# Список контейнеров для backup # List of containers for backup
CONTAINERS=(%3) CONTAINERS=(%3)
# Backup каждого контейнера # Backup each container
for container in "${CONTAINERS[@]}"; do for container in "${CONTAINERS[@]}"; do
if sudo docker ps -a --format '{{.Names}}' | grep -q "^$container$"; then if sudo docker ps -a --format '{{.Names}}' | grep -q "^$container$"; then
echo "[INFO] Backing up $container..." echo "[INFO] Backing up $container..."
mkdir -p "$BACKUP_SUBDIR/$container" mkdir -p "$BACKUP_SUBDIR/$container"
# Копируем /opt/amnezia напрямую из контейнера # Copy /opt/amnezia directly from container
sudo docker cp "$container:/opt/amnezia" "$BACKUP_SUBDIR/$container/" 2>/dev/null || { sudo docker cp "$container:/opt/amnezia" "$BACKUP_SUBDIR/$container/" 2>/dev/null || {
echo "[WARN] Failed to copy /opt/amnezia from $container, trying alternative paths..." 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 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 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 sudo docker inspect "$container" > "$BACKUP_SUBDIR/$container/container_inspect.json" 2>/dev/null || true
else else
echo "[WARN] Container $container does not exist, skipping..." echo "[WARN] Container $container does not exist, skipping..."
fi fi
done done
# Создание архива # Create archive
cd "$BACKUP_DIR" cd "$BACKUP_DIR"
tar -czf "$BACKUP_FILENAME" -C "$BACKUP_SUBDIR" . 2>/dev/null tar -czf "$BACKUP_FILENAME" -C "$BACKUP_SUBDIR" . 2>/dev/null
rm -rf "$BACKUP_SUBDIR" rm -rf "$BACKUP_SUBDIR"
@@ -898,11 +935,11 @@ QString ServersBackupController::getRestoreScript(const QString &backupFilename,
const QStringList &containers, const QStringList &containers,
bool replaceMode) const 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; QString escapedFilename = backupFilename;
escapedFilename.replace("'", "'\\''"); // Экранируем одинарные кавычки escapedFilename.replace("'", "'\\''"); // Escape single quotes
return QString(R"( return QString(R"(
#!/bin/bash #!/bin/bash
@@ -920,13 +957,13 @@ else
echo "[INFO] Using add mode: data will be added to existing containers" echo "[INFO] Using add mode: data will be added to existing containers"
fi fi
# Проверка существования файла backup # Check backup file exists
if [ ! -f "$BACKUP_DIR/$BACKUP_FILE" ]; then if [ ! -f "$BACKUP_DIR/$BACKUP_FILE" ]; then
echo "[ERROR] Backup file not found: $BACKUP_DIR/$BACKUP_FILE" echo "[ERROR] Backup file not found: $BACKUP_DIR/$BACKUP_FILE"
exit 1 exit 1
fi fi
# Извлечение backup # Extract backup
mkdir -p "$TEMP_DIR" mkdir -p "$TEMP_DIR"
EXTRACT_OUTPUT=$(tar -xzf "$BACKUP_DIR/$BACKUP_FILE" -C "$TEMP_DIR" 2>&1) EXTRACT_OUTPUT=$(tar -xzf "$BACKUP_DIR/$BACKUP_FILE" -C "$TEMP_DIR" 2>&1)
EXTRACT_EXIT_CODE=$? EXTRACT_EXIT_CODE=$?
@@ -936,12 +973,12 @@ if [ $EXTRACT_EXIT_CODE -ne 0 ]; then
exit 1 exit 1
fi 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_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 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 if ls -d "$TEMP_DIR"/amnezia-* 2>/dev/null | head -1 > /dev/null; then
BACKUP_SUBDIR="$TEMP_DIR" BACKUP_SUBDIR="$TEMP_DIR"
else else
@@ -953,7 +990,7 @@ if [ -z "$BACKUP_SUBDIR" ]; then
fi fi
fi fi
# Восстановление каждого контейнера # Restore each container
for container_dir in "$BACKUP_SUBDIR"/*; do for container_dir in "$BACKUP_SUBDIR"/*; do
if [ ! -d "$container_dir" ]; then if [ ! -d "$container_dir" ]; then
continue continue
@@ -964,34 +1001,34 @@ for container_dir in "$BACKUP_SUBDIR"/*; do
if sudo docker ps -a --format '{{.Names}}' | grep -q "^$container_name$"; then if sudo docker ps -a --format '{{.Names}}' | grep -q "^$container_name$"; then
echo "[INFO] Restoring $container_name..." echo "[INFO] Restoring $container_name..."
# Остановка контейнера # Stop container
sudo docker stop "$container_name" 2>/dev/null || true sudo docker stop "$container_name" 2>/dev/null || true
# Режим замены: очистка контейнера перед восстановлением # Replace mode: clear container before restore
if [ "$REPLACE_MODE" = "1" ]; then if [ "$REPLACE_MODE" = "1" ]; then
echo "[INFO] Clearing container $container_name before restore..." echo "[INFO] Clearing container $container_name before restore..."
# Создаем пустую директорию для очистки /opt/amnezia # Create empty directory to clear /opt/amnezia
TEMP_CLEAR_DIR="/tmp/clear_amnezia_$$" TEMP_CLEAR_DIR="/tmp/clear_amnezia_$$"
mkdir -p "$TEMP_CLEAR_DIR/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 sudo docker cp "$TEMP_CLEAR_DIR/amnezia" "$container_name:/opt/" 2>/dev/null || true
# Удаляем временную директорию # Delete temporary directory
rm -rf "$TEMP_CLEAR_DIR" rm -rf "$TEMP_CLEAR_DIR"
fi fi
# Восстановление /opt/amnezia # Restore /opt/amnezia
if [ -d "$container_dir/amnezia" ]; then if [ -d "$container_dir/amnezia" ]; then
sudo docker cp "$container_dir/amnezia" "$container_name:/opt/" 2>/dev/null || true sudo docker cp "$container_dir/amnezia" "$container_name:/opt/" 2>/dev/null || true
fi fi
# Запуск контейнера # Start container
sudo docker start "$container_name" 2>/dev/null || true sudo docker start "$container_name" 2>/dev/null || true
echo "[INFO] $container_name restored" echo "[INFO] $container_name restored"
fi fi
done done
# Очистка # Cleanup
rm -rf "$TEMP_DIR" rm -rf "$TEMP_DIR"
echo "[INFO] Restore completed successfully" echo "[INFO] Restore completed successfully"
@@ -1007,12 +1044,12 @@ BACKUP_DIR=%1
echo "Backup directory: $BACKUP_DIR" echo "Backup directory: $BACKUP_DIR"
# Проверка наличия backup # Check backup availability
BACKUPS=$(ls -t "$BACKUP_DIR"/backup_*.tar.gz 2>/dev/null | wc -l) BACKUPS=$(ls -t "$BACKUP_DIR"/backup_*.tar.gz 2>/dev/null | wc -l)
echo "Total backups: $BACKUPS" echo "Total backups: $BACKUPS"
if [ "$BACKUPS" -gt 0 ]; then if [ "$BACKUPS" -gt 0 ]; then
# Информация о последнем backup # Last backup information
LATEST=$(ls -t "$BACKUP_DIR"/backup_*.tar.gz 2>/dev/null | head -1) LATEST=$(ls -t "$BACKUP_DIR"/backup_*.tar.gz 2>/dev/null | head -1)
if [ -n "$LATEST" ]; then if [ -n "$LATEST" ]; then
echo "Latest backup: $(basename "$LATEST")" echo "Latest backup: $(basename "$LATEST")"
@@ -1021,7 +1058,7 @@ if [ "$BACKUPS" -gt 0 ]; then
fi fi
fi fi
# Проверка запущенных контейнеров # Check running containers
echo "Running containers:" echo "Running containers:"
sudo docker ps --filter "name=amnezia-" --format '{{.Names}}' 2>/dev/null 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); )").arg(m_backupDir);
} }
// ============================================================================
// ПАРСИНГ ВЫВОДА
// ============================================================================
QList<ServersBackupController::BackupInfo> ServersBackupController::parseBackupList(const QString &output) QList<ServersBackupController::BackupInfo> ServersBackupController::parseBackupList(const QString &output)
{ {
QList<BackupInfo> backups; QList<BackupInfo> backups;
QStringList lines = output.split('\n', Qt::SkipEmptyParts); 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)$"); QRegularExpression re("^-.*\\s+(\\d+)\\s+\\w+\\s+\\d+\\s+([\\d:]+)\\s+(.+backup_(\\d{8}_\\d{6})\\.tar\\.gz)$");
for (const QString &line : lines) { for (const QString &line : lines) {
@@ -1061,7 +1094,7 @@ QList<ServersBackupController::BackupInfo> ServersBackupController::parseBackupL
info.filename = QFileInfo(match.captured(3)).fileName(); info.filename = QFileInfo(match.captured(3)).fileName();
info.fullPath = match.captured(3); info.fullPath = match.captured(3);
// Парсим дату из имени файла // Parse date from filename
QString dateStr = match.captured(4); QString dateStr = match.captured(4);
info.createdAt = QDateTime::fromString(dateStr, "yyyyMMdd_HHmmss"); info.createdAt = QDateTime::fromString(dateStr, "yyyyMMdd_HHmmss");
info.isValid = true; info.isValid = true;
@@ -1077,18 +1110,18 @@ QJsonObject ServersBackupController::parseBackupStatus(const QString &output)
{ {
QJsonObject status; QJsonObject status;
// Парсим текстовый вывод // Parse text output
status["raw_output"] = output; status["raw_output"] = output;
status["has_backups"] = output.contains("Total backups:"); status["has_backups"] = output.contains("Total backups:");
// Извлекаем количество backup // Extract backup count
QRegularExpression reTotal("Total backups: (\\d+)"); QRegularExpression reTotal("Total backups: (\\d+)");
QRegularExpressionMatch matchTotal = reTotal.match(output); QRegularExpressionMatch matchTotal = reTotal.match(output);
if (matchTotal.hasMatch()) { if (matchTotal.hasMatch()) {
status["total_backups"] = matchTotal.captured(1).toInt(); status["total_backups"] = matchTotal.captured(1).toInt();
} }
// Извлекаем информацию о последнем backup // Extract last backup information
QRegularExpression reLatest("Latest backup: (.+)"); QRegularExpression reLatest("Latest backup: (.+)");
QRegularExpressionMatch matchLatest = reLatest.match(output); QRegularExpressionMatch matchLatest = reLatest.match(output);
if (matchLatest.hasMatch()) { if (matchLatest.hasMatch()) {
@@ -1099,7 +1132,7 @@ QJsonObject ServersBackupController::parseBackupStatus(const QString &output)
} }
// ============================================================================ // ============================================================================
// ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ // HELPER METHODS
// ============================================================================ // ============================================================================
ErrorCode ServersBackupController::handleStdOut(const QString &data, QString &output) ErrorCode ServersBackupController::handleStdOut(const QString &data, QString &output)
@@ -1107,13 +1140,13 @@ ErrorCode ServersBackupController::handleStdOut(const QString &data, QString &ou
output += data; output += data;
qDebug().noquote() << "[BACKUP]" << data; qDebug().noquote() << "[BACKUP]" << data;
// Проверяем на ошибки в выводе // Check for errors in output
if (data.contains("[ERROR]") || data.contains("ERROR")) { if (data.contains("[ERROR]") || data.contains("ERROR")) {
// Ошибка обнаружена в stdout, но это не критично для handleStdOut // Error detected in stdout, but not critical for handleStdOut
// Основная проверка будет в restoreBackup после выполнения скрипта // Main check will be in restoreBackup after script execution
} }
// Обновляем прогресс на основе вывода // Update progress based on output
if (data.contains("Starting backup")) { if (data.contains("Starting backup")) {
setProgress(10, tr("Starting backup...")); setProgress(10, tr("Starting backup..."));
} else if (data.contains("Backing up")) { } else if (data.contains("Backing up")) {
@@ -1151,3 +1184,283 @@ void ServersBackupController::setProgress(int percent, const QString &message)
emit progressChanged(percent, 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();
}
}
+177 -75
View File
@@ -9,6 +9,7 @@
#include <QFileInfo> #include <QFileInfo>
class QTemporaryFile; class QTemporaryFile;
class ServersModel;
#include "core/controllers/serverController.h" #include "core/controllers/serverController.h"
#include "core/defs.h" #include "core/defs.h"
@@ -17,24 +18,24 @@ class QTemporaryFile;
using namespace amnezia; using namespace amnezia;
/** /**
* @brief Контроллер для управления backup конфигураций Amnezia VPN * @brief Controller for managing Amnezia VPN configuration backups
* *
* Использует существующий ServerController и libssh::Client из Amnezia * Uses existing ServerController and libssh::Client from Amnezia
* Bash скрипты встроены напрямую в C++ код * Bash scripts are embedded directly in C++ code
* Поддерживает backup конкретных контейнеров напрямую через docker cp * Supports direct container backup via docker cp
* *
* Полностью кроссплатформенный: Windows, macOS, Linux, iOS, Android * Fully cross-platform: Windows, macOS, Linux, iOS, Android
*/ */
class ServersBackupController : public QObject class ServersBackupController : public QObject
{ {
Q_OBJECT Q_OBJECT
public: public:
explicit ServersBackupController(std::shared_ptr<Settings> settings, QObject *parent = nullptr); explicit ServersBackupController(std::shared_ptr<Settings> settings, ServersModel *serversModel, QObject *parent = nullptr);
~ServersBackupController(); ~ServersBackupController();
/** /**
* @brief Информация о backup * @brief Backup information
*/ */
struct BackupInfo { struct BackupInfo {
QString filename; QString filename;
@@ -55,44 +56,52 @@ public:
public slots: public slots:
/** /**
* @brief Создать backup на сервере (всех контейнеров) * @brief Create backup on server (all containers)
* @param credentials Учетные данные сервера * @param credentials Server credentials
*/ */
void createBackup(const ServerCredentials &credentials); void createBackup(const ServerCredentials &credentials);
/** /**
* @brief Создать backup конкретного контейнера * @brief Create backup and automatically download to device (for QML)
* @param credentials Учетные данные сервера * @param downloadToDevice Download to device after creation?
* @param container Тип контейнера для backup * @param deleteFromServer Delete from server after download?
*/
Q_INVOKABLE void createBackupWithDownload(bool downloadToDevice = true,
bool deleteFromServer = true);
/**
* @brief Create backup of specific container
* @param credentials Server credentials
* @param container Container type for backup
*/ */
void createContainerBackup(const ServerCredentials &credentials, DockerContainer container); void createContainerBackup(const ServerCredentials &credentials, DockerContainer container);
/** /**
* @brief Создать backup конкретного контейнера по имени * @brief Create backup of specific container by name
* @param credentials Учетные данные сервера * @param credentials Server credentials
* @param containerName Имя контейнера (например "amnezia-awg") * @param containerName Container name (e.g. "amnezia-awg")
*/ */
void createBackupByName(const ServerCredentials &credentials, const QString &containerName); void createBackupByName(const ServerCredentials &credentials, const QString &containerName);
/** /**
* @brief Создать backup нескольких контейнеров * @brief Create backup of multiple containers
* @param credentials Учетные данные сервера * @param credentials Server credentials
* @param containers Список контейнеров для backup * @param containers List of containers for backup
*/ */
void createContainersBackup(const ServerCredentials &credentials, const QList<DockerContainer> &containers); void createContainersBackup(const ServerCredentials &credentials, const QList<DockerContainer> &containers);
/** /**
* @brief Получить список backup с сервера * @brief Get list of backups from server
* @param credentials Учетные данные сервера * @param credentials Server credentials
*/ */
void fetchBackupList(const ServerCredentials &credentials); void fetchBackupList(const ServerCredentials &credentials);
/** /**
* @brief Восстановить из backup * @brief Restore from backup
* @param credentials Учетные данные сервера * @param credentials Server credentials
* @param backupFilename Имя файла backup * @param backupFilename Backup file name
* @param containers Список контейнеров (пустой = все) * @param containers List of containers (empty = all)
* @param replaceMode Если true - сначала очищает контейнер, затем восстанавливает. Если false - добавляет данные поверх существующих * @param replaceMode If true - clears container first, then restores. If false - adds data on top of existing
*/ */
void restoreBackup(const ServerCredentials &credentials, void restoreBackup(const ServerCredentials &credentials,
const QString &backupFilename, const QString &backupFilename,
@@ -100,32 +109,32 @@ public slots:
bool replaceMode = false); bool replaceMode = false);
/** /**
* @brief Проверить состояние backup на сервере * @brief Check backup status on server
* @param credentials Учетные данные сервера * @param credentials Server credentials
*/ */
void checkBackupStatus(const ServerCredentials &credentials); void checkBackupStatus(const ServerCredentials &credentials);
/** /**
* @brief Скачать backup на локальную машину * @brief Download backup to local machine
* @param credentials Учетные данные сервера * @param credentials Server credentials
* @param backupFilename Имя файла backup * @param backupFilename Backup file name
* @param localPath Путь для сохранения * @param localPath Save path
*/ */
void downloadBackup(const ServerCredentials &credentials, void downloadBackup(const ServerCredentials &credentials,
const QString &backupFilename, const QString &backupFilename,
const QString &localPath); const QString &localPath);
/** /**
* @brief Загрузить backup на сервер * @brief Upload backup to server
* @param credentials Учетные данные сервера * @param credentials Server credentials
* @param localPath Путь к локальному файлу * @param localPath Path to local file
* @param replaceMode Режим восстановления (true = замена, false = добавление). Сохраняется для последующего использования в restoreBackup * @param replaceMode Restore mode (true = replace, false = add). Saved for later use in restoreBackup
*/ */
void uploadBackup(const ServerCredentials &credentials, void uploadBackup(const ServerCredentials &credentials,
const QString &localPath, const QString &localPath,
bool replaceMode = false); bool replaceMode = false);
// Перегруженный метод для setup wizard с отдельными параметрами credentials // Overloaded method for setup wizard with separate credential parameters
Q_INVOKABLE void uploadBackupWithStrings(const QString &hostname, Q_INVOKABLE void uploadBackupWithStrings(const QString &hostname,
const QString &username, const QString &username,
const QString &secretData, const QString &secretData,
@@ -133,154 +142,247 @@ public slots:
bool replaceMode = false); bool replaceMode = false);
/** /**
* @brief Сканировать backup файл и определить какие контейнеры в нем есть * @brief Universal method to start restore (from QML)
* @param localPath Путь к локальному backup файлу * Automatically selects correct path depending on parameters
* @return Список имен контейнеров найденных в backup * @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); Q_INVOKABLE QStringList scanBackupForContainers(const QString &localPath);
/** /**
* @brief Удалить backup с сервера * @brief Set default server and container after restore (for setup wizard)
* @param credentials Учетные данные сервера * @param isFromSetupWizard Was restore called from setup wizard
* @param backupFilename Имя файла backup * @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 Delete backup from server
* @param credentials Server credentials
* @param backupFilename Backup file name
*/ */
void deleteBackup(const ServerCredentials &credentials, void deleteBackup(const ServerCredentials &credentials,
const QString &backupFilename); const QString &backupFilename);
/** /**
* @brief Установить директорию backup на сервере * @brief Set backup directory on server
*/ */
void setBackupDirectory(const QString &directory); void setBackupDirectory(const QString &directory);
/** /**
* @brief Получить директорию backup * @brief Get backup directory
*/ */
QString backupDirectory() const { return m_backupDir; } QString backupDirectory() const { return m_backupDir; }
signals: signals:
/** /**
* @brief Изменился статус операции * @brief Operation status changed
*/ */
void statusChanged(BackupStatus status); void statusChanged(BackupStatus status);
/** /**
* @brief Прогресс операции (0-100) * @brief Operation progress (0-100)
*/ */
void progressChanged(int percent, const QString &message); void progressChanged(int percent, const QString &message);
/** /**
* @brief Получен список backup * @brief Backup list received
*/ */
void backupListReceived(const QList<BackupInfo> &backups); void backupListReceived(const QList<BackupInfo> &backups);
/** /**
* @brief Backup создан успешно * @brief Backup created successfully
*/ */
void backupCreated(const QString &backupFilename); void backupCreated(const QString &backupFilename);
/** /**
* @brief Backup восстановлен успешно * @brief Backup restored successfully
*/ */
void backupRestored(); void backupRestored();
/** /**
* @brief Backup скачан * @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 downloaded
*/ */
void backupDownloaded(const QString &localPath); void backupDownloaded(const QString &localPath);
/** /**
* @brief Backup загружен на сервер * @brief Backup uploaded to server
*/ */
void backupUploaded(const QString &serverPath); void backupUploaded(const QString &serverPath);
/** /**
* @brief Получена информация о состоянии backup * @brief Backup status information received
*/ */
void backupStatusReceived(const QJsonObject &status); void backupStatusReceived(const QJsonObject &status);
/** /**
* @brief Произошла ошибка * @brief Error occurred
*/ */
void errorOccurred(const QString &errorMessage, ErrorCode errorCode); void errorOccurred(const QString &errorMessage, ErrorCode errorCode);
private: private:
/** /**
* @brief Получить bash скрипт для создания backup всех контейнеров * @brief Get bash script for creating backup of all containers
* @param ipAddress IP адрес сервера в формате с подчеркиваниями (например "192_119_110_11") * @param ipAddress Server IP address in underscored format (e.g. "192_119_110_11")
*/ */
QString getBackupScript(const QString &ipAddress) const; QString getBackupScript(const QString &ipAddress) const;
/** /**
* @brief Получить bash скрипт для создания backup конкретного контейнера * @brief Get bash script for creating backup of specific container
* @param container Тип контейнера * @param container Container type
* @param ipAddress IP адрес сервера в формате с подчеркиваниями (например "192_119_110_11") * @param ipAddress Server IP address in underscored format (e.g. "192_119_110_11")
*/ */
QString getContainerBackupScript(DockerContainer container, const QString &ipAddress) const; QString getContainerBackupScript(DockerContainer container, const QString &ipAddress) const;
/** /**
* @brief Получить bash скрипт для создания backup нескольких контейнеров * @brief Get bash script for creating backup of multiple containers
* @param containers Список контейнеров * @param containers List of containers
* @param ipAddress IP адрес сервера в формате с подчеркиваниями (например "192_119_110_11") * @param ipAddress Server IP address in underscored format (e.g. "192_119_110_11")
*/ */
QString getContainersBackupScript(const QList<DockerContainer> &containers, const QString &ipAddress) const; QString getContainersBackupScript(const QList<DockerContainer> &containers, const QString &ipAddress) const;
/** /**
* @brief Получить bash скрипт для восстановления * @brief Get bash script for restore
* @param backupFilename Имя файла backup * @param backupFilename Backup file name
* @param containers Список контейнеров * @param containers List of containers
* @param replaceMode Если true - сначала очищает контейнер, затем восстанавливает * @param replaceMode If true - clears container first, then restores
*/ */
QString getRestoreScript(const QString &backupFilename, const QStringList &containers, bool replaceMode = false) const; QString getRestoreScript(const QString &backupFilename, const QStringList &containers, bool replaceMode = false) const;
/** /**
* @brief Получить bash скрипт для проверки состояния * @brief Get bash script for status check
*/ */
QString getCheckStatusScript() const; QString getCheckStatusScript() const;
/** /**
* @brief Получить bash скрипт для списка backup * @brief Get bash script for backup list
*/ */
QString getListBackupsScript() const; QString getListBackupsScript() const;
/** /**
* @brief Парсить список backup из вывода * @brief Parse backup list from output
*/ */
QList<BackupInfo> parseBackupList(const QString &output); QList<BackupInfo> parseBackupList(const QString &output);
/** /**
* @brief Парсить статус из вывода * @brief Parse status from output
*/ */
QJsonObject parseBackupStatus(const QString &output); QJsonObject parseBackupStatus(const QString &output);
/** /**
* @brief Обработать стандартный вывод * @brief Handle standard output
*/ */
ErrorCode handleStdOut(const QString &data, QString &output); ErrorCode handleStdOut(const QString &data, QString &output);
/** /**
* @brief Обработать вывод ошибок * @brief Handle error output
*/ */
ErrorCode handleStdErr(const QString &data, QString &error); ErrorCode handleStdErr(const QString &data, QString &error);
/** /**
* @brief Установить статус * @brief Set status
*/ */
void setStatus(BackupStatus status); void setStatus(BackupStatus status);
/** /**
* @brief Установить прогресс * @brief Set progress
*/ */
void setProgress(int percent, const QString &message); void setProgress(int percent, const QString &message);
/**
* @brief Attempt to set default container (called from timer)
*/
void trySetDefaultContainer();
private: private:
std::shared_ptr<Settings> m_settings; std::shared_ptr<Settings> m_settings;
ServersModel *m_serversModel;
ServerController *m_serverController; ServerController *m_serverController;
BackupStatus m_status; BackupStatus m_status;
QString m_backupDir; QString m_backupDir;
QString m_currentOutput; QString m_currentOutput;
QString m_currentError; QString m_currentError;
bool m_restoreReplaceMode; // Сохраняем режим восстановления для использования после uploadBackup bool m_restoreReplaceMode; // Save restore mode for use after uploadBackup
QTemporaryFile *m_tempUploadFile; // Временный файл для Android URI (чтобы не удалялся до завершения загрузки) 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 #endif // SERVERSBACKUPCONTROLLER_H
@@ -116,18 +116,9 @@ PageType {
} }
} }
// ============ Backup Functions ============
function getServerCredentials() {
var index = ServersModel.processedIndex
return ServersModel.getServerCredentials(index)
}
property bool downloadAfterCreate: false
function createBackup(shouldDownload) { function createBackup(shouldDownload) {
// По умолчанию shouldDownload = true, если не указано // Default shouldDownload = true
downloadAfterCreate = (shouldDownload !== undefined) ? shouldDownload : true var downloadAfterCreate = (shouldDownload !== undefined) ? shouldDownload : true
var headerText = downloadAfterCreate ? var headerText = downloadAfterCreate ?
qsTr("Create backup and download to device?") : qsTr("Create backup and download to device?") :
@@ -140,21 +131,15 @@ PageType {
var yesButtonFunction = function() { var yesButtonFunction = function() {
PageController.showBusyIndicator(true) PageController.showBusyIndicator(true)
// Call C++ method that manages download and delete automatically
var credentials = getServerCredentials() ServersBackupController.createBackupWithDownload(downloadAfterCreate, true)
// Всегда создаем backup всех контейнеров
ServersBackupController.createBackup(credentials)
} }
var noButtonFunction = function() {} var noButtonFunction = function() {}
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
} }
property string selectedBackupForRestore: ""
function restoreBackup() { function restoreBackup() {
// Для мобильных устройств используем все возможные расширения backup файлов
// Android преобразует расширения в MIME типы автоматически
var filter = GC.isMobile() ? "*.gz *.tgz *.tar.gz" : "Backup files (*.tar.gz *.backup *.tgz *.gz)" var filter = GC.isMobile() ? "*.gz *.tgz *.tar.gz" : "Backup files (*.tar.gz *.backup *.tgz *.gz)"
var localPath = SystemController.getFileName( var localPath = SystemController.getFileName(
qsTr("Select Backup to Restore"), qsTr("Select Backup to Restore"),
@@ -168,43 +153,25 @@ PageType {
return 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
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) { if (!serverIp || serverIp.length === 0) {
var credentials = getServerCredentials() serverIp = ServersModel.getProcessedServerData("hostName") || ""
serverIp = credentials.hostName || ""
} }
var serverName = ServersModel.getProcessedServerData("name") || qsTr("Server") 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") {
parentItem.push(PageController.getPagePath(PageEnum.PageSettingsServerRestoreMode), { parentItem.push(PageController.getPagePath(PageEnum.PageSettingsServerRestoreMode), {
"backupFilePath": localPath, "backupFilePath": localPath,
"backupFileName": fileName, "backupFileName": fileName,
@@ -218,50 +185,23 @@ PageType {
// ============ Backup Controller Connections ============ // ============ Backup Controller Connections ============
property string lastCreatedBackupFilename: ""
property string lastUploadedBackupFilename: ""
Connections { Connections {
target: ServersBackupController target: ServersBackupController
function onBackupCreated(backupFilename) { function onBackupCreated(backupFilename) {
lastCreatedBackupFilename = backupFilename // If auto-download is not enabled, show success message
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.showBusyIndicator(false)
PageController.showNotificationMessage(qsTr("Backup created successfully: %1").arg(backupFilename)) PageController.showNotificationMessage(qsTr("Backup created successfully: %1").arg(backupFilename))
} }
}
function onBackupDownloaded(localPath) { function onBackupDownloaded(localPath) {
PageController.showBusyIndicator(false) PageController.showBusyIndicator(false)
console.log("Backup downloaded to:", localPath) 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)) PageController.showNotificationMessage(qsTr("Backup downloaded successfully!\n\nSaved to:\n%1").arg(localPath))
} }
function onBackupUploaded(serverPath) {
// Этот обработчик больше не используется здесь, так как восстановление
// теперь происходит через PageSettingsServerRestoreMode
// Оставляем для совместимости, но не выполняем действий
}
function onBackupRestored() { function onBackupRestored() {
PageController.showBusyIndicator(false) PageController.showBusyIndicator(false)
selectedBackupForRestore = ""
PageController.showNotificationMessage(qsTr("Backup restored successfully! Containers are restarting...")) PageController.showNotificationMessage(qsTr("Backup restored successfully! Containers are restarting..."))
} }
@@ -20,7 +20,7 @@ PageType {
property string serverIp: "" property string serverIp: ""
property bool isFromSetupWizard: false 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 wizardHostname: ""
property string wizardUsername: "" property string wizardUsername: ""
property string wizardSecretData: "" property string wizardSecretData: ""
@@ -71,7 +71,7 @@ PageType {
Layout.rightMargin: 16 Layout.rightMargin: 16
text: { text: {
// Показываем только имя файла и IP адрес, без имени сервера // Show only filename and IP address, without server name
if (serverIp && serverIp.length > 0) { if (serverIp && serverIp.length > 0) {
return qsTr("%1 on %2").arg(backupFileName).arg(serverIp) return qsTr("%1 on %2").arg(backupFileName).arg(serverIp)
} }
@@ -95,7 +95,7 @@ PageType {
rightImageSource: "qrc:/images/controls/chevron-right.svg" rightImageSource: "qrc:/images/controls/chevron-right.svg"
clickedFunction: function() { clickedFunction: function() {
startRestore(false) // false = режим добавления startRestore(false) // false = add mode
} }
} }
@@ -110,7 +110,7 @@ PageType {
textColor: AmneziaStyle.color.vibrantRed textColor: AmneziaStyle.color.vibrantRed
clickedFunction: function() { clickedFunction: function() {
startRestore(true) // true = режим замены startRestore(true) // true = replace mode
} }
} }
} }
@@ -121,44 +121,17 @@ PageType {
function startRestore(replaceMode) { function startRestore(replaceMode) {
restoreReplaceMode = replaceMode restoreReplaceMode = replaceMode
// Если это setup wizard с wizard credentials, используем их напрямую
if (isFromSetupWizard && wizardHostname.length > 0) {
console.log("Setup wizard mode, using uploadBackupWithStrings")
PageController.showBusyIndicator(true) PageController.showBusyIndicator(true)
ServersBackupController.uploadBackupWithStrings(
wizardHostname, // Call universal C++ method that will determine how to perform restore
wizardUsername, ServersBackupController.startRestore(
wizardSecretData, isFromSetupWizard,
backupFilePath, backupFilePath,
replaceMode replaceMode,
wizardHostname || "",
wizardUsername || "",
wizardSecretData || ""
) )
} 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)
} }
property string lastUploadedBackupFilename: "" property string lastUploadedBackupFilename: ""
@@ -166,49 +139,36 @@ PageType {
Connections { Connections {
target: ServersBackupController 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() { function onBackupRestored() {
console.log(" onBackupRestored, isFromSetupWizard:", isFromSetupWizard) console.log(" onBackupRestored, isFromSetupWizard:", isFromSetupWizard)
PageController.showBusyIndicator(false) // For setup wizard, call C++ method to set default server and container
if (isFromSetupWizard) {
// Для setup wizard устанавливаем default container и сервер ServersBackupController.setDefaultServerAfterRestore(true)
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()
} else { } else {
// Для обычного режима сразу переходим // For regular mode, navigate directly
PageController.showBusyIndicator(false)
navigateToRestoredPage() 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) { function onErrorOccurred(errorMessage, errorCode) {
PageController.showBusyIndicator(false) PageController.showBusyIndicator(false)
PageController.showErrorMessage(qsTr("Backup restore error: %1").arg(errorMessage)) PageController.showErrorMessage(qsTr("Backup restore error: %1").arg(errorMessage))
} }
} }
// Удаляем Connections для scanServerFinished - больше не нужен
function navigateToRestoredPage() { function navigateToRestoredPage() {
// Переход на страницу успешного восстановления // Navigate to successful restore page
// Получаем реальное имя сервера из модели // Get actual server name from model
var actualServerName = serverName var actualServerName = serverName
if (root.isFromSetupWizard && ServersModel.getServersCount() > 0) { if (root.isFromSetupWizard && ServersModel.getServersCount() > 0) {
var serverIdx = ServersModel.getServersCount() - 1 var serverIdx = ServersModel.getServersCount() - 1
@@ -217,13 +177,13 @@ PageType {
actualServerName = ServersModel.getProcessedServerData("name") || qsTr("Server") actualServerName = ServersModel.getProcessedServerData("name") || qsTr("Server")
ServersModel.processedIndex = oldProcessedIndex ServersModel.processedIndex = oldProcessedIndex
} else if (!serverName || serverName.length === 0) { } else if (!serverName || serverName.length === 0) {
// Если имя не передано, получаем из processedIndex // If name not provided, get from processedIndex
actualServerName = ServersModel.getProcessedServerData("name") || qsTr("Server") actualServerName = ServersModel.getProcessedServerData("name") || qsTr("Server")
} }
var parentItem = root.parent var parentItem = root.parent
// Для setup wizard используем обычный StackView // For setup wizard use regular StackView
if (root.isFromSetupWizard) { if (root.isFromSetupWizard) {
while (parentItem && typeof parentItem.push !== "function") { while (parentItem && typeof parentItem.push !== "function") {
parentItem = parentItem.parent parentItem = parentItem.parent
@@ -237,7 +197,7 @@ PageType {
}) })
} }
} else { } else {
// Для меню управления ищем tabBarStackView // For management menu, find tabBarStackView
while (parentItem && parentItem.objectName !== "tabBarStackView") { while (parentItem && parentItem.objectName !== "tabBarStackView") {
parentItem = parentItem.parent 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()
}
}
}
} }
@@ -15,7 +15,6 @@ PageType {
property var setupWizardEasy: null property var setupWizardEasy: null
// Сохраняем credentials здесь для использования при восстановлении backup
property string savedHostname: "" property string savedHostname: ""
property string savedUsername: "" property string savedUsername: ""
property string savedSecretData: "" property string savedSecretData: ""
@@ -137,7 +136,6 @@ PageType {
return return
} }
// Сохраняем credentials в свойствах этой страницы
root.savedHostname = _hostname root.savedHostname = _hostname
root.savedUsername = _username root.savedUsername = _username
root.savedSecretData = _secretData root.savedSecretData = _secretData
+74 -60
View File
@@ -25,12 +25,51 @@ PageType {
property string restoreSecretData: "" property string restoreSecretData: ""
property bool waitingForServerToAdd: false property bool waitingForServerToAdd: false
// Для установки контейнеров из backup // For installing containers from backup
property var containersToInstall: [] property var containersToInstall: []
property int currentContainerIndex: 0 property int currentContainerIndex: 0
property bool isInstallingContainers: false 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 { Connections {
target: InstallController target: InstallController
@@ -39,10 +78,10 @@ PageType {
console.log("Server added successfully, now installing containers from backup...") console.log("Server added successfully, now installing containers from backup...")
root.waitingForServerToAdd = false root.waitingForServerToAdd = false
// Сервер уже создан, устанавливаем флаг в false // Server already created, set flag to false
InstallController.setShouldCreateServer(false) InstallController.setShouldCreateServer(false)
// Начинаем установку контейнеров // Start installing containers
root.isInstallingContainers = true root.isInstallingContainers = true
installNextContainer() installNextContainer()
} }
@@ -52,28 +91,28 @@ PageType {
if (root.isInstallingContainers) { if (root.isInstallingContainers) {
console.log("Container installed:", finishedMessage) console.log("Container installed:", finishedMessage)
// Переходим к следующему контейнеру // Move to next container
root.currentContainerIndex++ root.currentContainerIndex++
if (root.currentContainerIndex < root.containersToInstall.length) { if (root.currentContainerIndex < root.containersToInstall.length) {
// Устанавливаем следующий контейнер // Install next container
installNextContainer() installNextContainer()
} else { } else {
// Все контейнеры установлены, теперь делаем restore // All containers installed, now do restore
console.log("All containers installed, starting restore...") console.log("All containers installed, starting restore...")
root.isInstallingContainers = false root.isInstallingContainers = false
// ВАЖНО: Выключаем busy indicator перед переходом // IMPORTANT: Turn off busy indicator before navigation
PageController.showBusyIndicator(false) PageController.showBusyIndicator(false)
// Запускаем переход на страницу выбора режима restore // Start navigation to restore mode selection page
navigationTimer.start() navigationTimer.start()
} }
} }
} }
} }
// Функция для установки следующего контейнера из списка // Function to install next container from list
function installNextContainer() { function installNextContainer() {
if (root.currentContainerIndex >= root.containersToInstall.length) { if (root.currentContainerIndex >= root.containersToInstall.length) {
return return
@@ -82,7 +121,7 @@ PageType {
var containerName = root.containersToInstall[root.currentContainerIndex] var containerName = root.containersToInstall[root.currentContainerIndex]
console.log("Installing container:", containerName, "(", root.currentContainerIndex + 1, "/", root.containersToInstall.length, ")") console.log("Installing container:", containerName, "(", root.currentContainerIndex + 1, "/", root.containersToInstall.length, ")")
// Конвертируем имя контейнера в DockerContainer enum // Convert container name to DockerContainer enum
var dockerContainer = ContainerProps.containerFromString(containerName) var dockerContainer = ContainerProps.containerFromString(containerName)
if (dockerContainer === 0) { // None if (dockerContainer === 0) { // None
@@ -92,33 +131,33 @@ PageType {
return return
} }
// Получаем default настройки для контейнера // Get default settings for container
var defaultProtocol = ContainerProps.defaultProtocol(dockerContainer) var defaultProtocol = ContainerProps.defaultProtocol(dockerContainer)
var defaultPort = ProtocolProps.getPortForInstall(defaultProtocol) var defaultPort = ProtocolProps.getPortForInstall(defaultProtocol)
var defaultTransport = ProtocolProps.defaultTransportProto(defaultProtocol) var defaultTransport = ProtocolProps.defaultTransportProto(defaultProtocol)
// Показываем индикатор загрузки с сообщением // Show loading indicator with message
PageController.showBusyIndicator(true) PageController.showBusyIndicator(true)
PageController.showNotificationMessage(qsTr("Installing %1 (%2/%3)...") PageController.showNotificationMessage(qsTr("Installing %1 (%2/%3)...")
.arg(containerName) .arg(containerName)
.arg(root.currentContainerIndex + 1) .arg(root.currentContainerIndex + 1)
.arg(root.containersToInstall.length)) .arg(root.containersToInstall.length))
// Убеждаемся что credentials установлены // Ensure credentials are set
console.log("Setting credentials for container installation...") console.log("Setting credentials for container installation...")
InstallController.setProcessedServerCredentials(root.restoreHostname, root.restoreUsername, root.restoreSecretData) InstallController.setProcessedServerCredentials(root.restoreHostname, root.restoreUsername, root.restoreSecretData)
// Устанавливаем индекс сервера // Set server index
var serverIdx = ServersModel.getServersCount() - 1 var serverIdx = ServersModel.getServersCount() - 1
ServersModel.processedIndex = serverIdx ServersModel.processedIndex = serverIdx
// Устанавливаем контейнер // Install container
console.log("Calling InstallController.install for docker container:", dockerContainer) console.log("Calling InstallController.install for docker container:", dockerContainer)
ContainersModel.setProcessedContainerIndex(dockerContainer) ContainersModel.setProcessedContainerIndex(dockerContainer)
InstallController.install(dockerContainer, defaultPort, defaultTransport) InstallController.install(dockerContainer, defaultPort, defaultTransport)
} }
// Таймер для перехода на страницу выбора режима после выбора файла // Timer for navigating to restore mode selection page after file selection
Timer { Timer {
id: navigationTimer id: navigationTimer
interval: 500 interval: 500
@@ -128,7 +167,7 @@ PageType {
console.log("Navigation timer triggered, going to restore mode page") console.log("Navigation timer triggered, going to restore mode page")
console.log("Credentials available:", root.restoreHostname, root.restoreUsername, root.restoreSecretData.length > 0 ? "***" : "EMPTY") console.log("Credentials available:", root.restoreHostname, root.restoreUsername, root.restoreSecretData.length > 0 ? "***" : "EMPTY")
// Получаем имя файла // Get filename
var fileName = SystemController.getFileNameFromPath(root.backupFilePath) var fileName = SystemController.getFileNameFromPath(root.backupFilePath)
if (!fileName || fileName === undefined || fileName.length === 0) { if (!fileName || fileName === undefined || fileName.length === 0) {
var fallbackName = root.backupFilePath.split('/').pop() var fallbackName = root.backupFilePath.split('/').pop()
@@ -136,7 +175,7 @@ PageType {
} }
fileName = String(fileName) fileName = String(fileName)
// Извлекаем IP адрес из имени файла // Extract IP address from filename
var serverIp = "" var serverIp = ""
var ipMatch = fileName.match(/^([\d_]+)\s*-/) var ipMatch = fileName.match(/^([\d_]+)\s*-/)
if (ipMatch && ipMatch.length > 1) { if (ipMatch && ipMatch.length > 1) {
@@ -151,30 +190,30 @@ PageType {
serverName = qsTr("RestoredServer") serverName = qsTr("RestoredServer")
} }
// Переходим на страницу установки // Navigate to installation page
PageController.goToPage(PageEnum.PageSetupWizardInstalling) PageController.goToPage(PageEnum.PageSetupWizardInstalling)
// Сразу ищем StackView и переходим на страницу восстановления // Immediately find StackView and navigate to restore page
// Сервер уже добавлен, так как мы ждали onInstallServerFinished // Server already added, as we waited for onInstallServerFinished
Qt.callLater(function() { Qt.callLater(function() {
var pagePath = "qrc:/ui/qml/Pages2/PageSettingsServerRestoreMode.qml" var pagePath = "qrc:/ui/qml/Pages2/PageSettingsServerRestoreMode.qml"
// Находим главное окно приложения // Find main application window
var item = root var item = root
while (item.parent) { while (item.parent) {
item = item.parent item = item.parent
} }
// Находим StackView рекурсивно // Find StackView recursively
function findStackView(obj) { function findStackView(obj) {
if (!obj) return null if (!obj) return null
// Проверяем, является ли объект StackView // Check if object is StackView
if (obj.toString().indexOf("StackView") !== -1 || typeof obj.push === "function") { if (obj.toString().indexOf("StackView") !== -1 || typeof obj.push === "function") {
return obj return obj
} }
// Проверяем children // Check children
if (obj.children) { if (obj.children) {
for (var i = 0; i < obj.children.length; i++) { for (var i = 0; i < obj.children.length; i++) {
var result = findStackView(obj.children[i]) var result = findStackView(obj.children[i])
@@ -182,7 +221,7 @@ PageType {
} }
} }
// Проверяем contentItem // Check contentItem
if (obj.contentItem) { if (obj.contentItem) {
return findStackView(obj.contentItem) return findStackView(obj.contentItem)
} }
@@ -196,7 +235,7 @@ PageType {
stackView.push(pagePath, { stackView.push(pagePath, {
"backupFilePath": root.backupFilePath, "backupFilePath": root.backupFilePath,
"backupFileName": fileName, "backupFileName": fileName,
"serverName": "", // Будет получено из ServersModel "serverName": "", // Will be obtained from ServersModel
"serverIp": serverIp, "serverIp": serverIp,
"isFromSetupWizard": true, "isFromSetupWizard": true,
"wizardHostname": root.restoreHostname, "wizardHostname": root.restoreHostname,
@@ -359,9 +398,7 @@ PageType {
ButtonGroup.group: buttonGroup ButtonGroup.group: buttonGroup
onClicked: function() { onClicked: function() {
console.log("=== Restore from backup clicked ===")
// СНАЧАЛА выбираем файл
var filter = GC.isMobile() ? "*.gz *.tgz *.tar.gz" : "Backup files (*.tar.gz *.backup *.tgz *.gz)" var filter = GC.isMobile() ? "*.gz *.tgz *.tar.gz" : "Backup files (*.tar.gz *.backup *.tgz *.gz)"
var localPath = SystemController.getFileName( var localPath = SystemController.getFileName(
qsTr("Select Backup to Restore"), qsTr("Select Backup to Restore"),
@@ -378,34 +415,20 @@ PageType {
return return
} }
// Сохраняем путь к backup файлу // Save backup file path
root.backupFilePath = localPath root.backupFilePath = localPath
root.isRestoreFromBackup = true root.isRestoreFromBackup = true
// Сканируем backup для определения контейнеров // Get credentials from PageSetupWizardCredentials via StackView search
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
var credentialsPage = null var credentialsPage = null
var item = root var item = root
// Ищем StackView // Find StackView
while (item && !item.hasOwnProperty("depth")) { while (item && !item.hasOwnProperty("depth")) {
item = item.parent item = item.parent
} }
// Если нашли StackView, ищем PageSetupWizardCredentials в его истории // If found StackView, search for PageSetupWizardCredentials in its history
if (item && item.depth > 0) { if (item && item.depth > 0) {
for (var i = 0; i < item.depth; i++) { for (var i = 0; i < item.depth; i++) {
var page = item.get(i) var page = item.get(i)
@@ -422,18 +445,9 @@ PageType {
root.restoreSecretData = credentialsPage.savedSecretData root.restoreSecretData = credentialsPage.savedSecretData
console.log("Got credentials from PageSetupWizardCredentials:", root.restoreHostname, root.restoreUsername) console.log("Got credentials from PageSetupWizardCredentials:", root.restoreHostname, root.restoreUsername)
// ТЕПЕРЬ добавляем пустой сервер с этими credentials // Call C++ method to prepare restore
InstallController.setShouldCreateServer(true) // It will scan backup and send readyForRestore signal
InstallController.setProcessedServerCredentials(root.restoreHostname, root.restoreUsername, root.restoreSecretData) ServersBackupController.prepareRestoreFromBackup(localPath, root.restoreHostname, root.restoreUsername, root.restoreSecretData)
// Устанавливаем флаг ожидания
root.waitingForServerToAdd = true
console.log("Backup file selected, adding server...")
// Добавляем сервер (асинхронно)
InstallController.addEmptyServer()
// Дальнейшее выполнение произойдет в onInstallServerFinished
} else { } else {
console.log("WARNING: No credentials found") console.log("WARNING: No credentials found")
return return