feat: add seft-hosted server backup

This commit is contained in:
NickVs2015
2026-01-21 12:27:24 +03:00
parent d859b111ca
commit 8eabd549ff
9 changed files with 1556 additions and 1 deletions

View File

@@ -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_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());

View File

@@ -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;

View File

@@ -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");

View File

@@ -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());

View File

@@ -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) {

View File

@@ -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();
};
}

View File

@@ -0,0 +1,826 @@
#include "serversBackupController.h"
#include <QDebug>
#include <QDir>
#include <QRegularExpression>
#include <QJsonDocument>
#include <QStandardPaths>
#ifdef Q_OS_ANDROID
#include <QJniObject>
#endif
#ifdef Q_OS_IOS
#include "platforms/ios/ios_controller.h"
#endif
#include "containers/containers_defs.h"
ServersBackupController::ServersBackupController(std::shared_ptr<Settings> settings, QObject *parent)
: QObject(parent)
, m_settings(settings)
, m_serverController(new ServerController(settings, this))
, m_status(Idle)
, m_backupDir("/var/backups/amnezia")
{
}
ServersBackupController::~ServersBackupController()
{
}
void ServersBackupController::setBackupDirectory(const QString &directory)
{
m_backupDir = directory;
}
void ServersBackupController::createBackup(const ServerCredentials &credentials)
{
if (m_status == InProgress) {
emit errorOccurred("Another operation is in progress", ErrorCode::AmneziaServiceConnectionFailed);
return;
}
setStatus(InProgress);
setProgress(0, tr("Starting backup creation..."));
m_currentOutput.clear();
m_currentError.clear();
// Получаем bash скрипт для backup
QString script = getBackupScript();
// Callback для обработки stdout
auto cbStdOut = [this](const QString &data, libssh::Client &client) -> ErrorCode {
Q_UNUSED(client);
return handleStdOut(data, m_currentOutput);
};
// Callback для обработки stderr
auto cbStdErr = [this](const QString &data, libssh::Client &client) -> ErrorCode {
Q_UNUSED(client);
return handleStdErr(data, m_currentError);
};
// Запускаем скрипт на сервере
ErrorCode error = m_serverController->runHostScript(credentials, script, cbStdOut, cbStdErr);
if (error == ErrorCode::NoError) {
// Парсим имя созданного backup из вывода
QRegularExpression re("backup_(\\d{8}_\\d{6})\\.tar\\.gz");
QRegularExpressionMatch match = re.match(m_currentOutput);
if (match.hasMatch()) {
QString backupFilename = "backup_" + match.captured(1) + ".tar.gz";
setStatus(Success);
setProgress(100, tr("Backup created successfully"));
emit backupCreated(backupFilename);
} else {
setStatus(Failed);
emit errorOccurred(tr("Failed to parse backup filename from output: %1").arg(m_currentOutput.left(200)), ErrorCode::InternalError);
}
} else {
setStatus(Failed);
emit errorOccurred(tr("Failed to create backup: %1").arg(m_currentError), error);
}
}
void ServersBackupController::createBackupByName(const ServerCredentials &credentials, const QString &containerName)
{
DockerContainer container = ContainerProps::containerFromString(containerName);
if (container == DockerContainer::None) {
emit errorOccurred(tr("Unknown container: %1").arg(containerName), ErrorCode::InternalError);
return;
}
createContainerBackup(credentials, container);
}
void ServersBackupController::createContainerBackup(const ServerCredentials &credentials, DockerContainer container)
{
if (m_status == InProgress) {
emit errorOccurred("Another operation is in progress", ErrorCode::AmneziaServiceConnectionFailed);
return;
}
setStatus(InProgress);
QString containerName = ContainerProps::containerToString(container);
setProgress(0, tr("Starting backup for container: %1...").arg(containerName));
m_currentOutput.clear();
m_currentError.clear();
QString script = getContainerBackupScript(container);
auto cbStdOut = [this](const QString &data, libssh::Client &client) -> ErrorCode {
Q_UNUSED(client);
return handleStdOut(data, m_currentOutput);
};
auto cbStdErr = [this](const QString &data, libssh::Client &client) -> ErrorCode {
Q_UNUSED(client);
return handleStdErr(data, m_currentError);
};
ErrorCode error = m_serverController->runHostScript(credentials, script, cbStdOut, cbStdErr);
if (error == ErrorCode::NoError) {
QRegularExpression re("backup_(\\d{8}_\\d{6})\\.tar\\.gz");
QRegularExpressionMatch match = re.match(m_currentOutput);
if (match.hasMatch()) {
QString backupFilename = "backup_" + match.captured(1) + ".tar.gz";
setStatus(Success);
setProgress(100, tr("Container backup created successfully"));
emit backupCreated(backupFilename);
} else {
setStatus(Failed);
emit errorOccurred(tr("Failed to parse backup filename from output: %1").arg(m_currentOutput.left(200)), ErrorCode::InternalError);
}
} else {
setStatus(Failed);
emit errorOccurred(tr("Failed to create container backup: %1").arg(m_currentError), error);
}
}
void ServersBackupController::createContainersBackup(const ServerCredentials &credentials, const QList<DockerContainer> &containers)
{
if (m_status == InProgress) {
emit errorOccurred("Another operation is in progress", ErrorCode::AmneziaServiceConnectionFailed);
return;
}
setStatus(InProgress);
setProgress(0, tr("Starting backup for %1 containers...").arg(containers.size()));
m_currentOutput.clear();
m_currentError.clear();
QString script = getContainersBackupScript(containers);
auto cbStdOut = [this](const QString &data, libssh::Client &client) -> ErrorCode {
Q_UNUSED(client);
return handleStdOut(data, m_currentOutput);
};
auto cbStdErr = [this](const QString &data, libssh::Client &client) -> ErrorCode {
Q_UNUSED(client);
return handleStdErr(data, m_currentError);
};
ErrorCode error = m_serverController->runHostScript(credentials, script, cbStdOut, cbStdErr);
if (error == ErrorCode::NoError) {
QRegularExpression re("backup_(\\d{8}_\\d{6})\\.tar\\.gz");
QRegularExpressionMatch match = re.match(m_currentOutput);
if (match.hasMatch()) {
QString backupFilename = "backup_" + match.captured(1) + ".tar.gz";
setStatus(Success);
setProgress(100, tr("Containers backup created successfully"));
emit backupCreated(backupFilename);
} else {
setStatus(Failed);
emit errorOccurred(tr("Failed to parse backup filename from output: %1").arg(m_currentOutput.left(200)), ErrorCode::InternalError);
}
} else {
setStatus(Failed);
emit errorOccurred(tr("Failed to create containers backup: %1").arg(m_currentError), error);
}
}
void ServersBackupController::fetchBackupList(const ServerCredentials &credentials)
{
if (m_status == InProgress) {
emit errorOccurred("Another operation is in progress", ErrorCode::AmneziaServiceConnectionFailed);
return;
}
setStatus(InProgress);
setProgress(0, tr("Fetching backup list..."));
m_currentOutput.clear();
m_currentError.clear();
QString script = getListBackupsScript();
auto cbStdOut = [this](const QString &data, libssh::Client &client) -> ErrorCode {
Q_UNUSED(client);
return handleStdOut(data, m_currentOutput);
};
auto cbStdErr = [this](const QString &data, libssh::Client &client) -> ErrorCode {
Q_UNUSED(client);
return handleStdErr(data, m_currentError);
};
ErrorCode error = m_serverController->runHostScript(credentials, script, cbStdOut, cbStdErr);
if (error == ErrorCode::NoError) {
QList<BackupInfo> backups = parseBackupList(m_currentOutput);
setStatus(Success);
setProgress(100, tr("Backup list received"));
emit backupListReceived(backups);
} else {
setStatus(Failed);
emit errorOccurred(tr("Failed to fetch backup list: %1").arg(m_currentError), error);
}
}
void ServersBackupController::restoreBackup(const ServerCredentials &credentials,
const QString &backupFilename,
const QStringList &containers)
{
if (m_status == InProgress) {
emit errorOccurred("Another operation is in progress", ErrorCode::AmneziaServiceConnectionFailed);
return;
}
setStatus(InProgress);
setProgress(0, tr("Starting restore from %1...").arg(backupFilename));
m_currentOutput.clear();
m_currentError.clear();
QString script = getRestoreScript(backupFilename, containers);
auto cbStdOut = [this](const QString &data, libssh::Client &client) -> ErrorCode {
Q_UNUSED(client);
return handleStdOut(data, m_currentOutput);
};
auto cbStdErr = [this](const QString &data, libssh::Client &client) -> ErrorCode {
Q_UNUSED(client);
return handleStdErr(data, m_currentError);
};
ErrorCode error = m_serverController->runHostScript(credentials, script, cbStdOut, cbStdErr);
if (error == ErrorCode::NoError) {
setStatus(Success);
setProgress(100, tr("Backup restored successfully"));
emit backupRestored();
} else {
setStatus(Failed);
emit errorOccurred(tr("Failed to restore backup: %1").arg(m_currentError), error);
}
}
void ServersBackupController::checkBackupStatus(const ServerCredentials &credentials)
{
if (m_status == InProgress) {
emit errorOccurred("Another operation is in progress", ErrorCode::AmneziaServiceConnectionFailed);
return;
}
setStatus(InProgress);
setProgress(0, tr("Checking backup status..."));
m_currentOutput.clear();
m_currentError.clear();
QString script = getCheckStatusScript();
auto cbStdOut = [this](const QString &data, libssh::Client &client) -> ErrorCode {
Q_UNUSED(client);
return handleStdOut(data, m_currentOutput);
};
auto cbStdErr = [this](const QString &data, libssh::Client &client) -> ErrorCode {
Q_UNUSED(client);
return handleStdErr(data, m_currentError);
};
ErrorCode error = m_serverController->runHostScript(credentials, script, cbStdOut, cbStdErr);
if (error == ErrorCode::NoError) {
QJsonObject status = parseBackupStatus(m_currentOutput);
setStatus(Success);
setProgress(100, tr("Status received"));
emit backupStatusReceived(status);
} else {
setStatus(Failed);
emit errorOccurred(tr("Failed to check backup status: %1").arg(m_currentError), error);
}
}
void ServersBackupController::downloadBackup(const ServerCredentials &credentials,
const QString &backupFilename,
const QString &localPath)
{
if (m_status == InProgress) {
emit errorOccurred("Another operation is in progress", ErrorCode::AmneziaServiceConnectionFailed);
return;
}
setStatus(InProgress);
setProgress(0, tr("Downloading backup..."));
// Validate backup filename
if (backupFilename.isEmpty()) {
setStatus(Failed);
emit errorOccurred(tr("Backup filename is empty"), ErrorCode::InternalError);
return;
}
// Construct remote file path
QString remotePath = QString("%1/%2").arg(m_backupDir, backupFilename);
// Determine actual local path
QString actualLocalPath = localPath;
QFileInfo pathInfo(localPath);
// If only filename provided (no directory), use appropriate folder
if (!pathInfo.isAbsolute() || pathInfo.dir().path() == ".") {
#ifdef Q_OS_ANDROID
// На Android используем публичную папку Download (через JNI)
QJniObject mediaDir = QJniObject::callStaticObjectMethod(
"android/os/Environment",
"getExternalStoragePublicDirectory",
"(Ljava/lang/String;)Ljava/io/File;",
QJniObject::getStaticObjectField("android/os/Environment", "DIRECTORY_DOWNLOADS", "Ljava/lang/String;").object());
QString downloadsPath = mediaDir.callObjectMethod("getAbsolutePath", "()Ljava/lang/String;").toString();
actualLocalPath = QDir(downloadsPath).filePath(backupFilename);
#elif defined(Q_OS_IOS)
QString tempPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation);
actualLocalPath = QDir(tempPath).filePath(backupFilename);
#else
// На Desktop используем Documents (как обычный backup)
QString documentsPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
if (documentsPath.isEmpty()) {
documentsPath = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
}
actualLocalPath = QDir(documentsPath).filePath(backupFilename);
#endif
}
// Ensure local directory exists
QFileInfo localFileInfo(actualLocalPath);
QDir localDir = localFileInfo.dir();
if (!localDir.exists()) {
if (!localDir.mkpath(".")) {
setStatus(Failed);
emit errorOccurred(tr("Failed to create local directory: %1").arg(localDir.path()), ErrorCode::InternalError);
return;
}
}
setProgress(25, tr("Starting file transfer..."));
ErrorCode error = m_serverController->downloadFileFromHost(credentials, remotePath, actualLocalPath);
if (error == ErrorCode::NoError) {
// qDebug() << "Backup downloaded to:" << actualLocalPath;
#ifdef Q_OS_IOS
QStringList filesToShare;
filesToShare.append(actualLocalPath);
IosController::Instance()->shareText(filesToShare);
#endif
setStatus(Success);
setProgress(100, tr("Backup downloaded successfully"));
emit backupDownloaded(actualLocalPath);
} else {
setStatus(Failed);
emit errorOccurred(tr("Failed to download backup: error code %1").arg(static_cast<int>(error)), error);
}
}
void ServersBackupController::uploadBackup(const ServerCredentials &credentials,
const QString &localPath)
{
if (m_status == InProgress) {
emit errorOccurred("Another operation is in progress", ErrorCode::AmneziaServiceConnectionFailed);
return;
}
// Check if local file exists
QFileInfo localFileInfo(localPath);
if (!localFileInfo.exists()) {
emit errorOccurred(tr("Local file does not exist: %1").arg(localPath), ErrorCode::InternalError);
return;
}
if (!localFileInfo.isFile()) {
emit errorOccurred(tr("Path is not a file: %1").arg(localPath), ErrorCode::InternalError);
return;
}
setStatus(InProgress);
setProgress(0, tr("Uploading backup..."));
// Construct remote file path with filename from local path
QString filename = localFileInfo.fileName();
QString remotePath = QString("%1/%2").arg(m_backupDir, filename);
setProgress(25, tr("Starting file transfer..."));
ErrorCode error = m_serverController->uploadFileToHostPublic(credentials, localPath, remotePath,
libssh::ScpOverwriteMode::ScpOverwriteExisting);
if (error == ErrorCode::NoError) {
setStatus(Success);
setProgress(100, tr("Backup uploaded successfully"));
emit backupUploaded(remotePath);
} else {
setStatus(Failed);
emit errorOccurred(tr("Failed to upload backup: error code %1").arg(static_cast<int>(error)), error);
}
}
void ServersBackupController::deleteBackup(const ServerCredentials &credentials,
const QString &backupFilename)
{
if (m_status == InProgress) {
emit errorOccurred("Another operation is in progress", ErrorCode::AmneziaServiceConnectionFailed);
return;
}
setStatus(InProgress);
setProgress(0, tr("Deleting backup..."));
QString script = QString("sudo rm -f %1/%2").arg(m_backupDir).arg(backupFilename);
m_currentOutput.clear();
m_currentError.clear();
auto cbStdOut = [this](const QString &data, libssh::Client &client) -> ErrorCode {
Q_UNUSED(client);
return handleStdOut(data, m_currentOutput);
};
auto cbStdErr = [this](const QString &data, libssh::Client &client) -> ErrorCode {
Q_UNUSED(client);
return handleStdErr(data, m_currentError);
};
ErrorCode error = m_serverController->runHostScript(credentials, script, cbStdOut, cbStdErr);
if (error == ErrorCode::NoError) {
setStatus(Success);
setProgress(100, tr("Backup deleted"));
} else {
setStatus(Failed);
emit errorOccurred(tr("Failed to delete backup: %1").arg(m_currentError), error);
}
}
// ============================================================================
// ВСТРОЕННЫЕ BASH СКРИПТЫ
// ============================================================================
QString ServersBackupController::getBackupScript() const
{
// Упрощенная версия bash скрипта, встроенная в C++
return QString(R"(
#!/bin/bash
set -e
BACKUP_DIR=%1
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_SUBDIR="$BACKUP_DIR/backup_$TIMESTAMP"
echo "[INFO] Starting backup..."
# Создание директории
mkdir -p "$BACKUP_SUBDIR"
# Список контейнеров Amnezia
CONTAINERS=(
"amnezia-awg"
"amnezia-awg2"
"amnezia-openvpn"
"amnezia-xray"
"amnezia-wireguard"
"amnezia-ipsec"
"amnezia-cloak"
"amnezia-shadowsocks"
)
# Backup каждого контейнера (включая остановленные)
for container in "${CONTAINERS[@]}"; do
if sudo docker ps -a --format '{{.Names}}' | grep -q "^$container$"; then
echo "[INFO] Backing up $container..."
mkdir -p "$BACKUP_SUBDIR/$container"
# Копируем /opt/amnezia
sudo docker cp "$container:/opt/amnezia" "$BACKUP_SUBDIR/$container/" 2>/dev/null || true
# Сохраняем метаданные
sudo docker inspect "$container" > "$BACKUP_SUBDIR/$container/container_inspect.json" 2>/dev/null || true
fi
done
# Создание архива
cd "$BACKUP_DIR"
tar -czf "backup_$TIMESTAMP.tar.gz" "backup_$TIMESTAMP" 2>/dev/null
rm -rf "$BACKUP_SUBDIR"
echo "[INFO] Backup created: backup_$TIMESTAMP.tar.gz"
)").arg(m_backupDir);
}
QString ServersBackupController::getContainerBackupScript(DockerContainer container) const
{
QString containerName = ContainerProps::containerToString(container);
// Backup конкретного контейнера напрямую через docker cp
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"
echo "[INFO] Starting backup for container: $CONTAINER_NAME..."
# Проверка существования контейнера
if ! sudo docker ps -a --format '{{.Names}}' | grep -q "^$CONTAINER_NAME$"; then
echo "[ERROR] Container $CONTAINER_NAME does not exist"
exit 1
fi
# Создание директории
mkdir -p "$BACKUP_SUBDIR/$CONTAINER_NAME"
# Backup конфигураций из контейнера напрямую
echo "[INFO] Copying /opt/amnezia from container..."
sudo docker cp "$CONTAINER_NAME:/opt/amnezia" "$BACKUP_SUBDIR/$CONTAINER_NAME/" 2>/dev/null || {
echo "[WARN] Failed to copy /opt/amnezia, trying alternative paths..."
# Альтернативные пути для разных типов контейнеров
sudo docker cp "$CONTAINER_NAME:/etc/openvpn" "$BACKUP_SUBDIR/$CONTAINER_NAME/" 2>/dev/null || true
sudo docker cp "$CONTAINER_NAME:/etc/wireguard" "$BACKUP_SUBDIR/$CONTAINER_NAME/" 2>/dev/null || true
sudo docker cp "$CONTAINER_NAME:/etc/ipsec.d" "$BACKUP_SUBDIR/$CONTAINER_NAME/" 2>/dev/null || true
sudo docker cp "$CONTAINER_NAME:/etc/xray" "$BACKUP_SUBDIR/$CONTAINER_NAME/" 2>/dev/null || true
}
# Сохранение метаданных контейнера
echo "[INFO] Saving container metadata..."
sudo docker inspect "$CONTAINER_NAME" > "$BACKUP_SUBDIR/$CONTAINER_NAME/container_inspect.json" 2>/dev/null || true
# Сохранение конфигурации сети
sudo docker network inspect $(sudo docker inspect -f '{{range $k, $v := .NetworkSettings.Networks}}{{$k}} {{end}}' "$CONTAINER_NAME" | awk '{print $1}') \
> "$BACKUP_SUBDIR/$CONTAINER_NAME/network_config.json" 2>/dev/null || true
# Создание архива
cd "$BACKUP_DIR"
tar -czf "backup_$TIMESTAMP.tar.gz" "backup_$TIMESTAMP" 2>/dev/null
rm -rf "$BACKUP_SUBDIR"
echo "[INFO] Backup created: backup_$TIMESTAMP.tar.gz"
)").arg(m_backupDir).arg(containerName);
}
QString ServersBackupController::getContainersBackupScript(const QList<DockerContainer> &containers) const
{
QString containersList;
for (const DockerContainer &container : containers) {
QString containerName = ContainerProps::containerToString(container);
containersList += QString("\"%1\" ").arg(containerName);
}
return QString(R"(
#!/bin/bash
set -e
BACKUP_DIR=%1
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_SUBDIR="$BACKUP_DIR/backup_$TIMESTAMP"
echo "[INFO] Starting backup for containers..."
# Создание директории
mkdir -p "$BACKUP_SUBDIR"
# Список контейнеров для backup
CONTAINERS=(%2)
# Backup каждого контейнера
for container in "${CONTAINERS[@]}"; do
if sudo docker ps -a --format '{{.Names}}' | grep -q "^$container$"; then
echo "[INFO] Backing up $container..."
mkdir -p "$BACKUP_SUBDIR/$container"
# Копируем /opt/amnezia напрямую из контейнера
sudo docker cp "$container:/opt/amnezia" "$BACKUP_SUBDIR/$container/" 2>/dev/null || {
echo "[WARN] Failed to copy /opt/amnezia from $container, trying alternative paths..."
sudo docker cp "$container:/etc/openvpn" "$BACKUP_SUBDIR/$container/" 2>/dev/null || true
sudo docker cp "$container:/etc/wireguard" "$BACKUP_SUBDIR/$container/" 2>/dev/null || true
sudo docker cp "$container:/etc/ipsec.d" "$BACKUP_SUBDIR/$container/" 2>/dev/null || true
sudo docker cp "$container:/etc/xray" "$BACKUP_SUBDIR/$container/" 2>/dev/null || true
}
# Сохраняем метаданные
sudo docker inspect "$container" > "$BACKUP_SUBDIR/$container/container_inspect.json" 2>/dev/null || true
else
echo "[WARN] Container $container does not exist, skipping..."
fi
done
# Создание архива
cd "$BACKUP_DIR"
tar -czf "backup_$TIMESTAMP.tar.gz" "backup_$TIMESTAMP" 2>/dev/null
rm -rf "$BACKUP_SUBDIR"
echo "[INFO] Backup created: backup_$TIMESTAMP.tar.gz"
)").arg(m_backupDir).arg(containersList.trimmed());
}
QString ServersBackupController::getRestoreScript(const QString &backupFilename,
const QStringList &containers) const
{
Q_UNUSED(containers); // TODO: Использовать для выборочного восстановления
return QString(R"(
#!/bin/bash
set -e
BACKUP_DIR=%1
BACKUP_FILE=%2
TEMP_DIR="/tmp/amnezia_restore_$$"
echo "[INFO] Starting restore from $BACKUP_FILE..."
# Извлечение backup
mkdir -p "$TEMP_DIR"
tar -xzf "$BACKUP_DIR/$BACKUP_FILE" -C "$TEMP_DIR" 2>/dev/null
BACKUP_SUBDIR=$(ls -d "$TEMP_DIR"/backup_* 2>/dev/null | head -1)
if [ -z "$BACKUP_SUBDIR" ]; then
echo "[ERROR] Failed to extract backup"
exit 1
fi
# Восстановление каждого контейнера
for container_dir in "$BACKUP_SUBDIR"/*; do
if [ ! -d "$container_dir" ]; then
continue
fi
container_name=$(basename "$container_dir")
if sudo docker ps -a --format '{{.Names}}' | grep -q "^$container_name$"; then
echo "[INFO] Restoring $container_name..."
# Остановка контейнера
sudo docker stop "$container_name" 2>/dev/null || true
# Восстановление /opt/amnezia
if [ -d "$container_dir/amnezia" ]; then
sudo docker cp "$container_dir/amnezia" "$container_name:/opt/" 2>/dev/null || true
fi
# Запуск контейнера
sudo docker start "$container_name" 2>/dev/null || true
echo "[INFO] $container_name restored"
fi
done
# Очистка
rm -rf "$TEMP_DIR"
echo "[INFO] Restore completed successfully"
)").arg(m_backupDir).arg(backupFilename);
}
QString ServersBackupController::getCheckStatusScript() const
{
return QString(R"SCRIPT(
#!/bin/bash
BACKUP_DIR=%1
echo "Backup directory: $BACKUP_DIR"
# Проверка наличия backup
BACKUPS=$(ls -t "$BACKUP_DIR"/backup_*.tar.gz 2>/dev/null | wc -l)
echo "Total backups: $BACKUPS"
if [ "$BACKUPS" -gt 0 ]; then
# Информация о последнем backup
LATEST=$(ls -t "$BACKUP_DIR"/backup_*.tar.gz 2>/dev/null | head -1)
if [ -n "$LATEST" ]; then
echo "Latest backup: $(basename "$LATEST")"
echo "Size: $(du -h "$LATEST" | cut -f1)"
echo "Modified: $(stat -c %y "$LATEST" 2>/dev/null | cut -d'.' -f1)"
fi
fi
# Проверка запущенных контейнеров
echo "Running containers:"
sudo docker ps --filter "name=amnezia-" --format '{{.Names}}' 2>/dev/null
echo "Status check completed"
)SCRIPT").arg(m_backupDir);
}
QString ServersBackupController::getListBackupsScript() const
{
return QString(R"(
#!/bin/bash
BACKUP_DIR=%1
ls -lht "$BACKUP_DIR"/backup_*.tar.gz 2>/dev/null || echo "No backups found"
)").arg(m_backupDir);
}
// ============================================================================
// ПАРСИНГ ВЫВОДА
// ============================================================================
QList<ServersBackupController::BackupInfo> ServersBackupController::parseBackupList(const QString &output)
{
QList<BackupInfo> backups;
QStringList lines = output.split('\n', Qt::SkipEmptyParts);
// Парсим вывод ls -lht
QRegularExpression re("^-.*\\s+(\\d+)\\s+\\w+\\s+\\d+\\s+([\\d:]+)\\s+(.+backup_(\\d{8}_\\d{6})\\.tar\\.gz)$");
for (const QString &line : lines) {
QRegularExpressionMatch match = re.match(line);
if (match.hasMatch()) {
BackupInfo info;
info.size = match.captured(1).toLongLong();
info.filename = QFileInfo(match.captured(3)).fileName();
info.fullPath = match.captured(3);
// Парсим дату из имени файла
QString dateStr = match.captured(4);
info.createdAt = QDateTime::fromString(dateStr, "yyyyMMdd_HHmmss");
info.isValid = true;
backups.append(info);
}
}
return backups;
}
QJsonObject ServersBackupController::parseBackupStatus(const QString &output)
{
QJsonObject status;
// Парсим текстовый вывод
status["raw_output"] = output;
status["has_backups"] = output.contains("Total backups:");
// Извлекаем количество backup
QRegularExpression reTotal("Total backups: (\\d+)");
QRegularExpressionMatch matchTotal = reTotal.match(output);
if (matchTotal.hasMatch()) {
status["total_backups"] = matchTotal.captured(1).toInt();
}
// Извлекаем информацию о последнем backup
QRegularExpression reLatest("Latest backup: (.+)");
QRegularExpressionMatch matchLatest = reLatest.match(output);
if (matchLatest.hasMatch()) {
status["latest_backup"] = matchLatest.captured(1);
}
return status;
}
// ============================================================================
// ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ
// ============================================================================
ErrorCode ServersBackupController::handleStdOut(const QString &data, QString &output)
{
output += data;
qDebug().noquote() << "[BACKUP]" << data;
// Обновляем прогресс на основе вывода
if (data.contains("Starting backup")) {
setProgress(10, tr("Starting backup..."));
} else if (data.contains("Backing up")) {
setProgress(50, tr("Backing up containers..."));
} else if (data.contains("Backup created")) {
setProgress(90, tr("Finalizing..."));
}
return ErrorCode::NoError;
}
ErrorCode ServersBackupController::handleStdErr(const QString &data, QString &error)
{
error += data;
qDebug().noquote() << "[BACKUP ERROR]" << data;
return ErrorCode::NoError;
}
void ServersBackupController::setStatus(BackupStatus status)
{
if (m_status != status) {
m_status = status;
emit statusChanged(status);
}
}
void ServersBackupController::setProgress(int percent, const QString &message)
{
emit progressChanged(percent, message);
}

View File

@@ -0,0 +1,259 @@
#ifndef SERVERSBACKUPCONTROLLER_H
#define SERVERSBACKUPCONTROLLER_H
#include <QObject>
#include <QString>
#include <QDateTime>
#include <QJsonObject>
#include <QJsonArray>
#include <QFileInfo>
#include "core/controllers/serverController.h"
#include "core/defs.h"
#include "containers/containers_defs.h"
using namespace amnezia;
/**
* @brief Контроллер для управления backup конфигураций Amnezia VPN
*
* Использует существующий ServerController и libssh::Client из Amnezia
* Bash скрипты встроены напрямую в C++ код
* Поддерживает backup конкретных контейнеров напрямую через docker cp
*
* Полностью кроссплатформенный: Windows, macOS, Linux, iOS, Android
*/
class ServersBackupController : public QObject
{
Q_OBJECT
public:
explicit ServersBackupController(std::shared_ptr<Settings> settings, QObject *parent = nullptr);
~ServersBackupController();
/**
* @brief Информация о backup
*/
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 Создать backup на сервере (всех контейнеров)
* @param credentials Учетные данные сервера
*/
void createBackup(const ServerCredentials &credentials);
/**
* @brief Создать backup конкретного контейнера
* @param credentials Учетные данные сервера
* @param container Тип контейнера для backup
*/
void createContainerBackup(const ServerCredentials &credentials, DockerContainer container);
/**
* @brief Создать backup конкретного контейнера по имени
* @param credentials Учетные данные сервера
* @param containerName Имя контейнера (например "amnezia-awg")
*/
void createBackupByName(const ServerCredentials &credentials, const QString &containerName);
/**
* @brief Создать backup нескольких контейнеров
* @param credentials Учетные данные сервера
* @param containers Список контейнеров для backup
*/
void createContainersBackup(const ServerCredentials &credentials, const QList<DockerContainer> &containers);
/**
* @brief Получить список backup с сервера
* @param credentials Учетные данные сервера
*/
void fetchBackupList(const ServerCredentials &credentials);
/**
* @brief Восстановить из backup
* @param credentials Учетные данные сервера
* @param backupFilename Имя файла backup
* @param containers Список контейнеров (пустой = все)
*/
void restoreBackup(const ServerCredentials &credentials,
const QString &backupFilename,
const QStringList &containers = QStringList());
/**
* @brief Проверить состояние backup на сервере
* @param credentials Учетные данные сервера
*/
void checkBackupStatus(const ServerCredentials &credentials);
/**
* @brief Скачать backup на локальную машину
* @param credentials Учетные данные сервера
* @param backupFilename Имя файла backup
* @param localPath Путь для сохранения
*/
void downloadBackup(const ServerCredentials &credentials,
const QString &backupFilename,
const QString &localPath);
/**
* @brief Загрузить backup на сервер
* @param credentials Учетные данные сервера
* @param localPath Путь к локальному файлу
*/
void uploadBackup(const ServerCredentials &credentials,
const QString &localPath);
/**
* @brief Удалить backup с сервера
* @param credentials Учетные данные сервера
* @param backupFilename Имя файла backup
*/
void deleteBackup(const ServerCredentials &credentials,
const QString &backupFilename);
/**
* @brief Установить директорию backup на сервере
*/
void setBackupDirectory(const QString &directory);
/**
* @brief Получить директорию backup
*/
QString backupDirectory() const { return m_backupDir; }
signals:
/**
* @brief Изменился статус операции
*/
void statusChanged(BackupStatus status);
/**
* @brief Прогресс операции (0-100)
*/
void progressChanged(int percent, const QString &message);
/**
* @brief Получен список backup
*/
void backupListReceived(const QList<BackupInfo> &backups);
/**
* @brief Backup создан успешно
*/
void backupCreated(const QString &backupFilename);
/**
* @brief Backup восстановлен успешно
*/
void backupRestored();
/**
* @brief Backup скачан
*/
void backupDownloaded(const QString &localPath);
/**
* @brief Backup загружен на сервер
*/
void backupUploaded(const QString &serverPath);
/**
* @brief Получена информация о состоянии backup
*/
void backupStatusReceived(const QJsonObject &status);
/**
* @brief Произошла ошибка
*/
void errorOccurred(const QString &errorMessage, ErrorCode errorCode);
private:
/**
* @brief Получить bash скрипт для создания backup всех контейнеров
*/
QString getBackupScript() const;
/**
* @brief Получить bash скрипт для создания backup конкретного контейнера
* @param container Тип контейнера
*/
QString getContainerBackupScript(DockerContainer container) const;
/**
* @brief Получить bash скрипт для создания backup нескольких контейнеров
* @param containers Список контейнеров
*/
QString getContainersBackupScript(const QList<DockerContainer> &containers) const;
/**
* @brief Получить bash скрипт для восстановления
*/
QString getRestoreScript(const QString &backupFilename, const QStringList &containers) const;
/**
* @brief Получить bash скрипт для проверки состояния
*/
QString getCheckStatusScript() const;
/**
* @brief Получить bash скрипт для списка backup
*/
QString getListBackupsScript() const;
/**
* @brief Парсить список backup из вывода
*/
QList<BackupInfo> parseBackupList(const QString &output);
/**
* @brief Парсить статус из вывода
*/
QJsonObject parseBackupStatus(const QString &output);
/**
* @brief Обработать стандартный вывод
*/
ErrorCode handleStdOut(const QString &data, QString &output);
/**
* @brief Обработать вывод ошибок
*/
ErrorCode handleStdErr(const QString &data, QString &error);
/**
* @brief Установить статус
*/
void setStatus(BackupStatus status);
/**
* @brief Установить прогресс
*/
void setProgress(int percent, const QString &message);
private:
std::shared_ptr<Settings> m_settings;
ServerController *m_serverController;
BackupStatus m_status;
QString m_backupDir;
QString m_currentOutput;
QString m_currentError;
};
#endif // SERVERSBACKUPCONTROLLER_H

View File

@@ -19,6 +19,7 @@ PageType {
signal lastItemTabClickedSignal()
property bool isServerWithWriteAccess: ServersModel.isProcessedServerHasWriteAccess()
property string selectedContainerValue: "all"
Connections {
target: InstallController
@@ -75,10 +76,148 @@ 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
visible: isVisible && !isBackupSection
text: title
descriptionText: description
@@ -97,6 +236,7 @@ PageType {
property list<QtObject> serverActions: [
check,
backupSection,
reboot,
remove,
clear,
@@ -108,6 +248,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 +259,22 @@ PageType {
}
}
QtObject {
id: backupSection
property bool isVisible: root.isServerWithWriteAccess
readonly property bool isBackupSection: true
readonly property string title: ""
readonly property string description: ""
readonly property var tColor: AmneziaStyle.color.paleGray
readonly property var clickedHandler: function() {}
}
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 +305,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 +336,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 +366,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 +397,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 +406,171 @@ PageType {
ApiPremV1MigrationController.showMigrationDrawer()
}
}
// ============ Backup Functions ============
function getServerCredentials() {
var index = ServersModel.processedIndex
return ServersModel.getServerCredentials(index)
}
function getSelectedContainerValue() {
return root.selectedContainerValue
}
property bool downloadAfterCreate: false
function createBackup(shouldDownload) {
downloadAfterCreate = shouldDownload || false
var headerText = shouldDownload ?
qsTr("Create backup and download to device?") :
qsTr("Create server configuration backup?")
var descriptionText = shouldDownload ?
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()
var containerValue = getSelectedContainerValue()
if (containerValue === "all") {
ServersBackupController.createBackup(credentials)
} else {
ServersBackupController.createBackupByName(credentials, containerValue)
}
}
var noButtonFunction = function() {}
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
}
property string selectedBackupForRestore: ""
function restoreBackup() {
// Шаг 1: Выбрать backup файл на устройстве
// На Android используем "*.gz" для MIME типа application/gzip
// На Desktop используем "Backup files (*.tar.gz *.backup)"
var filter = GC.isMobile() ? "*.gz" : "Backup files (*.tar.gz *.backup)"
var localPath = SystemController.getFileName(
qsTr("Select Backup to Restore"),
filter,
"",
false, // openMode
""
)
if (!localPath || localPath.length === 0) {
return // Пользователь отменил
}
selectedBackupForRestore = localPath
// Шаг 2: Подтверждение восстановления
var headerText = qsTr("Restore server configuration?")
var descriptionText = qsTr("Selected backup will be uploaded to server and restored. Current configuration will be overwritten. All containers will be restarted.")
var yesButtonText = qsTr("Restore")
var noButtonText = qsTr("Cancel")
var yesButtonFunction = function() {
PageController.showBusyIndicator(true)
var credentials = getServerCredentials()
// Загрузить backup на сервер
ServersBackupController.uploadBackup(credentials, selectedBackupForRestore)
}
var noButtonFunction = function() {
selectedBackupForRestore = ""
}
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
}
function manageBackups() {
PageController.showNotificationMessage(qsTr("Backup management page will be available in the next update"))
}
// ============ Backup Controller Connections ============
property string lastCreatedBackupFilename: ""
property string lastUploadedBackupFilename: ""
Connections {
target: ServersBackupController
function onBackupCreated(backupFilename) {
lastCreatedBackupFilename = backupFilename
if (downloadAfterCreate) {
// Скачать backup на устройство
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)
// Удалить backup с сервера после успешного скачивания
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) {
// Backup загружен на сервер, теперь восстановить
PageController.showNotificationMessage(qsTr("Backup uploaded. Restoring configuration..."))
// Извлечь имя файла из пути и сохранить для последующего удаления
var backupFilename = serverPath.split('/').pop()
lastUploadedBackupFilename = backupFilename
var credentials = getServerCredentials()
ServersBackupController.restoreBackup(credentials, backupFilename)
}
function onBackupRestored() {
PageController.showBusyIndicator(false)
// Удалить backup с сервера после успешного восстановления
/* if (lastUploadedBackupFilename && lastUploadedBackupFilename.length > 0) {
var credentials = getServerCredentials()
ServersBackupController.deleteBackup(credentials, lastUploadedBackupFilename)
console.log("Deleting backup from server after restore:", lastUploadedBackupFilename)
lastUploadedBackupFilename = ""
}*/
selectedBackupForRestore = ""
PageController.showNotificationMessage(qsTr("Backup restored successfully! Containers are restarting..."))
}
function onProgressChanged(percent, message) {
// TODO: Show progress in UI
console.log("Backup progress:", percent, "%", message)
}
function onErrorOccurred(errorMessage, errorCode) {
PageController.showBusyIndicator(false)
PageController.showErrorMessage(qsTr("Backup error: %1").arg(errorMessage))
}
}
}