mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-06-22 02:01:08 +07:00
fix: restore configs
This commit is contained in:
@@ -32,6 +32,9 @@ namespace PageLoader
|
||||
PageSettingsNewsNotifications,
|
||||
PageSettingsNewsDetail,
|
||||
PageSettingsBackup,
|
||||
PageSettingsServerBackup,
|
||||
PageSettingsServerRestoreMode,
|
||||
PageSettingsServerBackupRestored,
|
||||
PageSettingsAbout,
|
||||
PageSettingsLogging,
|
||||
PageSettingsSplitTunneling,
|
||||
@@ -151,6 +154,7 @@ signals:
|
||||
void goToPageSettings();
|
||||
void goToPageViewConfig();
|
||||
void goToPageSettingsServerServices();
|
||||
void goToPageSettingsServerManagement();
|
||||
void goToPageSettingsBackup();
|
||||
void goToShareConnectionPage(QString headerText, QString configContentHeaderText, QString configCaption, QString configExtension,
|
||||
QString configFileName);
|
||||
|
||||
@@ -4,13 +4,18 @@
|
||||
#include <QRegularExpression>
|
||||
#include <QJsonDocument>
|
||||
#include <QStandardPaths>
|
||||
#include <QTemporaryFile>
|
||||
#include <QUrl>
|
||||
#ifdef Q_OS_ANDROID
|
||||
#include <QJniObject>
|
||||
#include "platforms/android/android_controller.h"
|
||||
#endif
|
||||
#ifdef Q_OS_IOS
|
||||
#include "platforms/ios/ios_controller.h"
|
||||
#endif
|
||||
#include "containers/containers_defs.h"
|
||||
#include "core/networkUtilities.h"
|
||||
#include "systemController.h"
|
||||
|
||||
ServersBackupController::ServersBackupController(std::shared_ptr<Settings> settings, QObject *parent)
|
||||
: QObject(parent)
|
||||
@@ -18,6 +23,8 @@ ServersBackupController::ServersBackupController(std::shared_ptr<Settings> setti
|
||||
, m_serverController(new ServerController(settings, this))
|
||||
, m_status(Idle)
|
||||
, m_backupDir("/var/backups/amnezia")
|
||||
, m_restoreReplaceMode(false)
|
||||
, m_tempUploadFile(nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -43,8 +50,17 @@ void ServersBackupController::createBackup(const ServerCredentials &credentials)
|
||||
m_currentOutput.clear();
|
||||
m_currentError.clear();
|
||||
|
||||
// Получаем bash скрипт для backup
|
||||
QString script = getBackupScript();
|
||||
// Получаем IP адрес сервера
|
||||
QString serverIp = NetworkUtilities::getIPAddress(credentials.hostName);
|
||||
if (serverIp.isEmpty()) {
|
||||
serverIp = credentials.hostName;
|
||||
}
|
||||
// Форматируем IP: заменяем точки на подчеркивания
|
||||
QString ipFormatted = serverIp;
|
||||
ipFormatted.replace(".", "_");
|
||||
|
||||
// Получаем bash скрипт для backup с IP адресом
|
||||
QString script = getBackupScript(ipFormatted);
|
||||
|
||||
// Callback для обработки stdout
|
||||
auto cbStdOut = [this](const QString &data, libssh::Client &client) -> ErrorCode {
|
||||
@@ -62,12 +78,12 @@ void ServersBackupController::createBackup(const ServerCredentials &credentials)
|
||||
ErrorCode error = m_serverController->runHostScript(credentials, script, cbStdOut, cbStdErr);
|
||||
|
||||
if (error == ErrorCode::NoError) {
|
||||
// Парсим имя созданного backup из вывода
|
||||
QRegularExpression re("backup_(\\d{8}_\\d{6})\\.tar\\.gz");
|
||||
// Парсим имя созданного backup из вывода: формат "IP_ADDRESS - DD-MM-YYYY_HH-MM-SS.tgz"
|
||||
QRegularExpression re("([\\d_]+)\\s+-\\s+(\\d{2}-\\d{2}-\\d{4})_(\\d{2}-\\d{2}-\\d{2})\\.tgz");
|
||||
QRegularExpressionMatch match = re.match(m_currentOutput);
|
||||
|
||||
if (match.hasMatch()) {
|
||||
QString backupFilename = "backup_" + match.captured(1) + ".tar.gz";
|
||||
QString backupFilename = match.captured(1) + " - " + match.captured(2) + "_" + match.captured(3) + ".tgz";
|
||||
setStatus(Success);
|
||||
setProgress(100, tr("Backup created successfully"));
|
||||
emit backupCreated(backupFilename);
|
||||
@@ -105,7 +121,16 @@ void ServersBackupController::createContainerBackup(const ServerCredentials &cre
|
||||
m_currentOutput.clear();
|
||||
m_currentError.clear();
|
||||
|
||||
QString script = getContainerBackupScript(container);
|
||||
// Получаем IP адрес сервера
|
||||
QString serverIp = NetworkUtilities::getIPAddress(credentials.hostName);
|
||||
if (serverIp.isEmpty()) {
|
||||
serverIp = credentials.hostName;
|
||||
}
|
||||
// Форматируем IP: заменяем точки на подчеркивания
|
||||
QString ipFormatted = serverIp;
|
||||
ipFormatted.replace(".", "_");
|
||||
|
||||
QString script = getContainerBackupScript(container, ipFormatted);
|
||||
|
||||
auto cbStdOut = [this](const QString &data, libssh::Client &client) -> ErrorCode {
|
||||
Q_UNUSED(client);
|
||||
@@ -120,11 +145,12 @@ void ServersBackupController::createContainerBackup(const ServerCredentials &cre
|
||||
ErrorCode error = m_serverController->runHostScript(credentials, script, cbStdOut, cbStdErr);
|
||||
|
||||
if (error == ErrorCode::NoError) {
|
||||
QRegularExpression re("backup_(\\d{8}_\\d{6})\\.tar\\.gz");
|
||||
// Парсим имя созданного backup из вывода: формат "IP_ADDRESS - DD-MM-YYYY_HH-MM-SS.tgz"
|
||||
QRegularExpression re("([\\d_]+)\\s+-\\s+(\\d{2}-\\d{2}-\\d{4})_(\\d{2}-\\d{2}-\\d{2})\\.tgz");
|
||||
QRegularExpressionMatch match = re.match(m_currentOutput);
|
||||
|
||||
if (match.hasMatch()) {
|
||||
QString backupFilename = "backup_" + match.captured(1) + ".tar.gz";
|
||||
QString backupFilename = match.captured(1) + " - " + match.captured(2) + "_" + match.captured(3) + ".tgz";
|
||||
setStatus(Success);
|
||||
setProgress(100, tr("Container backup created successfully"));
|
||||
emit backupCreated(backupFilename);
|
||||
@@ -151,7 +177,16 @@ void ServersBackupController::createContainersBackup(const ServerCredentials &cr
|
||||
m_currentOutput.clear();
|
||||
m_currentError.clear();
|
||||
|
||||
QString script = getContainersBackupScript(containers);
|
||||
// Получаем IP адрес сервера
|
||||
QString serverIp = NetworkUtilities::getIPAddress(credentials.hostName);
|
||||
if (serverIp.isEmpty()) {
|
||||
serverIp = credentials.hostName;
|
||||
}
|
||||
// Форматируем IP: заменяем точки на подчеркивания
|
||||
QString ipFormatted = serverIp;
|
||||
ipFormatted.replace(".", "_");
|
||||
|
||||
QString script = getContainersBackupScript(containers, ipFormatted);
|
||||
|
||||
auto cbStdOut = [this](const QString &data, libssh::Client &client) -> ErrorCode {
|
||||
Q_UNUSED(client);
|
||||
@@ -166,11 +201,12 @@ void ServersBackupController::createContainersBackup(const ServerCredentials &cr
|
||||
ErrorCode error = m_serverController->runHostScript(credentials, script, cbStdOut, cbStdErr);
|
||||
|
||||
if (error == ErrorCode::NoError) {
|
||||
QRegularExpression re("backup_(\\d{8}_\\d{6})\\.tar\\.gz");
|
||||
// Парсим имя созданного backup из вывода: формат "IP_ADDRESS - DD-MM-YYYY_HH-MM-SS.tgz"
|
||||
QRegularExpression re("([\\d_]+)\\s+-\\s+(\\d{2}-\\d{2}-\\d{4})_(\\d{2}-\\d{2}-\\d{2})\\.tgz");
|
||||
QRegularExpressionMatch match = re.match(m_currentOutput);
|
||||
|
||||
if (match.hasMatch()) {
|
||||
QString backupFilename = "backup_" + match.captured(1) + ".tar.gz";
|
||||
QString backupFilename = match.captured(1) + " - " + match.captured(2) + "_" + match.captured(3) + ".tgz";
|
||||
setStatus(Success);
|
||||
setProgress(100, tr("Containers backup created successfully"));
|
||||
emit backupCreated(backupFilename);
|
||||
@@ -224,7 +260,8 @@ void ServersBackupController::fetchBackupList(const ServerCredentials &credentia
|
||||
|
||||
void ServersBackupController::restoreBackup(const ServerCredentials &credentials,
|
||||
const QString &backupFilename,
|
||||
const QStringList &containers)
|
||||
const QStringList &containers,
|
||||
bool replaceMode)
|
||||
{
|
||||
if (m_status == InProgress) {
|
||||
emit errorOccurred("Another operation is in progress", ErrorCode::AmneziaServiceConnectionFailed);
|
||||
@@ -232,12 +269,13 @@ void ServersBackupController::restoreBackup(const ServerCredentials &credentials
|
||||
}
|
||||
|
||||
setStatus(InProgress);
|
||||
setProgress(0, tr("Starting restore from %1...").arg(backupFilename));
|
||||
QString modeText = replaceMode ? tr("replace mode") : tr("add mode");
|
||||
setProgress(0, tr("Starting restore from %1 (%2)...").arg(backupFilename, modeText));
|
||||
|
||||
m_currentOutput.clear();
|
||||
m_currentError.clear();
|
||||
|
||||
QString script = getRestoreScript(backupFilename, containers);
|
||||
QString script = getRestoreScript(backupFilename, containers, replaceMode);
|
||||
|
||||
auto cbStdOut = [this](const QString &data, libssh::Client &client) -> ErrorCode {
|
||||
Q_UNUSED(client);
|
||||
@@ -251,13 +289,28 @@ void ServersBackupController::restoreBackup(const ServerCredentials &credentials
|
||||
|
||||
ErrorCode error = m_serverController->runHostScript(credentials, script, cbStdOut, cbStdErr);
|
||||
|
||||
if (error == ErrorCode::NoError) {
|
||||
setStatus(Success);
|
||||
setProgress(100, tr("Backup restored successfully"));
|
||||
emit backupRestored();
|
||||
// Проверяем вывод на наличие ошибок, даже если скрипт завершился с кодом 0
|
||||
bool hasError = m_currentOutput.contains("[ERROR]") ||
|
||||
m_currentOutput.contains("Failed to extract backup") ||
|
||||
m_currentError.contains("[ERROR]") ||
|
||||
m_currentError.contains("Failed to extract backup");
|
||||
|
||||
if (error == ErrorCode::NoError && !hasError) {
|
||||
// Проверяем, что восстановление действительно завершилось успешно
|
||||
if (m_currentOutput.contains("Restore completed successfully")) {
|
||||
setStatus(Success);
|
||||
setProgress(100, tr("Backup restored successfully"));
|
||||
emit backupRestored();
|
||||
} else {
|
||||
setStatus(Failed);
|
||||
emit errorOccurred(tr("Backup restore did not complete successfully. Output: %1").arg(m_currentOutput), ErrorCode::InternalError);
|
||||
}
|
||||
} else {
|
||||
setStatus(Failed);
|
||||
emit errorOccurred(tr("Failed to restore backup: %1").arg(m_currentError), error);
|
||||
QString errorMessage = hasError ?
|
||||
tr("Failed to restore backup: %1").arg(m_currentOutput + "\n" + m_currentError) :
|
||||
tr("Failed to restore backup: %1").arg(m_currentError);
|
||||
emit errorOccurred(errorMessage, error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,14 +436,98 @@ void ServersBackupController::downloadBackup(const ServerCredentials &credential
|
||||
}
|
||||
|
||||
void ServersBackupController::uploadBackup(const ServerCredentials &credentials,
|
||||
const QString &localPath)
|
||||
const QString &localPath,
|
||||
bool replaceMode)
|
||||
{
|
||||
if (m_status == InProgress) {
|
||||
emit errorOccurred("Another operation is in progress", ErrorCode::AmneziaServiceConnectionFailed);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if local file exists
|
||||
// Сохраняем режим восстановления для последующего использования
|
||||
m_restoreReplaceMode = replaceMode;
|
||||
|
||||
QString actualLocalPath = localPath;
|
||||
QString filename;
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
// Для Android URI нужно получить имя файла и использовать файловый дескриптор
|
||||
if (localPath.startsWith("content://")) {
|
||||
// Получаем имя файла из URI
|
||||
filename = AndroidController::instance()->getFileName(localPath);
|
||||
if (filename.isEmpty()) {
|
||||
// Fallback: извлекаем имя из URI
|
||||
QStringList parts = localPath.split('/');
|
||||
if (!parts.isEmpty()) {
|
||||
filename = parts.last();
|
||||
// Декодируем URL-кодированные символы
|
||||
if (filename.contains('%')) {
|
||||
filename = QUrl::fromPercentEncoding(filename.toUtf8());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Для Android URI используем файловый дескриптор через SystemController::readFile
|
||||
// Но scpFileCopy требует путь к файлу, поэтому нужно скопировать файл во временную директорию
|
||||
QByteArray fileData;
|
||||
qDebug() << "Reading Android URI:" << localPath;
|
||||
if (!SystemController::readFile(localPath, fileData)) {
|
||||
qDebug() << "Failed to read from Android URI";
|
||||
emit errorOccurred(tr("Failed to read backup file from Android storage"), ErrorCode::ReadError);
|
||||
return;
|
||||
}
|
||||
qDebug() << "Read" << fileData.size() << "bytes from Android URI";
|
||||
|
||||
// Удаляем предыдущий временный файл, если он существует
|
||||
if (m_tempUploadFile) {
|
||||
delete m_tempUploadFile;
|
||||
m_tempUploadFile = nullptr;
|
||||
}
|
||||
|
||||
// Создаем временный файл (сохраняем в член класса, чтобы не удалялся)
|
||||
// Используем setAutoRemove(false) чтобы файл не удалялся автоматически
|
||||
m_tempUploadFile = new QTemporaryFile(this);
|
||||
m_tempUploadFile->setAutoRemove(false);
|
||||
if (!m_tempUploadFile->open()) {
|
||||
qDebug() << "Failed to create temporary file";
|
||||
emit errorOccurred(tr("Failed to create temporary file"), ErrorCode::OpenError);
|
||||
delete m_tempUploadFile;
|
||||
m_tempUploadFile = nullptr;
|
||||
return;
|
||||
}
|
||||
|
||||
qint64 written = m_tempUploadFile->write(fileData);
|
||||
m_tempUploadFile->flush();
|
||||
// НЕ закрываем файл - он должен оставаться открытым для SCP
|
||||
// m_tempUploadFile->close();
|
||||
actualLocalPath = m_tempUploadFile->fileName();
|
||||
|
||||
qDebug() << "Created temp file:" << actualLocalPath << "written:" << written << "bytes, size:" << QFileInfo(actualLocalPath).size();
|
||||
|
||||
// Проверяем, что файл существует и доступен для чтения
|
||||
QFileInfo tempFileInfo(actualLocalPath);
|
||||
if (!tempFileInfo.exists()) {
|
||||
qDebug() << "Temp file does not exist after creation!";
|
||||
emit errorOccurred(tr("Failed to create temporary file"), ErrorCode::OpenError);
|
||||
delete m_tempUploadFile;
|
||||
m_tempUploadFile = nullptr;
|
||||
return;
|
||||
}
|
||||
|
||||
// Если имя файла пустое, используем имя из временного файла
|
||||
if (filename.isEmpty()) {
|
||||
filename = QFileInfo(actualLocalPath).fileName();
|
||||
}
|
||||
} else {
|
||||
QFileInfo localFileInfo(localPath);
|
||||
if (!localFileInfo.exists()) {
|
||||
emit errorOccurred(tr("Local file does not exist: %1").arg(localPath), ErrorCode::InternalError);
|
||||
return;
|
||||
}
|
||||
filename = localFileInfo.fileName();
|
||||
}
|
||||
#else
|
||||
// Для других платформ используем обычную проверку
|
||||
QFileInfo localFileInfo(localPath);
|
||||
if (!localFileInfo.exists()) {
|
||||
emit errorOccurred(tr("Local file does not exist: %1").arg(localPath), ErrorCode::InternalError);
|
||||
@@ -401,29 +538,69 @@ void ServersBackupController::uploadBackup(const ServerCredentials &credentials,
|
||||
emit errorOccurred(tr("Path is not a file: %1").arg(localPath), ErrorCode::InternalError);
|
||||
return;
|
||||
}
|
||||
|
||||
filename = localFileInfo.fileName();
|
||||
#endif
|
||||
|
||||
setStatus(InProgress);
|
||||
setProgress(0, tr("Uploading backup..."));
|
||||
|
||||
// Construct remote file path with filename from local path
|
||||
QString filename = localFileInfo.fileName();
|
||||
// Construct remote file path with filename
|
||||
QString remotePath = QString("%1/%2").arg(m_backupDir, filename);
|
||||
|
||||
setProgress(25, tr("Starting file transfer..."));
|
||||
|
||||
ErrorCode error = m_serverController->uploadFileToHostPublic(credentials, localPath, remotePath,
|
||||
ErrorCode error = m_serverController->uploadFileToHostPublic(credentials, actualLocalPath, remotePath,
|
||||
libssh::ScpOverwriteMode::ScpOverwriteExisting);
|
||||
|
||||
qDebug() << "Upload result, error code:" << static_cast<int>(error);
|
||||
|
||||
if (error == ErrorCode::NoError) {
|
||||
setStatus(Success);
|
||||
setProgress(100, tr("Backup uploaded successfully"));
|
||||
emit backupUploaded(remotePath);
|
||||
|
||||
// Удаляем временный файл после успешной загрузки
|
||||
if (m_tempUploadFile) {
|
||||
qDebug() << "Removing temp file:" << m_tempUploadFile->fileName();
|
||||
m_tempUploadFile->remove();
|
||||
delete m_tempUploadFile;
|
||||
m_tempUploadFile = nullptr;
|
||||
}
|
||||
} else {
|
||||
setStatus(Failed);
|
||||
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);
|
||||
|
||||
// Удаляем временный файл при ошибке
|
||||
if (m_tempUploadFile) {
|
||||
m_tempUploadFile->remove();
|
||||
delete m_tempUploadFile;
|
||||
m_tempUploadFile = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Перегруженный метод для setup wizard с отдельными параметрами credentials
|
||||
void ServersBackupController::uploadBackupWithStrings(const QString &hostname,
|
||||
const QString &username,
|
||||
const QString &secretData,
|
||||
const QString &localPath,
|
||||
bool replaceMode)
|
||||
{
|
||||
// Создаем ServerCredentials из строк
|
||||
ServerCredentials credentials;
|
||||
credentials.hostName = hostname;
|
||||
credentials.userName = username;
|
||||
credentials.secretData = secretData;
|
||||
credentials.port = 22; // Default SSH port
|
||||
|
||||
qDebug() << "uploadBackupWithStrings called with hostname:" << hostname << "username:" << username;
|
||||
|
||||
// Вызываем основной метод
|
||||
uploadBackup(credentials, localPath, replaceMode);
|
||||
}
|
||||
|
||||
void ServersBackupController::deleteBackup(const ServerCredentials &credentials,
|
||||
const QString &backupFilename)
|
||||
{
|
||||
@@ -435,7 +612,10 @@ void ServersBackupController::deleteBackup(const ServerCredentials &credentials,
|
||||
setStatus(InProgress);
|
||||
setProgress(0, tr("Deleting backup..."));
|
||||
|
||||
QString script = QString("sudo rm -f %1/%2").arg(m_backupDir).arg(backupFilename);
|
||||
// Экранируем имя файла для безопасного использования в bash
|
||||
QString escapedFilename = backupFilename;
|
||||
escapedFilename.replace("'", "'\\''"); // Экранируем одинарные кавычки
|
||||
QString script = QString("sudo rm -f '%1/%2'").arg(m_backupDir).arg(escapedFilename);
|
||||
|
||||
m_currentOutput.clear();
|
||||
m_currentError.clear();
|
||||
@@ -465,16 +645,20 @@ void ServersBackupController::deleteBackup(const ServerCredentials &credentials,
|
||||
// ВСТРОЕННЫЕ BASH СКРИПТЫ
|
||||
// ============================================================================
|
||||
|
||||
QString ServersBackupController::getBackupScript() const
|
||||
QString ServersBackupController::getBackupScript(const QString &ipAddress) const
|
||||
{
|
||||
// Упрощенная версия bash скрипта, встроенная в C++
|
||||
// Формат имени файла: IP_ADDRESS - DD-MM-YYYY_HH-MM-SS.tgz
|
||||
return QString(R"(
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
BACKUP_DIR=%1
|
||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||
BACKUP_SUBDIR="$BACKUP_DIR/backup_$TIMESTAMP"
|
||||
IP_ADDRESS=%2
|
||||
DATE=$(date +%d-%m-%Y)
|
||||
TIME=$(date +%H-%M-%S)
|
||||
BACKUP_FILENAME="$IP_ADDRESS - ${DATE}_${TIME}.tgz"
|
||||
BACKUP_SUBDIR="$BACKUP_DIR/backup_temp_$$"
|
||||
|
||||
echo "[INFO] Starting backup..."
|
||||
|
||||
@@ -509,26 +693,30 @@ done
|
||||
|
||||
# Создание архива
|
||||
cd "$BACKUP_DIR"
|
||||
tar -czf "backup_$TIMESTAMP.tar.gz" "backup_$TIMESTAMP" 2>/dev/null
|
||||
tar -czf "$BACKUP_FILENAME" -C "$BACKUP_SUBDIR" . 2>/dev/null
|
||||
rm -rf "$BACKUP_SUBDIR"
|
||||
|
||||
echo "[INFO] Backup created: backup_$TIMESTAMP.tar.gz"
|
||||
)").arg(m_backupDir);
|
||||
echo "[INFO] Backup created: $BACKUP_FILENAME"
|
||||
)").arg(m_backupDir, ipAddress);
|
||||
}
|
||||
|
||||
QString ServersBackupController::getContainerBackupScript(DockerContainer container) const
|
||||
QString ServersBackupController::getContainerBackupScript(DockerContainer container, const QString &ipAddress) const
|
||||
{
|
||||
QString containerName = ContainerProps::containerToString(container);
|
||||
|
||||
// Backup конкретного контейнера напрямую через docker cp
|
||||
// Формат имени файла: IP_ADDRESS - DD-MM-YYYY_HH-MM-SS.tgz
|
||||
return QString(R"(
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
BACKUP_DIR=%1
|
||||
CONTAINER_NAME=%2
|
||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||
BACKUP_SUBDIR="$BACKUP_DIR/backup_$TIMESTAMP"
|
||||
IP_ADDRESS=%3
|
||||
DATE=$(date +%d-%m-%Y)
|
||||
TIME=$(date +%H-%M-%S)
|
||||
BACKUP_FILENAME="$IP_ADDRESS - ${DATE}_${TIME}.tgz"
|
||||
BACKUP_SUBDIR="$BACKUP_DIR/backup_temp_$$"
|
||||
|
||||
echo "[INFO] Starting backup for container: $CONTAINER_NAME..."
|
||||
|
||||
@@ -562,14 +750,14 @@ sudo docker network inspect $(sudo docker inspect -f '{{range $k, $v := .Network
|
||||
|
||||
# Создание архива
|
||||
cd "$BACKUP_DIR"
|
||||
tar -czf "backup_$TIMESTAMP.tar.gz" "backup_$TIMESTAMP" 2>/dev/null
|
||||
tar -czf "$BACKUP_FILENAME" -C "$BACKUP_SUBDIR" . 2>/dev/null
|
||||
rm -rf "$BACKUP_SUBDIR"
|
||||
|
||||
echo "[INFO] Backup created: backup_$TIMESTAMP.tar.gz"
|
||||
)").arg(m_backupDir).arg(containerName);
|
||||
echo "[INFO] Backup created: $BACKUP_FILENAME"
|
||||
)").arg(m_backupDir, containerName, ipAddress);
|
||||
}
|
||||
|
||||
QString ServersBackupController::getContainersBackupScript(const QList<DockerContainer> &containers) const
|
||||
QString ServersBackupController::getContainersBackupScript(const QList<DockerContainer> &containers, const QString &ipAddress) const
|
||||
{
|
||||
QString containersList;
|
||||
for (const DockerContainer &container : containers) {
|
||||
@@ -577,13 +765,17 @@ QString ServersBackupController::getContainersBackupScript(const QList<DockerCon
|
||||
containersList += QString("\"%1\" ").arg(containerName);
|
||||
}
|
||||
|
||||
// Формат имени файла: IP_ADDRESS - DD-MM-YYYY_HH-MM-SS.tgz
|
||||
return QString(R"(
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
BACKUP_DIR=%1
|
||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||
BACKUP_SUBDIR="$BACKUP_DIR/backup_$TIMESTAMP"
|
||||
IP_ADDRESS=%2
|
||||
DATE=$(date +%d-%m-%Y)
|
||||
TIME=$(date +%H-%M-%S)
|
||||
BACKUP_FILENAME="$IP_ADDRESS - ${DATE}_${TIME}.tgz"
|
||||
BACKUP_SUBDIR="$BACKUP_DIR/backup_temp_$$"
|
||||
|
||||
echo "[INFO] Starting backup for containers..."
|
||||
|
||||
@@ -591,7 +783,7 @@ echo "[INFO] Starting backup for containers..."
|
||||
mkdir -p "$BACKUP_SUBDIR"
|
||||
|
||||
# Список контейнеров для backup
|
||||
CONTAINERS=(%2)
|
||||
CONTAINERS=(%3)
|
||||
|
||||
# Backup каждого контейнера
|
||||
for container in "${CONTAINERS[@]}"; do
|
||||
@@ -617,37 +809,70 @@ done
|
||||
|
||||
# Создание архива
|
||||
cd "$BACKUP_DIR"
|
||||
tar -czf "backup_$TIMESTAMP.tar.gz" "backup_$TIMESTAMP" 2>/dev/null
|
||||
tar -czf "$BACKUP_FILENAME" -C "$BACKUP_SUBDIR" . 2>/dev/null
|
||||
rm -rf "$BACKUP_SUBDIR"
|
||||
|
||||
echo "[INFO] Backup created: backup_$TIMESTAMP.tar.gz"
|
||||
)").arg(m_backupDir).arg(containersList.trimmed());
|
||||
echo "[INFO] Backup created: $BACKUP_FILENAME"
|
||||
)").arg(m_backupDir, ipAddress, containersList.trimmed());
|
||||
}
|
||||
|
||||
QString ServersBackupController::getRestoreScript(const QString &backupFilename,
|
||||
const QStringList &containers) const
|
||||
const QStringList &containers,
|
||||
bool replaceMode) const
|
||||
{
|
||||
Q_UNUSED(containers); // TODO: Использовать для выборочного восстановления
|
||||
|
||||
// Экранируем имя файла для безопасного использования в bash
|
||||
QString escapedFilename = backupFilename;
|
||||
escapedFilename.replace("'", "'\\''"); // Экранируем одинарные кавычки
|
||||
|
||||
return QString(R"(
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
BACKUP_DIR=%1
|
||||
BACKUP_FILE=%2
|
||||
BACKUP_FILE='%2'
|
||||
REPLACE_MODE=%3
|
||||
TEMP_DIR="/tmp/amnezia_restore_$$"
|
||||
|
||||
echo "[INFO] Starting restore from $BACKUP_FILE..."
|
||||
if [ "$REPLACE_MODE" = "1" ]; then
|
||||
echo "[INFO] Using replace mode: containers will be cleared before restore"
|
||||
else
|
||||
echo "[INFO] Using add mode: data will be added to existing containers"
|
||||
fi
|
||||
|
||||
# Проверка существования файла backup
|
||||
if [ ! -f "$BACKUP_DIR/$BACKUP_FILE" ]; then
|
||||
echo "[ERROR] Backup file not found: $BACKUP_DIR/$BACKUP_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Извлечение backup
|
||||
mkdir -p "$TEMP_DIR"
|
||||
tar -xzf "$BACKUP_DIR/$BACKUP_FILE" -C "$TEMP_DIR" 2>/dev/null
|
||||
EXTRACT_OUTPUT=$(tar -xzf "$BACKUP_DIR/$BACKUP_FILE" -C "$TEMP_DIR" 2>&1)
|
||||
EXTRACT_EXIT_CODE=$?
|
||||
if [ $EXTRACT_EXIT_CODE -ne 0 ]; then
|
||||
echo "[ERROR] Failed to extract backup archive: $EXTRACT_OUTPUT"
|
||||
rm -rf "$TEMP_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Ищем директорию с контейнерами (может быть backup_temp_* или просто контейнеры напрямую)
|
||||
BACKUP_SUBDIR=$(ls -d "$TEMP_DIR"/backup_* 2>/dev/null | head -1)
|
||||
|
||||
# Если не нашли backup_*, проверяем, есть ли директории контейнеров напрямую
|
||||
if [ -z "$BACKUP_SUBDIR" ]; then
|
||||
echo "[ERROR] Failed to extract backup"
|
||||
exit 1
|
||||
# Проверяем, есть ли директории контейнеров (amnezia-*) напрямую в TEMP_DIR
|
||||
if ls -d "$TEMP_DIR"/amnezia-* 2>/dev/null | head -1 > /dev/null; then
|
||||
BACKUP_SUBDIR="$TEMP_DIR"
|
||||
else
|
||||
echo "[ERROR] Failed to extract backup: backup directory not found in archive"
|
||||
echo "[DEBUG] Contents of $TEMP_DIR:"
|
||||
ls -la "$TEMP_DIR" || true
|
||||
rm -rf "$TEMP_DIR"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Восстановление каждого контейнера
|
||||
@@ -664,6 +889,18 @@ for container_dir in "$BACKUP_SUBDIR"/*; do
|
||||
# Остановка контейнера
|
||||
sudo docker stop "$container_name" 2>/dev/null || true
|
||||
|
||||
# Режим замены: очистка контейнера перед восстановлением
|
||||
if [ "$REPLACE_MODE" = "1" ]; then
|
||||
echo "[INFO] Clearing container $container_name before restore..."
|
||||
# Создаем пустую директорию для очистки /opt/amnezia
|
||||
TEMP_CLEAR_DIR="/tmp/clear_amnezia_$$"
|
||||
mkdir -p "$TEMP_CLEAR_DIR/amnezia"
|
||||
# Копируем пустую директорию, что удалит старое содержимое
|
||||
sudo docker cp "$TEMP_CLEAR_DIR/amnezia" "$container_name:/opt/" 2>/dev/null || true
|
||||
# Удаляем временную директорию
|
||||
rm -rf "$TEMP_CLEAR_DIR"
|
||||
fi
|
||||
|
||||
# Восстановление /opt/amnezia
|
||||
if [ -d "$container_dir/amnezia" ]; then
|
||||
sudo docker cp "$container_dir/amnezia" "$container_name:/opt/" 2>/dev/null || true
|
||||
@@ -680,7 +917,7 @@ done
|
||||
rm -rf "$TEMP_DIR"
|
||||
|
||||
echo "[INFO] Restore completed successfully"
|
||||
)").arg(m_backupDir).arg(backupFilename);
|
||||
)").arg(m_backupDir).arg(escapedFilename).arg(replaceMode ? "1" : "0");
|
||||
}
|
||||
|
||||
QString ServersBackupController::getCheckStatusScript() const
|
||||
@@ -792,6 +1029,12 @@ ErrorCode ServersBackupController::handleStdOut(const QString &data, QString &ou
|
||||
output += data;
|
||||
qDebug().noquote() << "[BACKUP]" << data;
|
||||
|
||||
// Проверяем на ошибки в выводе
|
||||
if (data.contains("[ERROR]") || data.contains("ERROR")) {
|
||||
// Ошибка обнаружена в stdout, но это не критично для handleStdOut
|
||||
// Основная проверка будет в restoreBackup после выполнения скрипта
|
||||
}
|
||||
|
||||
// Обновляем прогресс на основе вывода
|
||||
if (data.contains("Starting backup")) {
|
||||
setProgress(10, tr("Starting backup..."));
|
||||
@@ -799,6 +1042,12 @@ ErrorCode ServersBackupController::handleStdOut(const QString &data, QString &ou
|
||||
setProgress(50, tr("Backing up containers..."));
|
||||
} else if (data.contains("Backup created")) {
|
||||
setProgress(90, tr("Finalizing..."));
|
||||
} else if (data.contains("Starting restore")) {
|
||||
setProgress(10, tr("Starting restore..."));
|
||||
} else if (data.contains("Restoring")) {
|
||||
setProgress(50, tr("Restoring containers..."));
|
||||
} else if (data.contains("Restore completed successfully")) {
|
||||
setProgress(90, tr("Finalizing restore..."));
|
||||
}
|
||||
|
||||
return ErrorCode::NoError;
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
#include <QJsonArray>
|
||||
#include <QFileInfo>
|
||||
|
||||
class QTemporaryFile;
|
||||
|
||||
#include "core/controllers/serverController.h"
|
||||
#include "core/defs.h"
|
||||
#include "containers/containers_defs.h"
|
||||
@@ -90,10 +92,12 @@ public slots:
|
||||
* @param credentials Учетные данные сервера
|
||||
* @param backupFilename Имя файла backup
|
||||
* @param containers Список контейнеров (пустой = все)
|
||||
* @param replaceMode Если true - сначала очищает контейнер, затем восстанавливает. Если false - добавляет данные поверх существующих
|
||||
*/
|
||||
void restoreBackup(const ServerCredentials &credentials,
|
||||
const QString &backupFilename,
|
||||
const QStringList &containers = QStringList());
|
||||
const QStringList &containers = QStringList(),
|
||||
bool replaceMode = false);
|
||||
|
||||
/**
|
||||
* @brief Проверить состояние backup на сервере
|
||||
@@ -115,9 +119,18 @@ public slots:
|
||||
* @brief Загрузить backup на сервер
|
||||
* @param credentials Учетные данные сервера
|
||||
* @param localPath Путь к локальному файлу
|
||||
* @param replaceMode Режим восстановления (true = замена, false = добавление). Сохраняется для последующего использования в restoreBackup
|
||||
*/
|
||||
void uploadBackup(const ServerCredentials &credentials,
|
||||
const QString &localPath);
|
||||
const QString &localPath,
|
||||
bool replaceMode = false);
|
||||
|
||||
// Перегруженный метод для setup wizard с отдельными параметрами credentials
|
||||
Q_INVOKABLE void uploadBackupWithStrings(const QString &hostname,
|
||||
const QString &username,
|
||||
const QString &secretData,
|
||||
const QString &localPath,
|
||||
bool replaceMode = false);
|
||||
|
||||
/**
|
||||
* @brief Удалить backup с сервера
|
||||
@@ -186,25 +199,31 @@ signals:
|
||||
private:
|
||||
/**
|
||||
* @brief Получить bash скрипт для создания backup всех контейнеров
|
||||
* @param ipAddress IP адрес сервера в формате с подчеркиваниями (например "192_119_110_11")
|
||||
*/
|
||||
QString getBackupScript() const;
|
||||
QString getBackupScript(const QString &ipAddress) const;
|
||||
|
||||
/**
|
||||
* @brief Получить bash скрипт для создания backup конкретного контейнера
|
||||
* @param container Тип контейнера
|
||||
* @param ipAddress IP адрес сервера в формате с подчеркиваниями (например "192_119_110_11")
|
||||
*/
|
||||
QString getContainerBackupScript(DockerContainer container) const;
|
||||
QString getContainerBackupScript(DockerContainer container, const QString &ipAddress) const;
|
||||
|
||||
/**
|
||||
* @brief Получить bash скрипт для создания backup нескольких контейнеров
|
||||
* @param containers Список контейнеров
|
||||
* @param ipAddress IP адрес сервера в формате с подчеркиваниями (например "192_119_110_11")
|
||||
*/
|
||||
QString getContainersBackupScript(const QList<DockerContainer> &containers) const;
|
||||
QString getContainersBackupScript(const QList<DockerContainer> &containers, const QString &ipAddress) const;
|
||||
|
||||
/**
|
||||
* @brief Получить bash скрипт для восстановления
|
||||
* @param backupFilename Имя файла backup
|
||||
* @param containers Список контейнеров
|
||||
* @param replaceMode Если true - сначала очищает контейнер, затем восстанавливает
|
||||
*/
|
||||
QString getRestoreScript(const QString &backupFilename, const QStringList &containers) const;
|
||||
QString getRestoreScript(const QString &backupFilename, const QStringList &containers, bool replaceMode = false) const;
|
||||
|
||||
/**
|
||||
* @brief Получить bash скрипт для проверки состояния
|
||||
@@ -253,6 +272,8 @@ private:
|
||||
QString m_backupDir;
|
||||
QString m_currentOutput;
|
||||
QString m_currentError;
|
||||
bool m_restoreReplaceMode; // Сохраняем режим восстановления для использования после uploadBackup
|
||||
QTemporaryFile *m_tempUploadFile; // Временный файл для Android URI (чтобы не удалялся до завершения загрузки)
|
||||
};
|
||||
|
||||
#endif // SERVERSBACKUPCONTROLLER_H
|
||||
|
||||
@@ -168,6 +168,42 @@ void SystemController::setQmlRoot(QObject *qmlRoot)
|
||||
m_qmlRoot = qmlRoot;
|
||||
}
|
||||
|
||||
QString SystemController::getFileNameFromPath(const QString &filePath)
|
||||
{
|
||||
if (filePath.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
// Для Android URI используем специальный метод для получения имени файла
|
||||
if (filePath.startsWith("content://")) {
|
||||
QString fileName = AndroidController::instance()->getFileName(filePath);
|
||||
if (!fileName.isEmpty()) {
|
||||
return fileName;
|
||||
}
|
||||
// Если не удалось получить имя через ContentResolver, пытаемся извлечь из URI
|
||||
}
|
||||
#endif
|
||||
|
||||
// Для обычных путей или если Android метод не сработал
|
||||
QFileInfo fileInfo(filePath);
|
||||
QString fileName = fileInfo.fileName();
|
||||
|
||||
// Если имя файла пустое, пытаемся извлечь из пути
|
||||
if (fileName.isEmpty()) {
|
||||
QStringList parts = filePath.split('/');
|
||||
if (!parts.isEmpty()) {
|
||||
fileName = parts.last();
|
||||
// Декодируем URL-кодированные символы
|
||||
if (fileName.contains('%')) {
|
||||
fileName = QUrl::fromPercentEncoding(fileName.toUtf8());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fileName;
|
||||
}
|
||||
|
||||
bool SystemController::isAuthenticated()
|
||||
{
|
||||
#ifdef Q_OS_ANDROID
|
||||
|
||||
@@ -18,6 +18,13 @@ public:
|
||||
public slots:
|
||||
QString getFileName(const QString &acceptLabel, const QString &nameFilter, const QString &selectedFile = "",
|
||||
const bool isSaveMode = false, const QString &defaultSuffix = "");
|
||||
|
||||
/**
|
||||
* @brief Получить имя файла из пути или URI (для Android)
|
||||
* @param filePath Путь к файлу или URI
|
||||
* @return Имя файла
|
||||
*/
|
||||
Q_INVOKABLE QString getFileNameFromPath(const QString &filePath);
|
||||
|
||||
void setQmlRoot(QObject *qmlRoot);
|
||||
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import PageEnum 1.0
|
||||
import ProtocolEnum 1.0
|
||||
import Style 1.0
|
||||
|
||||
import "../Controls2"
|
||||
import "../Controls2/TextTypes"
|
||||
import "../Components"
|
||||
import "../Config"
|
||||
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
|
||||
|
||||
onActiveFocusChanged: {
|
||||
if(backButton.enabled && backButton.activeFocus) {
|
||||
flickable.contentY = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FlickableType {
|
||||
id: flickable
|
||||
|
||||
anchors.top: backButton.bottom
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
contentHeight: contentColumn.implicitHeight
|
||||
|
||||
ColumnLayout {
|
||||
id: contentColumn
|
||||
width: flickable.width
|
||||
|
||||
spacing: 16
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
|
||||
headerText: qsTr("Backup")
|
||||
}
|
||||
|
||||
ParagraphTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
text: qsTr("Local copy of VPN protocols, services, all server settings and users.")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: -8
|
||||
|
||||
text: qsTr("More about backups")
|
||||
color: AmneziaStyle.color.goldenApricot
|
||||
font.pixelSize: 14
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
// TODO: Open help page or show more info
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 16
|
||||
spacing: 12
|
||||
|
||||
BasicButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Create backup")
|
||||
|
||||
clickedFunc: function() {
|
||||
createBackup(true)
|
||||
}
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Restore from backup")
|
||||
defaultColor: AmneziaStyle.color.transparent
|
||||
hoveredColor: Qt.rgba(1, 1, 1, 0.08)
|
||||
pressedColor: Qt.rgba(1, 1, 1, 0.12)
|
||||
disabledColor: AmneziaStyle.color.mutedGray
|
||||
textColor: AmneziaStyle.color.goldenApricot
|
||||
borderWidth: 1
|
||||
|
||||
clickedFunc: function() {
|
||||
restoreBackup()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Backup Functions ============
|
||||
|
||||
function getServerCredentials() {
|
||||
var index = ServersModel.processedIndex
|
||||
return ServersModel.getServerCredentials(index)
|
||||
}
|
||||
|
||||
property bool downloadAfterCreate: false
|
||||
|
||||
function createBackup(shouldDownload) {
|
||||
// По умолчанию shouldDownload = true, если не указано
|
||||
downloadAfterCreate = (shouldDownload !== undefined) ? shouldDownload : true
|
||||
|
||||
var headerText = downloadAfterCreate ?
|
||||
qsTr("Create backup and download to device?") :
|
||||
qsTr("Create server configuration backup?")
|
||||
var descriptionText = downloadAfterCreate ?
|
||||
qsTr("Backup will be created on server and automatically downloaded to your device") :
|
||||
qsTr("This will create a backup of your server containers configuration on the server")
|
||||
var yesButtonText = qsTr("Create")
|
||||
var noButtonText = qsTr("Cancel")
|
||||
|
||||
var yesButtonFunction = function() {
|
||||
PageController.showBusyIndicator(true)
|
||||
|
||||
var credentials = getServerCredentials()
|
||||
// Всегда создаем backup всех контейнеров
|
||||
ServersBackupController.createBackup(credentials)
|
||||
}
|
||||
var noButtonFunction = function() {}
|
||||
|
||||
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
|
||||
}
|
||||
|
||||
property string selectedBackupForRestore: ""
|
||||
|
||||
function restoreBackup() {
|
||||
// Для мобильных устройств используем все возможные расширения backup файлов
|
||||
// Android преобразует расширения в MIME типы автоматически
|
||||
var filter = GC.isMobile() ? "*.gz *.tgz *.tar.gz" : "Backup files (*.tar.gz *.backup *.tgz *.gz)"
|
||||
var localPath = SystemController.getFileName(
|
||||
qsTr("Select Backup to Restore"),
|
||||
filter,
|
||||
"",
|
||||
false,
|
||||
""
|
||||
)
|
||||
|
||||
if (!localPath || localPath.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
selectedBackupForRestore = localPath
|
||||
|
||||
// Открываем страницу выбора режима восстановления
|
||||
var parentItem = root.parent
|
||||
while (parentItem && parentItem.objectName !== "tabBarStackView") {
|
||||
parentItem = parentItem.parent
|
||||
}
|
||||
if (parentItem && typeof parentItem.push === "function") {
|
||||
// Используем SystemController для получения имени файла из пути или URI
|
||||
// Это правильно обработает Android URI через ContentResolver
|
||||
var fileName = SystemController.getFileNameFromPath(localPath)
|
||||
|
||||
// Если имя файла пустое или undefined, используем fallback
|
||||
if (!fileName || fileName === undefined || fileName.length === 0) {
|
||||
var fallbackName = localPath.split('/').pop()
|
||||
fileName = (fallbackName && fallbackName.length > 0) ? fallbackName : qsTr("backup.tgz")
|
||||
}
|
||||
|
||||
// Убеждаемся, что fileName - это строка
|
||||
fileName = String(fileName)
|
||||
|
||||
// Извлекаем IP адрес из имени файла (формат: IP_ADDRESS - DD-MM-YYYY_HH-MM-SS.tgz)
|
||||
var serverIp = ""
|
||||
var ipMatch = fileName.match(/^([\d_]+)\s*-/)
|
||||
if (ipMatch && ipMatch.length > 1) {
|
||||
// Заменяем подчеркивания на точки для отображения IP адреса
|
||||
serverIp = ipMatch[1].replace(/_/g, ".")
|
||||
}
|
||||
|
||||
// Если не удалось извлечь IP из имени файла, используем IP из credentials
|
||||
if (!serverIp || serverIp.length === 0) {
|
||||
var credentials = getServerCredentials()
|
||||
serverIp = credentials.hostName || ""
|
||||
}
|
||||
|
||||
var serverName = ServersModel.getProcessedServerData("name") || qsTr("Server")
|
||||
|
||||
parentItem.push(PageController.getPagePath(PageEnum.PageSettingsServerRestoreMode), {
|
||||
"backupFilePath": localPath,
|
||||
"backupFileName": fileName,
|
||||
"serverName": serverName,
|
||||
"serverIp": serverIp
|
||||
})
|
||||
} else {
|
||||
console.warn("Could not find StackView to navigate to restore mode page")
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Backup Controller Connections ============
|
||||
|
||||
property string lastCreatedBackupFilename: ""
|
||||
property string lastUploadedBackupFilename: ""
|
||||
|
||||
Connections {
|
||||
target: ServersBackupController
|
||||
|
||||
function onBackupCreated(backupFilename) {
|
||||
lastCreatedBackupFilename = backupFilename
|
||||
|
||||
if (downloadAfterCreate) {
|
||||
var credentials = getServerCredentials()
|
||||
var localPath = backupFilename
|
||||
PageController.showNotificationMessage(qsTr("Backup created. Downloading to device..."))
|
||||
ServersBackupController.downloadBackup(credentials, backupFilename, localPath)
|
||||
downloadAfterCreate = false
|
||||
} else {
|
||||
PageController.showBusyIndicator(false)
|
||||
PageController.showNotificationMessage(qsTr("Backup created successfully: %1").arg(backupFilename))
|
||||
}
|
||||
}
|
||||
|
||||
function onBackupDownloaded(localPath) {
|
||||
PageController.showBusyIndicator(false)
|
||||
console.log("Backup downloaded to:", localPath)
|
||||
|
||||
if (lastCreatedBackupFilename && lastCreatedBackupFilename.length > 0) {
|
||||
var credentials = getServerCredentials()
|
||||
ServersBackupController.deleteBackup(credentials, lastCreatedBackupFilename)
|
||||
console.log("Deleting backup from server:", lastCreatedBackupFilename)
|
||||
}
|
||||
|
||||
PageController.showNotificationMessage(qsTr("Backup downloaded successfully!\n\nSaved to:\n%1").arg(localPath))
|
||||
}
|
||||
|
||||
function onBackupUploaded(serverPath) {
|
||||
// Этот обработчик больше не используется здесь, так как восстановление
|
||||
// теперь происходит через PageSettingsServerRestoreMode
|
||||
// Оставляем для совместимости, но не выполняем действий
|
||||
}
|
||||
|
||||
function onBackupRestored() {
|
||||
PageController.showBusyIndicator(false)
|
||||
|
||||
selectedBackupForRestore = ""
|
||||
PageController.showNotificationMessage(qsTr("Backup restored successfully! Containers are restarting..."))
|
||||
}
|
||||
|
||||
function onProgressChanged(percent, message) {
|
||||
console.log("Backup progress:", percent, "%", message)
|
||||
}
|
||||
|
||||
function onErrorOccurred(errorMessage, errorCode) {
|
||||
PageController.showBusyIndicator(false)
|
||||
PageController.showErrorMessage(qsTr("Backup error: %1").arg(errorMessage))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import PageEnum 1.0
|
||||
import ProtocolEnum 1.0
|
||||
import Style 1.0
|
||||
|
||||
import "../Controls2"
|
||||
import "../Controls2/TextTypes"
|
||||
import "../Components"
|
||||
import "../Config"
|
||||
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
property string backupFileName: ""
|
||||
property string serverName: ""
|
||||
property string serverIp: ""
|
||||
property bool isFromSetupWizard: false
|
||||
|
||||
Component.onCompleted: {
|
||||
// Убеждаемся, что все свойства инициализированы
|
||||
if (!backupFileName) backupFileName = ""
|
||||
if (!serverName) serverName = ""
|
||||
if (!serverIp) serverIp = ""
|
||||
}
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
|
||||
|
||||
backButtonFunction: function() {
|
||||
// После успешного restore всегда идем на главную страницу
|
||||
PageController.goToPageHome()
|
||||
}
|
||||
|
||||
onActiveFocusChanged: {
|
||||
if(backButton.enabled && backButton.activeFocus) {
|
||||
flickable.contentY = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FlickableType {
|
||||
id: flickable
|
||||
|
||||
anchors.top: backButton.bottom
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
contentHeight: contentColumn.implicitHeight
|
||||
|
||||
ColumnLayout {
|
||||
id: contentColumn
|
||||
width: flickable.width
|
||||
|
||||
spacing: 16
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
|
||||
headerText: qsTr("Backup restored")
|
||||
}
|
||||
|
||||
ParagraphTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
text: {
|
||||
var baseText = qsTr("%1 on \"%2\"").arg(backupFileName).arg(serverName)
|
||||
if (serverIp && serverIp.length > 0) {
|
||||
return baseText + ", " + serverIp
|
||||
}
|
||||
return baseText
|
||||
}
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 24
|
||||
spacing: 12
|
||||
|
||||
BasicButtonType {
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: qsTr("To home")
|
||||
implicitHeight: 56
|
||||
|
||||
clickedFunc: function() {
|
||||
// Переход на главную страницу (PageHome)
|
||||
PageController.goToPageHome()
|
||||
}
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: qsTr("To server settings")
|
||||
implicitHeight: 56
|
||||
defaultColor: AmneziaStyle.color.transparent
|
||||
hoveredColor: AmneziaStyle.color.transparent
|
||||
pressedColor: AmneziaStyle.color.transparent
|
||||
borderWidth: 1
|
||||
borderColor: "#FFFFFF"
|
||||
textColor: "#FFFFFF"
|
||||
|
||||
clickedFunc: function() {
|
||||
// Открываем страницу настроек сервера с активной вкладкой "Управление"
|
||||
PageController.goToPage(PageEnum.PageSettingsServerInfo)
|
||||
// Устанавливаем активной вкладку "Управление" через сигнал
|
||||
PageController.goToPageSettingsServerManagement()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,148 +75,10 @@ PageType {
|
||||
delegate: ColumnLayout {
|
||||
width: listView.width
|
||||
|
||||
// Кастомная секция для backup
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
visible: isVisible && isBackupSection
|
||||
|
||||
spacing: 16
|
||||
|
||||
Header2Type {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
|
||||
headerText: qsTr("Backup & Restore")
|
||||
}
|
||||
|
||||
ParagraphTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
text: qsTr("Create and restore server configuration backups")
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
spacing: 12
|
||||
|
||||
LabelTextType {
|
||||
text: qsTr("Select containers:")
|
||||
}
|
||||
|
||||
DropDownType {
|
||||
id: containerSelector
|
||||
Layout.fillWidth: true
|
||||
|
||||
objectName: "containerSelector"
|
||||
descriptionText: qsTr("Choose which containers to backup")
|
||||
headerText: qsTr("Containers")
|
||||
|
||||
text: "All containers"
|
||||
|
||||
drawerHeight: 0.4375
|
||||
drawerParent: root
|
||||
|
||||
listView: ListViewWithRadioButtonType {
|
||||
id: containerSelectorListView
|
||||
rootWidth: root.width
|
||||
|
||||
model: ListModel {
|
||||
id: containersModel
|
||||
ListElement { name: "All containers"; value: "all" }
|
||||
ListElement { name: "OpenVPN"; value: "amnezia-openvpn" }
|
||||
ListElement { name: "WireGuard"; value: "amnezia-wireguard" }
|
||||
ListElement { name: "AmneziaWG (legacy)"; value: "amnezia-awg" }
|
||||
ListElement { name: "AmneziaWG 2"; value: "amnezia-awg2" }
|
||||
ListElement { name: "Xray"; value: "amnezia-xray" }
|
||||
ListElement { name: "IKEv2"; value: "amnezia-ipsec" }
|
||||
ListElement { name: "Cloak"; value: "amnezia-cloak" }
|
||||
ListElement { name: "ShadowSocks"; value: "amnezia-shadowsocks" }
|
||||
}
|
||||
|
||||
clickedFunction: function() {
|
||||
if (containerSelectorListView.selectedText) {
|
||||
containerSelector.text = containerSelectorListView.selectedText
|
||||
}
|
||||
// Сохранить выбранное значение
|
||||
var index = containerSelectorListView.selectedIndex
|
||||
if (index >= 0 && index < containersModel.count) {
|
||||
root.selectedContainerValue = containersModel.get(index).value
|
||||
}
|
||||
containerSelector.closeTriggered()
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
containerSelectorListView.selectedIndex = 0
|
||||
root.selectedContainerValue = "all"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 12
|
||||
|
||||
BasicButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Create Backup")
|
||||
|
||||
clickedFunc: function() {
|
||||
createBackup(false) // false = не скачивать автоматически
|
||||
}
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Backup & Download")
|
||||
|
||||
clickedFunc: function() {
|
||||
createBackup(true) // true = скачать после создания
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Restore Backup")
|
||||
defaultColor: AmneziaStyle.color.transparent
|
||||
hoveredColor: Qt.rgba(1, 1, 1, 0.08)
|
||||
pressedColor: Qt.rgba(1, 1, 1, 0.12)
|
||||
disabledColor: AmneziaStyle.color.mutedGray
|
||||
textColor: AmneziaStyle.color.goldenApricot
|
||||
|
||||
clickedFunc: function() {
|
||||
restoreBackup()
|
||||
}
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Manage Backups")
|
||||
defaultColor: AmneziaStyle.color.transparent
|
||||
hoveredColor: Qt.rgba(1, 1, 1, 0.08)
|
||||
pressedColor: Qt.rgba(1, 1, 1, 0.12)
|
||||
disabledColor: AmneziaStyle.color.mutedGray
|
||||
textColor: AmneziaStyle.color.goldenApricot
|
||||
|
||||
clickedFunc: function() {
|
||||
manageBackups()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Обычные кнопки для других действий
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
|
||||
visible: isVisible && !isBackupSection
|
||||
visible: isVisible
|
||||
|
||||
text: title
|
||||
descriptionText: description
|
||||
@@ -261,11 +123,14 @@ PageType {
|
||||
id: backupSection
|
||||
|
||||
property bool isVisible: root.isServerWithWriteAccess
|
||||
readonly property bool isBackupSection: true
|
||||
readonly property string title: ""
|
||||
readonly property string description: ""
|
||||
readonly property bool isBackupSection: false
|
||||
readonly property string title: qsTr("Backup")
|
||||
readonly property string description: qsTr("Local copy of VPN protocols, services, all server settings and users")
|
||||
readonly property var tColor: AmneziaStyle.color.paleGray
|
||||
readonly property var clickedHandler: function() {}
|
||||
readonly property var clickedHandler: function() {
|
||||
// Navigate to server backup page using PageController
|
||||
PageController.goToPage(PageEnum.PageSettingsServerBackup)
|
||||
}
|
||||
}
|
||||
|
||||
QtObject {
|
||||
@@ -525,4 +390,4 @@ PageType {
|
||||
PageController.showErrorMessage(qsTr("Backup error: %1").arg(errorMessage))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,10 @@ PageType {
|
||||
function onGoToPageSettingsServerServices() {
|
||||
tabBar.setCurrentIndex(root.pageSettingsServerServices)
|
||||
}
|
||||
|
||||
function onGoToPageSettingsServerManagement() {
|
||||
tabBar.setCurrentIndex(root.pageSettingsServerData)
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
|
||||
@@ -0,0 +1,313 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import PageEnum 1.0
|
||||
import ProtocolEnum 1.0
|
||||
import Style 1.0
|
||||
|
||||
import "../Controls2"
|
||||
import "../Controls2/TextTypes"
|
||||
import "../Components"
|
||||
import "../Config"
|
||||
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
property string backupFilePath: ""
|
||||
property string backupFileName: ""
|
||||
property string serverName: ""
|
||||
property string serverIp: ""
|
||||
property bool isFromSetupWizard: false
|
||||
|
||||
// Credentials для setup wizard (когда сервер еще не добавлен в ServersModel)
|
||||
property string wizardHostname: ""
|
||||
property string wizardUsername: ""
|
||||
property string wizardSecretData: ""
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
|
||||
|
||||
onActiveFocusChanged: {
|
||||
if(backButton.enabled && backButton.activeFocus) {
|
||||
flickable.contentY = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FlickableType {
|
||||
id: flickable
|
||||
|
||||
anchors.top: backButton.bottom
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
contentHeight: contentColumn.implicitHeight
|
||||
|
||||
ColumnLayout {
|
||||
id: contentColumn
|
||||
width: flickable.width
|
||||
|
||||
spacing: 16
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
|
||||
headerText: qsTr("Restore from backup")
|
||||
}
|
||||
|
||||
ParagraphTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
text: {
|
||||
// Показываем только имя файла и IP адрес, без имени сервера
|
||||
if (serverIp && serverIp.length > 0) {
|
||||
return qsTr("%1 on %2").arg(backupFileName).arg(serverIp)
|
||||
}
|
||||
return backupFileName
|
||||
}
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 16
|
||||
spacing: 0
|
||||
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: qsTr("Add data from backup")
|
||||
descriptionText: qsTr("If the same protocols are already installed on the server, they will be updated. Created users and access will be saved")
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
|
||||
clickedFunction: function() {
|
||||
startRestore(false) // false = режим добавления
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {}
|
||||
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: qsTr("Replace")
|
||||
descriptionText: qsTr("All installed protocols, users and their access will not be saved")
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
textColor: AmneziaStyle.color.vibrantRed
|
||||
|
||||
clickedFunction: function() {
|
||||
startRestore(true) // true = режим замены
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property bool restoreReplaceMode: false
|
||||
|
||||
function startRestore(replaceMode) {
|
||||
restoreReplaceMode = replaceMode
|
||||
|
||||
// Если это setup wizard с wizard credentials, используем их напрямую
|
||||
if (isFromSetupWizard && wizardHostname.length > 0) {
|
||||
console.log("Setup wizard mode, using uploadBackupWithStrings")
|
||||
PageController.showBusyIndicator(true)
|
||||
ServersBackupController.uploadBackupWithStrings(
|
||||
wizardHostname,
|
||||
wizardUsername,
|
||||
wizardSecretData,
|
||||
backupFilePath,
|
||||
replaceMode
|
||||
)
|
||||
} else {
|
||||
// Обычный режим - берем из ServersModel
|
||||
PageController.showBusyIndicator(true)
|
||||
var credentials = getServerCredentials()
|
||||
ServersBackupController.uploadBackup(credentials, backupFilePath, replaceMode)
|
||||
}
|
||||
}
|
||||
|
||||
function getServerCredentials() {
|
||||
// Если это setup wizard, используем переданные credentials
|
||||
if (isFromSetupWizard && wizardHostname.length > 0) {
|
||||
console.log("Using wizard credentials:", wizardHostname, wizardUsername)
|
||||
// Устанавливаем credentials в InstallController
|
||||
InstallController.setProcessedServerCredentials(wizardHostname, wizardUsername, wizardSecretData)
|
||||
// Получаем их обратно как C++ объект через ServersModel
|
||||
// Сначала проверяем, есть ли сервер в модели
|
||||
if (ServersModel.getServersCount() > 0 && ServersModel.processedIndex >= 0) {
|
||||
return ServersModel.getServerCredentials(ServersModel.processedIndex)
|
||||
}
|
||||
// Если сервера нет, создаем временный объект (это не сработает, нужен другой подход)
|
||||
console.error("Server not in model yet, cannot get credentials")
|
||||
}
|
||||
|
||||
// Иначе берем из ServersModel
|
||||
var index = ServersModel.processedIndex
|
||||
return ServersModel.getServerCredentials(index)
|
||||
}
|
||||
|
||||
property string lastUploadedBackupFilename: ""
|
||||
|
||||
Connections {
|
||||
target: ServersBackupController
|
||||
|
||||
function onBackupUploaded(serverPath) {
|
||||
PageController.showNotificationMessage(qsTr("Backup uploaded. Restoring configuration..."))
|
||||
|
||||
var backupFilename = serverPath.split('/').pop()
|
||||
lastUploadedBackupFilename = backupFilename
|
||||
|
||||
var credentials = getServerCredentials()
|
||||
ServersBackupController.restoreBackup(credentials, backupFilename, [], restoreReplaceMode)
|
||||
}
|
||||
|
||||
function onBackupRestored() {
|
||||
|
||||
// После успешного восстановления сканируем сервер для обновления информации о контейнерах
|
||||
if (isFromSetupWizard && ServersModel.getServersCount() > 0) {
|
||||
var serverIdx = ServersModel.getServersCount() - 1
|
||||
console.log(" Setting server as default:", serverIdx)
|
||||
ServersModel.setDefaultServerIndex(serverIdx)
|
||||
ServersModel.processedIndex = serverIdx
|
||||
|
||||
// Сканируем сервер для обновления информации о контейнерах
|
||||
console.log(" Scanning server for installed containers...")
|
||||
PageController.showNotificationMessage(qsTr("Updating container information..."))
|
||||
InstallController.scanServerForInstalledContainers()
|
||||
|
||||
// Timer запустится автоматически после получения сигнала scanServerFinished
|
||||
} else {
|
||||
console.log(" Skipping default container setup (not from wizard or no servers)")
|
||||
PageController.showBusyIndicator(false)
|
||||
navigateToRestoredPage()
|
||||
}
|
||||
}
|
||||
|
||||
function onErrorOccurred(errorMessage, errorCode) {
|
||||
PageController.showBusyIndicator(false)
|
||||
PageController.showErrorMessage(qsTr("Backup restore error: %1").arg(errorMessage))
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: InstallController
|
||||
|
||||
function onScanServerFinished(isInstalledContainerFound) {
|
||||
console.log("Scan finished, containers found:", isInstalledContainerFound)
|
||||
|
||||
// Если это setup wizard и сканирование завершено
|
||||
if (isFromSetupWizard) {
|
||||
// Запускаем timer для установки default container
|
||||
setDefaultContainerTimer.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function navigateToRestoredPage() {
|
||||
// Переход на страницу успешного восстановления
|
||||
// Получаем реальное имя сервера из модели
|
||||
var actualServerName = serverName
|
||||
if (root.isFromSetupWizard && ServersModel.getServersCount() > 0) {
|
||||
var serverIdx = ServersModel.getServersCount() - 1
|
||||
var oldProcessedIndex = ServersModel.processedIndex
|
||||
ServersModel.processedIndex = serverIdx
|
||||
actualServerName = ServersModel.getProcessedServerData("name") || qsTr("Server")
|
||||
ServersModel.processedIndex = oldProcessedIndex
|
||||
} else if (!serverName || serverName.length === 0) {
|
||||
// Если имя не передано, получаем из processedIndex
|
||||
actualServerName = ServersModel.getProcessedServerData("name") || qsTr("Server")
|
||||
}
|
||||
|
||||
var parentItem = root.parent
|
||||
|
||||
// Для setup wizard используем обычный StackView
|
||||
if (root.isFromSetupWizard) {
|
||||
while (parentItem && typeof parentItem.push !== "function") {
|
||||
parentItem = parentItem.parent
|
||||
}
|
||||
if (parentItem && typeof parentItem.push === "function") {
|
||||
parentItem.push(PageController.getPagePath(PageEnum.PageSettingsServerBackupRestored), {
|
||||
"backupFileName": backupFileName,
|
||||
"serverName": actualServerName,
|
||||
"serverIp": serverIp,
|
||||
"isFromSetupWizard": true
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Для меню управления ищем tabBarStackView
|
||||
while (parentItem && parentItem.objectName !== "tabBarStackView") {
|
||||
parentItem = parentItem.parent
|
||||
}
|
||||
if (parentItem && typeof parentItem.push === "function") {
|
||||
parentItem.push(PageController.getPagePath(PageEnum.PageSettingsServerBackupRestored), {
|
||||
"backupFileName": backupFileName,
|
||||
"serverName": actualServerName,
|
||||
"serverIp": serverIp,
|
||||
"isFromSetupWizard": false
|
||||
})
|
||||
} else {
|
||||
console.warn("Could not find StackView to navigate to restored page")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,13 @@ import "../Controls2/TextTypes"
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
property var setupWizardEasy: null
|
||||
|
||||
// Сохраняем credentials здесь для использования при восстановлении backup
|
||||
property string savedHostname: ""
|
||||
property string savedUsername: ""
|
||||
property string savedSecretData: ""
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
|
||||
@@ -130,6 +137,12 @@ PageType {
|
||||
return
|
||||
}
|
||||
|
||||
// Сохраняем credentials в свойствах этой страницы
|
||||
root.savedHostname = _hostname
|
||||
root.savedUsername = _username
|
||||
root.savedSecretData = _secretData
|
||||
console.log("Saved credentials in PageSetupWizardCredentials:", _hostname, _username)
|
||||
|
||||
PageController.goToPage(PageEnum.PageSetupWizardEasy)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,123 @@ import "../Config"
|
||||
|
||||
PageType {
|
||||
id: root
|
||||
objectName: "pageSetupWizardEasy"
|
||||
|
||||
property bool isEasySetup: true
|
||||
property bool isRestoreFromBackup: false
|
||||
property string backupFilePath: ""
|
||||
property string restoreHostname: ""
|
||||
property string restoreUsername: ""
|
||||
property string restoreSecretData: ""
|
||||
property bool waitingForServerToAdd: false
|
||||
|
||||
// Connections для отслеживания добавления сервера
|
||||
Connections {
|
||||
target: InstallController
|
||||
|
||||
function onInstallServerFinished(finishedMessage) {
|
||||
if (root.waitingForServerToAdd && root.isRestoreFromBackup && root.backupFilePath.length > 0) {
|
||||
console.log("Server added successfully, now showing restore mode page")
|
||||
root.waitingForServerToAdd = false
|
||||
|
||||
// Запускаем переход на страницу восстановления
|
||||
navigationTimer.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Таймер для перехода на страницу выбора режима после выбора файла
|
||||
Timer {
|
||||
id: navigationTimer
|
||||
interval: 500
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (root.backupFilePath.length > 0 && root.isRestoreFromBackup) {
|
||||
console.log("Navigation timer triggered, going to restore mode page")
|
||||
console.log("Credentials available:", root.restoreHostname, root.restoreUsername, root.restoreSecretData.length > 0 ? "***" : "EMPTY")
|
||||
|
||||
// Получаем имя файла
|
||||
var fileName = SystemController.getFileNameFromPath(root.backupFilePath)
|
||||
if (!fileName || fileName === undefined || fileName.length === 0) {
|
||||
var fallbackName = root.backupFilePath.split('/').pop()
|
||||
fileName = (fallbackName && fallbackName.length > 0) ? fallbackName : qsTr("backup.tgz")
|
||||
}
|
||||
fileName = String(fileName)
|
||||
|
||||
// Извлекаем IP адрес из имени файла
|
||||
var serverIp = ""
|
||||
var ipMatch = fileName.match(/^([\d_]+)\s*-/)
|
||||
if (ipMatch && ipMatch.length > 1) {
|
||||
serverIp = ipMatch[1].replace(/_/g, ".")
|
||||
}
|
||||
if (!serverIp || serverIp.length === 0) {
|
||||
serverIp = root.restoreHostname
|
||||
}
|
||||
|
||||
var serverName = root.restoreHostname
|
||||
if (!serverName || serverName.length === 0) {
|
||||
serverName = qsTr("RestoredServer")
|
||||
}
|
||||
|
||||
// Переходим на страницу установки
|
||||
PageController.goToPage(PageEnum.PageSetupWizardInstalling)
|
||||
|
||||
// Сразу ищем StackView и переходим на страницу восстановления
|
||||
// Сервер уже добавлен, так как мы ждали onInstallServerFinished
|
||||
Qt.callLater(function() {
|
||||
var pagePath = "qrc:/ui/qml/Pages2/PageSettingsServerRestoreMode.qml"
|
||||
|
||||
// Находим главное окно приложения
|
||||
var item = root
|
||||
while (item.parent) {
|
||||
item = item.parent
|
||||
}
|
||||
|
||||
// Находим StackView рекурсивно
|
||||
function findStackView(obj) {
|
||||
if (!obj) return null
|
||||
|
||||
// Проверяем, является ли объект StackView
|
||||
if (obj.toString().indexOf("StackView") !== -1 || typeof obj.push === "function") {
|
||||
return obj
|
||||
}
|
||||
|
||||
// Проверяем children
|
||||
if (obj.children) {
|
||||
for (var i = 0; i < obj.children.length; i++) {
|
||||
var result = findStackView(obj.children[i])
|
||||
if (result) return result
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяем contentItem
|
||||
if (obj.contentItem) {
|
||||
return findStackView(obj.contentItem)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
var stackView = findStackView(item)
|
||||
if (stackView) {
|
||||
console.log("Found StackView, pushing restore mode page")
|
||||
stackView.push(pagePath, {
|
||||
"backupFilePath": root.backupFilePath,
|
||||
"backupFileName": fileName,
|
||||
"serverName": "", // Будет получено из ServersModel
|
||||
"serverIp": serverIp,
|
||||
"isFromSetupWizard": true,
|
||||
"wizardHostname": root.restoreHostname,
|
||||
"wizardUsername": root.restoreUsername,
|
||||
"wizardSecretData": root.restoreSecretData
|
||||
})
|
||||
} else {
|
||||
console.error("Could not find StackView")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SortFilterProxyModel {
|
||||
id: proxyContainersModel
|
||||
@@ -149,6 +264,94 @@ PageType {
|
||||
Keys.onReturnPressed: this.clicked()
|
||||
}
|
||||
|
||||
DividerType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
}
|
||||
|
||||
CardType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
headerText: qsTr("Restore from backup")
|
||||
bodyText: qsTr("Restoration of VPN protocols, services, all server settings and users")
|
||||
|
||||
ButtonGroup.group: buttonGroup
|
||||
|
||||
onClicked: function() {
|
||||
console.log("=== Restore from backup clicked ===")
|
||||
|
||||
// СНАЧАЛА выбираем файл
|
||||
var filter = GC.isMobile() ? "*.gz *.tgz *.tar.gz" : "Backup files (*.tar.gz *.backup *.tgz *.gz)"
|
||||
var localPath = SystemController.getFileName(
|
||||
qsTr("Select Backup to Restore"),
|
||||
filter,
|
||||
"",
|
||||
false,
|
||||
""
|
||||
)
|
||||
|
||||
console.log("Selected file path:", localPath)
|
||||
|
||||
if (!localPath || localPath.length === 0) {
|
||||
console.log("No file selected")
|
||||
return
|
||||
}
|
||||
|
||||
// Сохраняем путь к backup файлу
|
||||
root.backupFilePath = localPath
|
||||
root.isRestoreFromBackup = true
|
||||
|
||||
// Получаем credentials из PageSetupWizardCredentials через поиск в StackView
|
||||
var credentialsPage = null
|
||||
var item = root
|
||||
|
||||
// Ищем StackView
|
||||
while (item && !item.hasOwnProperty("depth")) {
|
||||
item = item.parent
|
||||
}
|
||||
|
||||
// Если нашли StackView, ищем PageSetupWizardCredentials в его истории
|
||||
if (item && item.depth > 0) {
|
||||
for (var i = 0; i < item.depth; i++) {
|
||||
var page = item.get(i)
|
||||
if (page && page.hasOwnProperty("savedHostname")) {
|
||||
credentialsPage = page
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (credentialsPage && credentialsPage.savedHostname.length > 0) {
|
||||
root.restoreHostname = credentialsPage.savedHostname
|
||||
root.restoreUsername = credentialsPage.savedUsername
|
||||
root.restoreSecretData = credentialsPage.savedSecretData
|
||||
console.log("Got credentials from PageSetupWizardCredentials:", root.restoreHostname, root.restoreUsername)
|
||||
|
||||
// ТЕПЕРЬ добавляем пустой сервер с этими credentials
|
||||
InstallController.setShouldCreateServer(true)
|
||||
InstallController.setProcessedServerCredentials(root.restoreHostname, root.restoreUsername, root.restoreSecretData)
|
||||
|
||||
// Устанавливаем флаг ожидания
|
||||
root.waitingForServerToAdd = true
|
||||
|
||||
console.log("Backup file selected, adding server...")
|
||||
// Добавляем сервер (асинхронно)
|
||||
InstallController.addEmptyServer()
|
||||
|
||||
// Дальнейшее выполнение произойдет в onInstallServerFinished
|
||||
} else {
|
||||
console.log("WARNING: No credentials found")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: this.clicked()
|
||||
Keys.onReturnPressed: this.clicked()
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
id: continueButton
|
||||
|
||||
|
||||
Reference in New Issue
Block a user