mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-05-27 21:36:46 +03:00
Compare commits
5 Commits
dev
...
feat_copy_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f73e4e041 | ||
|
|
49807ebb5b | ||
|
|
11e018e654 | ||
|
|
0fa3f861fd | ||
|
|
8eabd549ff |
@@ -135,6 +135,9 @@ void CoreController::initControllers()
|
||||
new SettingsController(m_serversModel, m_containersModel, m_languageModel, m_sitesModel, m_appSplitTunnelingModel, m_settings));
|
||||
m_engine->rootContext()->setContextProperty("SettingsController", m_settingsController.get());
|
||||
|
||||
m_serversBackupController.reset(new ServersBackupController(m_settings, m_serversModel.get()));
|
||||
m_engine->rootContext()->setContextProperty("ServersBackupController", m_serversBackupController.get());
|
||||
|
||||
m_sitesController.reset(new SitesController(m_settings, m_vpnConnection, m_sitesModel));
|
||||
m_engine->rootContext()->setContextProperty("SitesController", m_sitesController.get());
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "ui/controllers/installController.h"
|
||||
#include "ui/controllers/pageController.h"
|
||||
#include "ui/controllers/settingsController.h"
|
||||
#include "ui/controllers/serversBackupController.h"
|
||||
#include "ui/controllers/sitesController.h"
|
||||
#include "ui/controllers/systemController.h"
|
||||
|
||||
@@ -115,6 +116,7 @@ private:
|
||||
QScopedPointer<ImportController> m_importController;
|
||||
QScopedPointer<ExportController> m_exportController;
|
||||
QScopedPointer<SettingsController> m_settingsController;
|
||||
QScopedPointer<ServersBackupController> m_serversBackupController;
|
||||
QScopedPointer<SitesController> m_sitesController;
|
||||
QScopedPointer<SystemController> m_systemController;
|
||||
QScopedPointer<AppSplitTunnelingController> m_appSplitTunnelingController;
|
||||
|
||||
@@ -94,6 +94,25 @@ ErrorCode ServerController::runScript(const ServerCredentials &credentials, QStr
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode ServerController::runHostScript(const ServerCredentials &credentials, QString script,
|
||||
const std::function<ErrorCode(const QString &, libssh::Client &)> &cbReadStdOut,
|
||||
const std::function<ErrorCode(const QString &, libssh::Client &)> &cbReadStdErr)
|
||||
{
|
||||
QString fileName = "/tmp/amnezia_" + Utils::getRandomString(16) + ".sh";
|
||||
|
||||
ErrorCode e = uploadFileToHost(credentials, script.toUtf8(), fileName);
|
||||
if (e)
|
||||
return e;
|
||||
|
||||
QString runner = QString("sudo bash %1").arg(fileName);
|
||||
e = runScript(credentials, runner, cbReadStdOut, cbReadStdErr);
|
||||
|
||||
QString remover = QString("sudo rm -f %1").arg(fileName);
|
||||
runScript(credentials, remover, cbReadStdOut, cbReadStdErr);
|
||||
|
||||
return e;
|
||||
}
|
||||
|
||||
ErrorCode ServerController::runContainerScript(const ServerCredentials &credentials, DockerContainer container, QString script,
|
||||
const std::function<ErrorCode(const QString &, libssh::Client &)> &cbReadStdOut,
|
||||
const std::function<ErrorCode(const QString &, libssh::Client &)> &cbReadStdErr)
|
||||
@@ -210,6 +229,37 @@ ErrorCode ServerController::uploadFileToHost(const ServerCredentials &credential
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode ServerController::downloadFileFromHost(const ServerCredentials &credentials, const QString &remotePath, const QString &localPath)
|
||||
{
|
||||
auto error = m_sshClient.connectToHost(credentials);
|
||||
if (error != ErrorCode::NoError) {
|
||||
return error;
|
||||
}
|
||||
|
||||
error = m_sshClient.scpFileDownload(remotePath, localPath);
|
||||
|
||||
if (error != ErrorCode::NoError) {
|
||||
return error;
|
||||
}
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode ServerController::uploadFileToHostPublic(const ServerCredentials &credentials, const QString &localPath, const QString &remotePath,
|
||||
libssh::ScpOverwriteMode overwriteMode)
|
||||
{
|
||||
auto error = m_sshClient.connectToHost(credentials);
|
||||
if (error != ErrorCode::NoError) {
|
||||
return error;
|
||||
}
|
||||
|
||||
error = m_sshClient.scpFileCopy(overwriteMode, localPath, remotePath, "backup_file");
|
||||
|
||||
if (error != ErrorCode::NoError) {
|
||||
return error;
|
||||
}
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode ServerController::rebootServer(const ServerCredentials &credentials)
|
||||
{
|
||||
QString script = QString("sudo reboot");
|
||||
|
||||
@@ -46,6 +46,10 @@ public:
|
||||
const std::function<ErrorCode(const QString &, libssh::Client &)> &cbReadStdOut = nullptr,
|
||||
const std::function<ErrorCode(const QString &, libssh::Client &)> &cbReadStdErr = nullptr);
|
||||
|
||||
ErrorCode runHostScript(const ServerCredentials &credentials, QString script,
|
||||
const std::function<ErrorCode(const QString &, libssh::Client &)> &cbReadStdOut = nullptr,
|
||||
const std::function<ErrorCode(const QString &, libssh::Client &)> &cbReadStdErr = nullptr);
|
||||
|
||||
ErrorCode runContainerScript(const ServerCredentials &credentials, DockerContainer container, QString script,
|
||||
const std::function<ErrorCode(const QString &, libssh::Client &)> &cbReadStdOut = nullptr,
|
||||
const std::function<ErrorCode(const QString &, libssh::Client &)> &cbReadStdErr = nullptr);
|
||||
@@ -57,6 +61,10 @@ public:
|
||||
ErrorCode getDecryptedPrivateKey(const ServerCredentials &credentials, QString &decryptedPrivateKey,
|
||||
const std::function<QString()> &callback);
|
||||
|
||||
ErrorCode downloadFileFromHost(const ServerCredentials &credentials, const QString &remotePath, const QString &localPath);
|
||||
ErrorCode uploadFileToHostPublic(const ServerCredentials &credentials, const QString &localPath, const QString &remotePath,
|
||||
libssh::ScpOverwriteMode overwriteMode = libssh::ScpOverwriteMode::ScpOverwriteExisting);
|
||||
|
||||
private:
|
||||
ErrorCode installDockerWorker(const ServerCredentials &credentials, DockerContainer container);
|
||||
ErrorCode prepareHostWorker(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &config = QJsonObject());
|
||||
|
||||
@@ -290,6 +290,86 @@ namespace libssh {
|
||||
return watcher.result();
|
||||
}
|
||||
|
||||
ErrorCode Client::scpFileDownload(const QString& remotePath, const QString& localPath)
|
||||
{
|
||||
// Use full path for SCP download
|
||||
m_scpSession = ssh_scp_new(m_session, SSH_SCP_READ, remotePath.toStdString().c_str());
|
||||
|
||||
if (m_scpSession == nullptr) {
|
||||
return fromLibsshErrorCode();
|
||||
}
|
||||
|
||||
if (ssh_scp_init(m_scpSession) != SSH_OK) {
|
||||
auto errorCode = fromLibsshErrorCode();
|
||||
closeScpSession();
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
QFutureWatcher<ErrorCode> watcher;
|
||||
connect(&watcher, &QFutureWatcher<ErrorCode>::finished, this, &Client::scpFileDownloadFinished);
|
||||
QFuture<ErrorCode> future = QtConcurrent::run([this, &remotePath, &localPath]() {
|
||||
// Pull request - this gets file info
|
||||
int result = ssh_scp_pull_request(m_scpSession);
|
||||
if (result != SSH_SCP_REQUEST_NEWFILE) {
|
||||
return fromLibsshErrorCode();
|
||||
}
|
||||
|
||||
// Accept the request
|
||||
ssh_scp_accept_request(m_scpSession);
|
||||
|
||||
// Get file size
|
||||
int fileSize = ssh_scp_request_get_size(m_scpSession);
|
||||
if (fileSize <= 0) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
// Open local file for writing
|
||||
QFile fout(localPath);
|
||||
if (!fout.open(QIODevice::WriteOnly)) {
|
||||
return fromFileErrorCode(fout.error());
|
||||
}
|
||||
|
||||
// Read file data in chunks
|
||||
constexpr size_t bufferSize = 16384;
|
||||
int transferred = 0;
|
||||
|
||||
while (transferred < fileSize) {
|
||||
int chunkSize = qMin(bufferSize, static_cast<size_t>(fileSize - transferred));
|
||||
QByteArray buffer(chunkSize, 0);
|
||||
|
||||
int bytesRead = ssh_scp_read(m_scpSession, buffer.data(), chunkSize);
|
||||
if (bytesRead == SSH_ERROR) {
|
||||
fout.close();
|
||||
return fromLibsshErrorCode();
|
||||
}
|
||||
|
||||
if (bytesRead != chunkSize) {
|
||||
fout.close();
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
qint64 bytesWritten = fout.write(buffer);
|
||||
if (bytesWritten != chunkSize) {
|
||||
fout.close();
|
||||
return fromFileErrorCode(fout.error());
|
||||
}
|
||||
|
||||
transferred += bytesRead;
|
||||
}
|
||||
|
||||
fout.close();
|
||||
return ErrorCode::NoError;
|
||||
});
|
||||
watcher.setFuture(future);
|
||||
|
||||
QEventLoop wait;
|
||||
QObject::connect(this, &Client::scpFileDownloadFinished, &wait, &QEventLoop::quit);
|
||||
wait.exec();
|
||||
|
||||
closeScpSession();
|
||||
return watcher.result();
|
||||
}
|
||||
|
||||
void Client::closeScpSession()
|
||||
{
|
||||
if (m_scpSession != nullptr) {
|
||||
|
||||
@@ -36,6 +36,8 @@ namespace libssh {
|
||||
const QString &localPath,
|
||||
const QString &remotePath,
|
||||
const QString &fileDesc);
|
||||
ErrorCode scpFileDownload(const QString &remotePath,
|
||||
const QString &localPath);
|
||||
ErrorCode getDecryptedPrivateKey(const ServerCredentials &credentials, QString &decryptedPrivateKey, const std::function<QString()> &passphraseCallback);
|
||||
private:
|
||||
ErrorCode closeChannel();
|
||||
@@ -52,6 +54,7 @@ namespace libssh {
|
||||
signals:
|
||||
void writeToChannelFinished();
|
||||
void scpFileCopyFinished();
|
||||
void scpFileDownloadFinished();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -208,6 +208,9 @@
|
||||
<file>ui/qml/Pages2/PageSettingsApplication.qml</file>
|
||||
<file>ui/qml/Pages2/PageSettingsAppSplitTunneling.qml</file>
|
||||
<file>ui/qml/Pages2/PageSettingsBackup.qml</file>
|
||||
<file>ui/qml/Pages2/PageSettingsServerBackup.qml</file>
|
||||
<file>ui/qml/Pages2/PageSettingsServerRestoreMode.qml</file>
|
||||
<file>ui/qml/Pages2/PageSettingsServerBackupRestored.qml</file>
|
||||
<file>ui/qml/Pages2/PageSettingsConnection.qml</file>
|
||||
<file>ui/qml/Pages2/PageSettingsDns.qml</file>
|
||||
<file>ui/qml/Pages2/PageSettingsKillSwitch.qml</file>
|
||||
|
||||
@@ -29,6 +29,9 @@ namespace PageLoader
|
||||
PageSettingsNewsNotifications,
|
||||
PageSettingsNewsDetail,
|
||||
PageSettingsBackup,
|
||||
PageSettingsServerBackup,
|
||||
PageSettingsServerRestoreMode,
|
||||
PageSettingsServerBackupRestored,
|
||||
PageSettingsAbout,
|
||||
PageSettingsLogging,
|
||||
PageSettingsSplitTunneling,
|
||||
@@ -126,6 +129,7 @@ signals:
|
||||
void goToPageSettings();
|
||||
void goToPageViewConfig();
|
||||
void goToPageSettingsServerServices();
|
||||
void goToPageSettingsServerManagement();
|
||||
void goToPageSettingsBackup();
|
||||
void goToShareConnectionPage(QString headerText, QString configContentHeaderText, QString configCaption, QString configExtension,
|
||||
QString configFileName);
|
||||
|
||||
1484
client/ui/controllers/serversBackupController.cpp
Normal file
1484
client/ui/controllers/serversBackupController.cpp
Normal file
File diff suppressed because it is too large
Load Diff
389
client/ui/controllers/serversBackupController.h
Normal file
389
client/ui/controllers/serversBackupController.h
Normal file
@@ -0,0 +1,389 @@
|
||||
#ifndef SERVERSBACKUPCONTROLLER_H
|
||||
#define SERVERSBACKUPCONTROLLER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QDateTime>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QFileInfo>
|
||||
|
||||
class QTemporaryFile;
|
||||
class ServersModel;
|
||||
|
||||
#include "core/controllers/serverController.h"
|
||||
#include "core/defs.h"
|
||||
#include "containers/containers_defs.h"
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
/**
|
||||
* @brief Controller for managing Amnezia VPN configuration backups
|
||||
*
|
||||
* Uses existing ServerController and libssh::Client from Amnezia
|
||||
* Bash scripts are embedded directly in C++ code
|
||||
* Supports direct container backup via docker cp
|
||||
*
|
||||
* Fully cross-platform: Windows, macOS, Linux, iOS, Android
|
||||
*/
|
||||
class ServersBackupController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ServersBackupController(std::shared_ptr<Settings> settings, ServersModel *serversModel, QObject *parent = nullptr);
|
||||
~ServersBackupController();
|
||||
|
||||
/**
|
||||
* @brief Backup information
|
||||
*/
|
||||
struct BackupInfo {
|
||||
QString filename;
|
||||
QString fullPath;
|
||||
QDateTime createdAt;
|
||||
qint64 size;
|
||||
bool isValid;
|
||||
QStringList containers;
|
||||
};
|
||||
|
||||
enum BackupStatus {
|
||||
Idle,
|
||||
InProgress,
|
||||
Success,
|
||||
Failed
|
||||
};
|
||||
Q_ENUM(BackupStatus)
|
||||
|
||||
public slots:
|
||||
/**
|
||||
* @brief Create backup on server (all containers)
|
||||
* @param credentials Server credentials
|
||||
*/
|
||||
void createBackup(const ServerCredentials &credentials);
|
||||
|
||||
/**
|
||||
* @brief Create backup and automatically download to device (for QML)
|
||||
* @param downloadToDevice Download to device after creation?
|
||||
* @param deleteFromServer Delete from server after download?
|
||||
*/
|
||||
Q_INVOKABLE void createBackupWithDownload(bool downloadToDevice = true,
|
||||
bool deleteFromServer = true);
|
||||
|
||||
/**
|
||||
* @brief Create backup of specific container
|
||||
* @param credentials Server credentials
|
||||
* @param container Container type for backup
|
||||
*/
|
||||
void createContainerBackup(const ServerCredentials &credentials, DockerContainer container);
|
||||
|
||||
/**
|
||||
* @brief Create backup of specific container by name
|
||||
* @param credentials Server credentials
|
||||
* @param containerName Container name (e.g. "amnezia-awg")
|
||||
*/
|
||||
void createBackupByName(const ServerCredentials &credentials, const QString &containerName);
|
||||
|
||||
/**
|
||||
* @brief Create backup of multiple containers
|
||||
* @param credentials Server credentials
|
||||
* @param containers List of containers for backup
|
||||
*/
|
||||
void createContainersBackup(const ServerCredentials &credentials, const QList<DockerContainer> &containers);
|
||||
|
||||
/**
|
||||
* @brief Get list of backups from server
|
||||
* @param credentials Server credentials
|
||||
*/
|
||||
void fetchBackupList(const ServerCredentials &credentials);
|
||||
|
||||
/**
|
||||
* @brief Restore from backup
|
||||
* @param credentials Server credentials
|
||||
* @param backupFilename Backup file name
|
||||
* @param containers List of containers (empty = all)
|
||||
* @param replaceMode If true - clears container first, then restores. If false - adds data on top of existing
|
||||
*/
|
||||
void restoreBackup(const ServerCredentials &credentials,
|
||||
const QString &backupFilename,
|
||||
const QStringList &containers = QStringList(),
|
||||
bool replaceMode = false);
|
||||
|
||||
/**
|
||||
* @brief Check backup status on server
|
||||
* @param credentials Server credentials
|
||||
*/
|
||||
void checkBackupStatus(const ServerCredentials &credentials);
|
||||
|
||||
/**
|
||||
* @brief Download backup to local machine
|
||||
* @param credentials Server credentials
|
||||
* @param backupFilename Backup file name
|
||||
* @param localPath Save path
|
||||
*/
|
||||
void downloadBackup(const ServerCredentials &credentials,
|
||||
const QString &backupFilename,
|
||||
const QString &localPath);
|
||||
|
||||
/**
|
||||
* @brief Upload backup to server
|
||||
* @param credentials Server credentials
|
||||
* @param localPath Path to local file
|
||||
* @param replaceMode Restore mode (true = replace, false = add). Saved for later use in restoreBackup
|
||||
*/
|
||||
void uploadBackup(const ServerCredentials &credentials,
|
||||
const QString &localPath,
|
||||
bool replaceMode = false);
|
||||
|
||||
// Overloaded method for setup wizard with separate credential parameters
|
||||
Q_INVOKABLE void uploadBackupWithStrings(const QString &hostname,
|
||||
const QString &username,
|
||||
const QString &secretData,
|
||||
const QString &localPath,
|
||||
bool replaceMode = false);
|
||||
|
||||
/**
|
||||
* @brief Universal method to start restore (from QML)
|
||||
* Automatically selects correct path depending on parameters
|
||||
* @param isFromSetupWizard Restore from setup wizard?
|
||||
* @param backupFilePath Path to local backup file
|
||||
* @param replaceMode Restore mode (true = replace, false = add)
|
||||
* @param wizardHostname Hostname for setup wizard (optional)
|
||||
* @param wizardUsername Username for setup wizard (optional)
|
||||
* @param wizardSecretData Secret data for setup wizard (optional)
|
||||
*/
|
||||
Q_INVOKABLE void startRestore(bool isFromSetupWizard,
|
||||
const QString &backupFilePath,
|
||||
bool replaceMode,
|
||||
const QString &wizardHostname = QString(),
|
||||
const QString &wizardUsername = QString(),
|
||||
const QString &wizardSecretData = QString());
|
||||
|
||||
/**
|
||||
* @brief Prepare restore information from backup file
|
||||
* Parses filename, extracts IP, prepares metadata
|
||||
* @param backupFilePath Path to backup file
|
||||
* @return QVariantMap with keys: fileName, serverIp
|
||||
*/
|
||||
Q_INVOKABLE QVariantMap getBackupFileInfo(const QString &backupFilePath);
|
||||
|
||||
/**
|
||||
* @brief Scan backup file and determine which containers it contains
|
||||
* @param localPath Path to local backup file
|
||||
* @return List of container names found in backup
|
||||
*/
|
||||
Q_INVOKABLE QStringList scanBackupForContainers(const QString &localPath);
|
||||
|
||||
/**
|
||||
* @brief Set default server and container after restore (for setup wizard)
|
||||
* @param isFromSetupWizard Was restore called from setup wizard
|
||||
* @return true if successful, false if no servers or containers
|
||||
*/
|
||||
Q_INVOKABLE bool setDefaultServerAfterRestore(bool isFromSetupWizard);
|
||||
|
||||
/**
|
||||
* @brief Install containers from backup on empty server (for setup wizard)
|
||||
* Scans backup, adds empty server and sends signal to install containers
|
||||
* @param backupFilePath Path to local backup file
|
||||
* @param hostname Server hostname
|
||||
* @param username Username for SSH
|
||||
* @param secretData Password/key for SSH
|
||||
*/
|
||||
Q_INVOKABLE void prepareRestoreFromBackup(const QString &backupFilePath,
|
||||
const QString &hostname,
|
||||
const QString &username,
|
||||
const QString &secretData);
|
||||
|
||||
/**
|
||||
* @brief Delete backup from server
|
||||
* @param credentials Server credentials
|
||||
* @param backupFilename Backup file name
|
||||
*/
|
||||
void deleteBackup(const ServerCredentials &credentials,
|
||||
const QString &backupFilename);
|
||||
|
||||
/**
|
||||
* @brief Set backup directory on server
|
||||
*/
|
||||
void setBackupDirectory(const QString &directory);
|
||||
|
||||
/**
|
||||
* @brief Get backup directory
|
||||
*/
|
||||
QString backupDirectory() const { return m_backupDir; }
|
||||
|
||||
signals:
|
||||
/**
|
||||
* @brief Operation status changed
|
||||
*/
|
||||
void statusChanged(BackupStatus status);
|
||||
|
||||
/**
|
||||
* @brief Operation progress (0-100)
|
||||
*/
|
||||
void progressChanged(int percent, const QString &message);
|
||||
|
||||
/**
|
||||
* @brief Backup list received
|
||||
*/
|
||||
void backupListReceived(const QList<BackupInfo> &backups);
|
||||
|
||||
/**
|
||||
* @brief Backup created successfully
|
||||
*/
|
||||
void backupCreated(const QString &backupFilename);
|
||||
|
||||
/**
|
||||
* @brief Backup restored successfully
|
||||
*/
|
||||
void backupRestored();
|
||||
|
||||
/**
|
||||
* @brief Need to set default server and container (for setup wizard)
|
||||
* This signal is sent after backupRestored() if restore was from setup wizard
|
||||
*/
|
||||
void needSetDefaultServer();
|
||||
|
||||
/**
|
||||
* @brief Default server and container successfully set
|
||||
* Can navigate to result page
|
||||
*/
|
||||
void defaultServerAndContainerSet();
|
||||
|
||||
/**
|
||||
* @brief All containers from backup installed
|
||||
* Can proceed to data restore
|
||||
* @param backupFilePath Path to backup file
|
||||
* @param hostname Hostname
|
||||
* @param username Username
|
||||
* @param secretData Secret data
|
||||
* @param serverIp IP address (for display)
|
||||
* @param fileName File name (for display)
|
||||
*/
|
||||
void readyForRestore(const QString &backupFilePath,
|
||||
const QString &hostname,
|
||||
const QString &username,
|
||||
const QString &secretData,
|
||||
const QString &serverIp,
|
||||
const QString &fileName);
|
||||
|
||||
/**
|
||||
* @brief Backup downloaded
|
||||
*/
|
||||
void backupDownloaded(const QString &localPath);
|
||||
|
||||
/**
|
||||
* @brief Backup uploaded to server
|
||||
*/
|
||||
void backupUploaded(const QString &serverPath);
|
||||
|
||||
/**
|
||||
* @brief Backup status information received
|
||||
*/
|
||||
void backupStatusReceived(const QJsonObject &status);
|
||||
|
||||
/**
|
||||
* @brief Error occurred
|
||||
*/
|
||||
void errorOccurred(const QString &errorMessage, ErrorCode errorCode);
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Get bash script for creating backup of all containers
|
||||
* @param ipAddress Server IP address in underscored format (e.g. "192_119_110_11")
|
||||
*/
|
||||
QString getBackupScript(const QString &ipAddress) const;
|
||||
|
||||
/**
|
||||
* @brief Get bash script for creating backup of specific container
|
||||
* @param container Container type
|
||||
* @param ipAddress Server IP address in underscored format (e.g. "192_119_110_11")
|
||||
*/
|
||||
QString getContainerBackupScript(DockerContainer container, const QString &ipAddress) const;
|
||||
|
||||
/**
|
||||
* @brief Get bash script for creating backup of multiple containers
|
||||
* @param containers List of containers
|
||||
* @param ipAddress Server IP address in underscored format (e.g. "192_119_110_11")
|
||||
*/
|
||||
QString getContainersBackupScript(const QList<DockerContainer> &containers, const QString &ipAddress) const;
|
||||
|
||||
/**
|
||||
* @brief Get bash script for restore
|
||||
* @param backupFilename Backup file name
|
||||
* @param containers List of containers
|
||||
* @param replaceMode If true - clears container first, then restores
|
||||
*/
|
||||
QString getRestoreScript(const QString &backupFilename, const QStringList &containers, bool replaceMode = false) const;
|
||||
|
||||
/**
|
||||
* @brief Get bash script for status check
|
||||
*/
|
||||
QString getCheckStatusScript() const;
|
||||
|
||||
/**
|
||||
* @brief Get bash script for backup list
|
||||
*/
|
||||
QString getListBackupsScript() const;
|
||||
|
||||
/**
|
||||
* @brief Parse backup list from output
|
||||
*/
|
||||
QList<BackupInfo> parseBackupList(const QString &output);
|
||||
|
||||
/**
|
||||
* @brief Parse status from output
|
||||
*/
|
||||
QJsonObject parseBackupStatus(const QString &output);
|
||||
|
||||
/**
|
||||
* @brief Handle standard output
|
||||
*/
|
||||
ErrorCode handleStdOut(const QString &data, QString &output);
|
||||
|
||||
/**
|
||||
* @brief Handle error output
|
||||
*/
|
||||
ErrorCode handleStdErr(const QString &data, QString &error);
|
||||
|
||||
/**
|
||||
* @brief Set status
|
||||
*/
|
||||
void setStatus(BackupStatus status);
|
||||
|
||||
/**
|
||||
* @brief Set progress
|
||||
*/
|
||||
void setProgress(int percent, const QString &message);
|
||||
|
||||
/**
|
||||
* @brief Attempt to set default container (called from timer)
|
||||
*/
|
||||
void trySetDefaultContainer();
|
||||
|
||||
private:
|
||||
std::shared_ptr<Settings> m_settings;
|
||||
ServersModel *m_serversModel;
|
||||
ServerController *m_serverController;
|
||||
BackupStatus m_status;
|
||||
QString m_backupDir;
|
||||
QString m_currentOutput;
|
||||
QString m_currentError;
|
||||
bool m_restoreReplaceMode; // Save restore mode for use after uploadBackup
|
||||
QTemporaryFile *m_tempUploadFile; // Temp file for Android URI (to prevent deletion before upload completes)
|
||||
|
||||
// For setting default container
|
||||
int m_containerRetryCount;
|
||||
static constexpr int m_maxContainerRetries = 3;
|
||||
|
||||
// For automatic restore after upload
|
||||
ServerCredentials m_pendingRestoreCredentials;
|
||||
bool m_autoRestoreAfterUpload;
|
||||
|
||||
// For automatic backup download/delete
|
||||
bool m_autoDownloadAfterCreate;
|
||||
bool m_autoDeleteAfterDownload;
|
||||
QString m_lastCreatedBackupFilename;
|
||||
};
|
||||
|
||||
#endif // SERVERSBACKUPCONTROLLER_H
|
||||
|
||||
@@ -39,7 +39,10 @@ void SystemController::saveFile(const QString &fileName, const QString &data)
|
||||
#endif
|
||||
|
||||
// todo check if save successful
|
||||
file.open(QIODevice::WriteOnly);
|
||||
if (!file.open(QIODevice::WriteOnly)) {
|
||||
qWarning() << "Failed to open file for writing:" << fileName;
|
||||
return;
|
||||
}
|
||||
file.write(data.toUtf8());
|
||||
file.close();
|
||||
|
||||
@@ -153,6 +156,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);
|
||||
|
||||
|
||||
217
client/ui/qml/Pages2/PageSettingsServerBackup.qml
Normal file
217
client/ui/qml/Pages2/PageSettingsServerBackup.qml
Normal file
@@ -0,0 +1,217 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createBackup(shouldDownload) {
|
||||
// Default shouldDownload = true
|
||||
var 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)
|
||||
// Call C++ method that manages download and delete automatically
|
||||
ServersBackupController.createBackupWithDownload(downloadAfterCreate, true)
|
||||
}
|
||||
var noButtonFunction = function() {}
|
||||
|
||||
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
|
||||
}
|
||||
|
||||
function restoreBackup() {
|
||||
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
|
||||
}
|
||||
|
||||
// Get file information via C++
|
||||
var fileInfo = ServersBackupController.getBackupFileInfo(localPath)
|
||||
var fileName = fileInfo.fileName || "backup.tgz"
|
||||
var serverIp = fileInfo.serverIp || ""
|
||||
|
||||
// If IP not found in filename, use current server
|
||||
if (!serverIp || serverIp.length === 0) {
|
||||
serverIp = ServersModel.getProcessedServerData("hostName") || ""
|
||||
}
|
||||
|
||||
var serverName = ServersModel.getProcessedServerData("name") || qsTr("Server")
|
||||
|
||||
// Open restore mode selection page
|
||||
var parentItem = root.parent
|
||||
while (parentItem && parentItem.objectName !== "tabBarStackView") {
|
||||
parentItem = parentItem.parent
|
||||
}
|
||||
|
||||
if (parentItem && typeof parentItem.push === "function") {
|
||||
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 ============
|
||||
|
||||
Connections {
|
||||
target: ServersBackupController
|
||||
|
||||
function onBackupCreated(backupFilename) {
|
||||
// If auto-download is not enabled, show success message
|
||||
PageController.showBusyIndicator(false)
|
||||
PageController.showNotificationMessage(qsTr("Backup created successfully: %1").arg(backupFilename))
|
||||
}
|
||||
|
||||
function onBackupDownloaded(localPath) {
|
||||
PageController.showBusyIndicator(false)
|
||||
console.log("Backup downloaded to:", localPath)
|
||||
PageController.showNotificationMessage(qsTr("Backup downloaded successfully!\n\nSaved to:\n%1").arg(localPath))
|
||||
}
|
||||
|
||||
function onBackupRestored() {
|
||||
PageController.showBusyIndicator(false)
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
130
client/ui/qml/Pages2/PageSettingsServerBackupRestored.qml
Normal file
130
client/ui/qml/Pages2/PageSettingsServerBackupRestored.qml
Normal file
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,6 +97,7 @@ PageType {
|
||||
|
||||
property list<QtObject> serverActions: [
|
||||
check,
|
||||
backupSection,
|
||||
reboot,
|
||||
remove,
|
||||
clear,
|
||||
@@ -108,6 +109,7 @@ PageType {
|
||||
id: check
|
||||
|
||||
property bool isVisible: root.isServerWithWriteAccess
|
||||
readonly property bool isBackupSection: false
|
||||
readonly property string title: qsTr("Check the server for previously installed Amnezia services")
|
||||
readonly property string description: qsTr("Add them to the application if they were not displayed")
|
||||
readonly property var tColor: AmneziaStyle.color.paleGray
|
||||
@@ -118,10 +120,25 @@ PageType {
|
||||
}
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: backupSection
|
||||
|
||||
property bool isVisible: root.isServerWithWriteAccess
|
||||
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() {
|
||||
// Navigate to server backup page using PageController
|
||||
PageController.goToPage(PageEnum.PageSettingsServerBackup)
|
||||
}
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: reboot
|
||||
|
||||
property bool isVisible: root.isServerWithWriteAccess
|
||||
readonly property bool isBackupSection: false
|
||||
readonly property string title: qsTr("Reboot server")
|
||||
readonly property string description: ""
|
||||
readonly property var tColor: AmneziaStyle.color.vibrantRed
|
||||
@@ -152,6 +169,7 @@ PageType {
|
||||
id: remove
|
||||
|
||||
property bool isVisible: true
|
||||
readonly property bool isBackupSection: false
|
||||
readonly property string title: qsTr("Remove server from application")
|
||||
readonly property string description: ""
|
||||
readonly property var tColor: AmneziaStyle.color.vibrantRed
|
||||
@@ -182,6 +200,7 @@ PageType {
|
||||
id: clear
|
||||
|
||||
property bool isVisible: root.isServerWithWriteAccess
|
||||
readonly property bool isBackupSection: false
|
||||
readonly property string title: qsTr("Clear server from Amnezia software")
|
||||
readonly property string description: ""
|
||||
readonly property var tColor: AmneziaStyle.color.vibrantRed
|
||||
@@ -211,6 +230,7 @@ PageType {
|
||||
id: reset
|
||||
|
||||
property bool isVisible: ServersModel.getProcessedServerData("isServerFromTelegramApi")
|
||||
readonly property bool isBackupSection: false
|
||||
readonly property string title: qsTr("Reset API config")
|
||||
readonly property string description: ""
|
||||
readonly property var tColor: AmneziaStyle.color.vibrantRed
|
||||
@@ -241,6 +261,7 @@ PageType {
|
||||
id: switch_to_premium
|
||||
|
||||
property bool isVisible: ServersModel.getProcessedServerData("isServerFromTelegramApi") && ServersModel.processedServerIsPremium
|
||||
readonly property bool isBackupSection: false
|
||||
readonly property string title: qsTr("Switch to the new Amnezia Premium subscription")
|
||||
readonly property string description: ""
|
||||
readonly property var tColor: AmneziaStyle.color.vibrantRed
|
||||
@@ -249,4 +270,5 @@ PageType {
|
||||
ApiPremV1MigrationController.showMigrationDrawer()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -31,6 +31,10 @@ PageType {
|
||||
function onGoToPageSettingsServerServices() {
|
||||
tabBar.setCurrentIndex(root.pageSettingsServerServices)
|
||||
}
|
||||
|
||||
function onGoToPageSettingsServerManagement() {
|
||||
tabBar.setCurrentIndex(root.pageSettingsServerData)
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
|
||||
216
client/ui/qml/Pages2/PageSettingsServerRestoreMode.qml
Normal file
216
client/ui/qml/Pages2/PageSettingsServerRestoreMode.qml
Normal file
@@ -0,0 +1,216 @@
|
||||
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 for setup wizard (when server is not yet added to 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: {
|
||||
// Show only filename and IP address, without server name
|
||||
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 = add mode
|
||||
}
|
||||
}
|
||||
|
||||
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 = replace mode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property bool restoreReplaceMode: false
|
||||
|
||||
function startRestore(replaceMode) {
|
||||
restoreReplaceMode = replaceMode
|
||||
PageController.showBusyIndicator(true)
|
||||
|
||||
// Call universal C++ method that will determine how to perform restore
|
||||
ServersBackupController.startRestore(
|
||||
isFromSetupWizard,
|
||||
backupFilePath,
|
||||
replaceMode,
|
||||
wizardHostname || "",
|
||||
wizardUsername || "",
|
||||
wizardSecretData || ""
|
||||
)
|
||||
}
|
||||
|
||||
property string lastUploadedBackupFilename: ""
|
||||
|
||||
Connections {
|
||||
target: ServersBackupController
|
||||
|
||||
function onBackupRestored() {
|
||||
console.log(" onBackupRestored, isFromSetupWizard:", isFromSetupWizard)
|
||||
|
||||
// For setup wizard, call C++ method to set default server and container
|
||||
if (isFromSetupWizard) {
|
||||
ServersBackupController.setDefaultServerAfterRestore(true)
|
||||
} else {
|
||||
// For regular mode, navigate directly
|
||||
PageController.showBusyIndicator(false)
|
||||
navigateToRestoredPage()
|
||||
}
|
||||
}
|
||||
|
||||
function onDefaultServerAndContainerSet() {
|
||||
console.log(" onDefaultServerAndContainerSet - navigating to restored page")
|
||||
// C++ has set default server and container, navigate to result page
|
||||
PageController.showBusyIndicator(false)
|
||||
navigateToRestoredPage()
|
||||
}
|
||||
|
||||
function onErrorOccurred(errorMessage, errorCode) {
|
||||
PageController.showBusyIndicator(false)
|
||||
PageController.showErrorMessage(qsTr("Backup restore error: %1").arg(errorMessage))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function navigateToRestoredPage() {
|
||||
// Navigate to successful restore page
|
||||
// Get actual server name from model
|
||||
var actualServerName = serverName
|
||||
if (root.isFromSetupWizard && ServersModel.getServersCount() > 0) {
|
||||
var serverIdx = ServersModel.getServersCount() - 1
|
||||
var oldProcessedIndex = ServersModel.processedIndex
|
||||
ServersModel.processedIndex = serverIdx
|
||||
actualServerName = ServersModel.getProcessedServerData("name") || qsTr("Server")
|
||||
ServersModel.processedIndex = oldProcessedIndex
|
||||
} else if (!serverName || serverName.length === 0) {
|
||||
// If name not provided, get from processedIndex
|
||||
actualServerName = ServersModel.getProcessedServerData("name") || qsTr("Server")
|
||||
}
|
||||
|
||||
var parentItem = root.parent
|
||||
|
||||
// For setup wizard use regular 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 {
|
||||
// For management menu, find 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,12 @@ import "../Controls2/TextTypes"
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
property var setupWizardEasy: null
|
||||
|
||||
property string savedHostname: ""
|
||||
property string savedUsername: ""
|
||||
property string savedSecretData: ""
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
|
||||
@@ -118,6 +124,11 @@ PageType {
|
||||
return
|
||||
}
|
||||
|
||||
root.savedHostname = _hostname
|
||||
root.savedUsername = _username
|
||||
root.savedSecretData = _secretData
|
||||
console.log("Saved credentials in PageSetupWizardCredentials:", _hostname, _username)
|
||||
|
||||
PageController.goToPage(PageEnum.PageSetupWizardEasy)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,240 @@ 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
|
||||
|
||||
// For installing containers from backup
|
||||
property var containersToInstall: []
|
||||
property int currentContainerIndex: 0
|
||||
property bool isInstallingContainers: false
|
||||
|
||||
// Connections for ServersBackupController
|
||||
Connections {
|
||||
target: ServersBackupController
|
||||
|
||||
function onReadyForRestore(backupFilePath, hostname, username, secretData, serverIp, fileName) {
|
||||
console.log("onReadyForRestore received from C++")
|
||||
console.log(" backupFilePath:", backupFilePath)
|
||||
console.log(" hostname:", hostname)
|
||||
console.log(" serverIp:", serverIp)
|
||||
console.log(" fileName:", fileName)
|
||||
|
||||
// Scan backup to determine containers (C++ already did this, but needed for QML)
|
||||
var foundContainers = ServersBackupController.scanBackupForContainers(backupFilePath)
|
||||
console.log("Found containers:", foundContainers)
|
||||
|
||||
if (foundContainers.length === 0) {
|
||||
PageController.showErrorMessage(qsTr("No containers found in backup file"))
|
||||
root.isRestoreFromBackup = false
|
||||
return
|
||||
}
|
||||
|
||||
root.containersToInstall = foundContainers
|
||||
root.currentContainerIndex = 0
|
||||
|
||||
// Now add empty server with these credentials
|
||||
InstallController.setShouldCreateServer(true)
|
||||
InstallController.setProcessedServerCredentials(hostname, username, secretData)
|
||||
|
||||
// Set waiting flag
|
||||
root.waitingForServerToAdd = true
|
||||
|
||||
console.log("Backup scanned, adding server...")
|
||||
// Add server (asynchronously)
|
||||
InstallController.addEmptyServer()
|
||||
|
||||
// Further execution will happen in onInstallServerFinished
|
||||
}
|
||||
}
|
||||
|
||||
// Connections for tracking server addition
|
||||
Connections {
|
||||
target: InstallController
|
||||
|
||||
function onInstallServerFinished(finishedMessage) {
|
||||
if (root.waitingForServerToAdd && root.isRestoreFromBackup && root.backupFilePath.length > 0) {
|
||||
console.log("Server added successfully, now installing containers from backup...")
|
||||
root.waitingForServerToAdd = false
|
||||
|
||||
// Server already created, set flag to false
|
||||
InstallController.setShouldCreateServer(false)
|
||||
|
||||
// Start installing containers
|
||||
root.isInstallingContainers = true
|
||||
installNextContainer()
|
||||
}
|
||||
}
|
||||
|
||||
function onInstallContainerFinished(finishedMessage, isServiceInstall) {
|
||||
if (root.isInstallingContainers) {
|
||||
console.log("Container installed:", finishedMessage)
|
||||
|
||||
// Move to next container
|
||||
root.currentContainerIndex++
|
||||
|
||||
if (root.currentContainerIndex < root.containersToInstall.length) {
|
||||
// Install next container
|
||||
installNextContainer()
|
||||
} else {
|
||||
// All containers installed, now do restore
|
||||
console.log("All containers installed, starting restore...")
|
||||
root.isInstallingContainers = false
|
||||
|
||||
// IMPORTANT: Turn off busy indicator before navigation
|
||||
PageController.showBusyIndicator(false)
|
||||
|
||||
// Start navigation to restore mode selection page
|
||||
navigationTimer.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Function to install next container from list
|
||||
function installNextContainer() {
|
||||
if (root.currentContainerIndex >= root.containersToInstall.length) {
|
||||
return
|
||||
}
|
||||
|
||||
var containerName = root.containersToInstall[root.currentContainerIndex]
|
||||
console.log("Installing container:", containerName, "(", root.currentContainerIndex + 1, "/", root.containersToInstall.length, ")")
|
||||
|
||||
// Convert container name to DockerContainer enum
|
||||
var dockerContainer = ContainerProps.containerFromString(containerName)
|
||||
|
||||
if (dockerContainer === 0) { // None
|
||||
console.log("Unknown container:", containerName, "skipping...")
|
||||
root.currentContainerIndex++
|
||||
installNextContainer()
|
||||
return
|
||||
}
|
||||
|
||||
// Get default settings for container
|
||||
var defaultProtocol = ContainerProps.defaultProtocol(dockerContainer)
|
||||
var defaultPort = ProtocolProps.getPortForInstall(defaultProtocol)
|
||||
var defaultTransport = ProtocolProps.defaultTransportProto(defaultProtocol)
|
||||
|
||||
// Show loading indicator with message
|
||||
PageController.showBusyIndicator(true)
|
||||
PageController.showNotificationMessage(qsTr("Installing %1 (%2/%3)...")
|
||||
.arg(containerName)
|
||||
.arg(root.currentContainerIndex + 1)
|
||||
.arg(root.containersToInstall.length))
|
||||
|
||||
// Ensure credentials are set
|
||||
console.log("Setting credentials for container installation...")
|
||||
InstallController.setProcessedServerCredentials(root.restoreHostname, root.restoreUsername, root.restoreSecretData)
|
||||
|
||||
// Set server index
|
||||
var serverIdx = ServersModel.getServersCount() - 1
|
||||
ServersModel.processedIndex = serverIdx
|
||||
|
||||
// Install container
|
||||
console.log("Calling InstallController.install for docker container:", dockerContainer)
|
||||
ContainersModel.setProcessedContainerIndex(dockerContainer)
|
||||
InstallController.install(dockerContainer, defaultPort, defaultTransport)
|
||||
}
|
||||
|
||||
// Timer for navigating to restore mode selection page after file selection
|
||||
Timer {
|
||||
id: navigationTimer
|
||||
interval: 500
|
||||
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")
|
||||
|
||||
// Get filename
|
||||
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)
|
||||
|
||||
// Extract IP address from filename
|
||||
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")
|
||||
}
|
||||
|
||||
// Navigate to installation page
|
||||
PageController.goToPage(PageEnum.PageSetupWizardInstalling)
|
||||
|
||||
// Immediately find StackView and navigate to restore page
|
||||
// Server already added, as we waited for onInstallServerFinished
|
||||
Qt.callLater(function() {
|
||||
var pagePath = "qrc:/ui/qml/Pages2/PageSettingsServerRestoreMode.qml"
|
||||
|
||||
// Find main application window
|
||||
var item = root
|
||||
while (item.parent) {
|
||||
item = item.parent
|
||||
}
|
||||
|
||||
// Find StackView recursively
|
||||
function findStackView(obj) {
|
||||
if (!obj) return null
|
||||
|
||||
// Check if object is StackView
|
||||
if (obj.toString().indexOf("StackView") !== -1 || typeof obj.push === "function") {
|
||||
return obj
|
||||
}
|
||||
|
||||
// Check children
|
||||
if (obj.children) {
|
||||
for (var i = 0; i < obj.children.length; i++) {
|
||||
var result = findStackView(obj.children[i])
|
||||
if (result) return result
|
||||
}
|
||||
}
|
||||
|
||||
// Check 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": "", // Will be obtained from ServersModel
|
||||
"serverIp": serverIp,
|
||||
"isFromSetupWizard": true,
|
||||
"wizardHostname": root.restoreHostname,
|
||||
"wizardUsername": root.restoreUsername,
|
||||
"wizardSecretData": root.restoreSecretData
|
||||
})
|
||||
} else {
|
||||
console.error("Could not find StackView")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SortFilterProxyModel {
|
||||
id: proxyContainersModel
|
||||
@@ -148,6 +380,83 @@ 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() {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Save backup file path
|
||||
root.backupFilePath = localPath
|
||||
root.isRestoreFromBackup = true
|
||||
|
||||
// Get credentials from PageSetupWizardCredentials via StackView search
|
||||
var credentialsPage = null
|
||||
var item = root
|
||||
|
||||
// Find StackView
|
||||
while (item && !item.hasOwnProperty("depth")) {
|
||||
item = item.parent
|
||||
}
|
||||
|
||||
// If found StackView, search for PageSetupWizardCredentials in its history
|
||||
if (item && item.depth > 0) {
|
||||
for (var i = 0; i < item.depth; i++) {
|
||||
var page = item.get(i)
|
||||
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)
|
||||
|
||||
// Call C++ method to prepare restore
|
||||
// It will scan backup and send readyForRestore signal
|
||||
ServersBackupController.prepareRestoreFromBackup(localPath, root.restoreHostname, root.restoreUsername, root.restoreSecretData)
|
||||
} else {
|
||||
console.log("WARNING: No credentials found")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: this.clicked()
|
||||
Keys.onReturnPressed: this.clicked()
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
id: continueButton
|
||||
|
||||
@@ -164,6 +473,9 @@ PageType {
|
||||
InstallController.install(listView.dockerContainer,
|
||||
listView.containerDefaultPort,
|
||||
listView.containerDefaultTransportProto)
|
||||
} else if (root.isRestoreFromBackup) {
|
||||
// Restore from backup is handled by restoreFromBackup function
|
||||
return
|
||||
} else {
|
||||
PageController.goToPage(PageEnum.PageSetupWizardProtocols)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user