mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-06-23 02:00:20 +07:00
fix: add container copy optimization
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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"));
|
||||||
emit backupCreated(backupFilename);
|
|
||||||
|
// Save filename for later deletion
|
||||||
|
m_lastCreatedBackupFilename = backupFilename;
|
||||||
|
|
||||||
|
// If auto-download enabled, start it
|
||||||
|
if (m_autoDownloadAfterCreate) {
|
||||||
|
qDebug() << "Auto-downloading backup after creation";
|
||||||
|
m_autoDownloadAfterCreate = false; // Reset flag
|
||||||
|
downloadBackup(credentials, backupFilename, backupFilename);
|
||||||
|
} else {
|
||||||
|
emit backupCreated(backupFilename);
|
||||||
|
}
|
||||||
} else {
|
} 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
if (!serverIp || serverIp.length === 0) {
|
||||||
|
serverIp = ServersModel.getProcessedServerData("hostName") || ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var serverName = ServersModel.getProcessedServerData("name") || qsTr("Server")
|
||||||
|
|
||||||
|
// Open restore mode selection page
|
||||||
var parentItem = root.parent
|
var parentItem = root.parent
|
||||||
while (parentItem && parentItem.objectName !== "tabBarStackView") {
|
while (parentItem && parentItem.objectName !== "tabBarStackView") {
|
||||||
parentItem = parentItem.parent
|
parentItem = parentItem.parent
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parentItem && typeof parentItem.push === "function") {
|
if (parentItem && typeof parentItem.push === "function") {
|
||||||
// Используем SystemController для получения имени файла из пути или URI
|
|
||||||
// Это правильно обработает Android URI через ContentResolver
|
|
||||||
var fileName = SystemController.getFileNameFromPath(localPath)
|
|
||||||
|
|
||||||
// Если имя файла пустое или undefined, используем fallback
|
|
||||||
if (!fileName || fileName === undefined || fileName.length === 0) {
|
|
||||||
var fallbackName = localPath.split('/').pop()
|
|
||||||
fileName = (fallbackName && fallbackName.length > 0) ? fallbackName : qsTr("backup.tgz")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Убеждаемся, что fileName - это строка
|
|
||||||
fileName = String(fileName)
|
|
||||||
|
|
||||||
// Извлекаем IP адрес из имени файла (формат: IP_ADDRESS - DD-MM-YYYY_HH-MM-SS.tgz)
|
|
||||||
var serverIp = ""
|
|
||||||
var ipMatch = fileName.match(/^([\d_]+)\s*-/)
|
|
||||||
if (ipMatch && ipMatch.length > 1) {
|
|
||||||
// Заменяем подчеркивания на точки для отображения IP адреса
|
|
||||||
serverIp = ipMatch[1].replace(/_/g, ".")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Если не удалось извлечь IP из имени файла, используем IP из credentials
|
|
||||||
if (!serverIp || serverIp.length === 0) {
|
|
||||||
var credentials = getServerCredentials()
|
|
||||||
serverIp = credentials.hostName || ""
|
|
||||||
}
|
|
||||||
|
|
||||||
var serverName = ServersModel.getProcessedServerData("name") || qsTr("Server")
|
|
||||||
|
|
||||||
parentItem.push(PageController.getPagePath(PageEnum.PageSettingsServerRestoreMode), {
|
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
|
||||||
|
PageController.showBusyIndicator(false)
|
||||||
if (downloadAfterCreate) {
|
PageController.showNotificationMessage(qsTr("Backup created successfully: %1").arg(backupFilename))
|
||||||
var credentials = getServerCredentials()
|
|
||||||
var localPath = backupFilename
|
|
||||||
PageController.showNotificationMessage(qsTr("Backup created. Downloading to device..."))
|
|
||||||
ServersBackupController.downloadBackup(credentials, backupFilename, localPath)
|
|
||||||
downloadAfterCreate = false
|
|
||||||
} else {
|
|
||||||
PageController.showBusyIndicator(false)
|
|
||||||
PageController.showNotificationMessage(qsTr("Backup created successfully: %1").arg(backupFilename))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
PageController.showBusyIndicator(true)
|
||||||
|
|
||||||
// Если это setup wizard с wizard credentials, используем их напрямую
|
// Call universal C++ method that will determine how to perform restore
|
||||||
if (isFromSetupWizard && wizardHostname.length > 0) {
|
ServersBackupController.startRestore(
|
||||||
console.log("Setup wizard mode, using uploadBackupWithStrings")
|
isFromSetupWizard,
|
||||||
PageController.showBusyIndicator(true)
|
backupFilePath,
|
||||||
ServersBackupController.uploadBackupWithStrings(
|
replaceMode,
|
||||||
wizardHostname,
|
wizardHostname || "",
|
||||||
wizardUsername,
|
wizardUsername || "",
|
||||||
wizardSecretData,
|
wizardSecretData || ""
|
||||||
backupFilePath,
|
)
|
||||||
replaceMode
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// Обычный режим - берем из ServersModel
|
|
||||||
PageController.showBusyIndicator(true)
|
|
||||||
var credentials = getServerCredentials()
|
|
||||||
ServersBackupController.uploadBackup(credentials, backupFilePath, replaceMode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getServerCredentials() {
|
|
||||||
// Если это setup wizard, используем переданные credentials
|
|
||||||
if (isFromSetupWizard && wizardHostname.length > 0) {
|
|
||||||
console.log("Using wizard credentials:", wizardHostname, wizardUsername)
|
|
||||||
// Устанавливаем credentials в InstallController
|
|
||||||
InstallController.setProcessedServerCredentials(wizardHostname, wizardUsername, wizardSecretData)
|
|
||||||
// Получаем их обратно как C++ объект через ServersModel
|
|
||||||
// Сначала проверяем, есть ли сервер в модели
|
|
||||||
if (ServersModel.getServersCount() > 0 && ServersModel.processedIndex >= 0) {
|
|
||||||
return ServersModel.getServerCredentials(ServersModel.processedIndex)
|
|
||||||
}
|
|
||||||
// Если сервера нет, создаем временный объект (это не сработает, нужен другой подход)
|
|
||||||
console.error("Server not in model yet, cannot get credentials")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Иначе берем из ServersModel
|
|
||||||
var index = ServersModel.processedIndex
|
|
||||||
return ServersModel.getServerCredentials(index)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user