Files
amnezia-client/client/ui/controllers/serversBackupController.cpp
T

1573 lines
57 KiB
C++
Raw Normal View History

2026-01-21 12:27:24 +03:00
#include "serversBackupController.h"
#include "secureQSettings.h"
#include "core/utils/utilities.h"
2026-01-21 12:27:24 +03:00
#include <QDebug>
#include <QDir>
#include <QRegularExpression>
#include <QJsonDocument>
#include <QStandardPaths>
2026-02-04 23:03:38 +03:00
#include <QTemporaryFile>
#include <QUrl>
2026-02-11 12:12:10 +03:00
#if !defined(Q_OS_IOS)
2026-02-05 10:29:18 +03:00
#include <QProcess>
2026-02-11 12:12:10 +03:00
#endif
2026-02-05 10:29:18 +03:00
#include <QSet>
2026-02-06 17:14:08 +03:00
#include <QTimer>
2026-01-21 12:27:24 +03:00
#ifdef Q_OS_ANDROID
#include <QJniObject>
2026-02-04 23:03:38 +03:00
#include "platforms/android/android_controller.h"
2026-01-21 12:27:24 +03:00
#endif
#ifdef Q_OS_IOS
#include "platforms/ios/ios_controller.h"
#endif
#include "core/utils/networkUtilities.h"
2026-02-04 23:03:38 +03:00
#include "systemController.h"
#include "ui/models/serversModel.h"
#include "ui/models/containersModel.h"
#include "ui/controllers/serversUiController.h"
#include "core/controllers/serversController.h"
ServersBackupController::ServersBackupController(SecureQSettings *settings, ServersModel *serversModel,
ServersUiController *serversUiController, ServersController *serversController,
QObject *parent)
2026-01-21 12:27:24 +03:00
: QObject(parent)
, m_settings(settings)
2026-02-06 17:14:08 +03:00
, m_serversModel(serversModel)
, m_serversUiController(serversUiController)
, m_serversController(serversController)
2026-01-21 12:27:24 +03:00
, m_status(Idle)
, m_backupDir("/var/backups/amnezia")
2026-02-04 23:03:38 +03:00
, m_restoreReplaceMode(false)
, m_tempUploadFile(nullptr)
2026-02-06 17:14:08 +03:00
, m_containerRetryCount(0)
, m_autoRestoreAfterUpload(false)
, m_autoDownloadAfterCreate(false)
, m_autoDeleteAfterDownload(false)
2026-01-21 12:27:24 +03:00
{
}
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;
}
m_sshSession.resetConnection();
2026-01-21 12:27:24 +03:00
setStatus(InProgress);
setProgress(0, tr("Starting backup creation..."));
m_currentOutput.clear();
m_currentError.clear();
2026-02-06 17:14:08 +03:00
// Get server IP address
2026-02-04 23:03:38 +03:00
QString serverIp = NetworkUtilities::getIPAddress(credentials.hostName);
if (serverIp.isEmpty()) {
serverIp = credentials.hostName;
}
2026-02-06 17:14:08 +03:00
// Format IP: replace dots with underscores
2026-02-04 23:03:38 +03:00
QString ipFormatted = serverIp;
ipFormatted.replace(".", "_");
2026-02-06 17:14:08 +03:00
// Get bash script for backup with IP address
2026-02-04 23:03:38 +03:00
QString script = getBackupScript(ipFormatted);
2026-01-21 12:27:24 +03:00
2026-02-06 17:14:08 +03:00
// Callback for handling stdout
2026-01-21 12:27:24 +03:00
auto cbStdOut = [this](const QString &data, libssh::Client &client) -> ErrorCode {
Q_UNUSED(client);
return handleStdOut(data, m_currentOutput);
};
2026-02-06 17:14:08 +03:00
// Callback for handling stderr
2026-01-21 12:27:24 +03:00
auto cbStdErr = [this](const QString &data, libssh::Client &client) -> ErrorCode {
Q_UNUSED(client);
return handleStdErr(data, m_currentError);
};
2026-02-06 17:14:08 +03:00
// Run script on server
ErrorCode error = runHostScript(credentials, script, cbStdOut, cbStdErr);
2026-01-21 12:27:24 +03:00
if (error == ErrorCode::NoError) {
2026-02-06 17:14:08 +03:00
// Parse created backup name from output: format "IP_ADDRESS - DD-MM-YYYY_HH-MM-SS.tgz"
2026-02-04 23:03:38 +03:00
QRegularExpression re("([\\d_]+)\\s+-\\s+(\\d{2}-\\d{2}-\\d{4})_(\\d{2}-\\d{2}-\\d{2})\\.tgz");
2026-01-21 12:27:24 +03:00
QRegularExpressionMatch match = re.match(m_currentOutput);
if (match.hasMatch()) {
2026-02-04 23:03:38 +03:00
QString backupFilename = match.captured(1) + " - " + match.captured(2) + "_" + match.captured(3) + ".tgz";
2026-01-21 12:27:24 +03:00
setStatus(Success);
setProgress(100, tr("Backup created successfully"));
2026-02-06 17:14:08 +03:00
// Save filename for later deletion
m_lastCreatedBackupFilename = backupFilename;
// If auto-download enabled, start it
if (m_autoDownloadAfterCreate) {
qDebug() << "Auto-downloading backup after creation";
m_autoDownloadAfterCreate = false; // Reset flag
downloadBackup(credentials, backupFilename, backupFilename);
} else {
emit backupCreated(backupFilename);
}
2026-01-21 12:27:24 +03:00
} 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 = ContainerUtils::containerFromString(containerName);
2026-01-21 12:27:24 +03:00
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 = ContainerUtils::containerToString(container);
2026-01-21 12:27:24 +03:00
setProgress(0, tr("Starting backup for container: %1...").arg(containerName));
m_currentOutput.clear();
m_currentError.clear();
2026-02-06 17:14:08 +03:00
// Get server IP address
2026-02-04 23:03:38 +03:00
QString serverIp = NetworkUtilities::getIPAddress(credentials.hostName);
if (serverIp.isEmpty()) {
serverIp = credentials.hostName;
}
2026-02-06 17:14:08 +03:00
// Format IP: replace dots with underscores
2026-02-04 23:03:38 +03:00
QString ipFormatted = serverIp;
ipFormatted.replace(".", "_");
QString script = getContainerBackupScript(container, ipFormatted);
2026-01-21 12:27:24 +03:00
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 = runHostScript(credentials, script, cbStdOut, cbStdErr);
2026-01-21 12:27:24 +03:00
if (error == ErrorCode::NoError) {
2026-02-06 17:14:08 +03:00
// Parse created backup name from output: format "IP_ADDRESS - DD-MM-YYYY_HH-MM-SS.tgz"
2026-02-04 23:03:38 +03:00
QRegularExpression re("([\\d_]+)\\s+-\\s+(\\d{2}-\\d{2}-\\d{4})_(\\d{2}-\\d{2}-\\d{2})\\.tgz");
2026-01-21 12:27:24 +03:00
QRegularExpressionMatch match = re.match(m_currentOutput);
if (match.hasMatch()) {
2026-02-04 23:03:38 +03:00
QString backupFilename = match.captured(1) + " - " + match.captured(2) + "_" + match.captured(3) + ".tgz";
2026-01-21 12:27:24 +03:00
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();
2026-02-06 17:14:08 +03:00
// Get server IP address
2026-02-04 23:03:38 +03:00
QString serverIp = NetworkUtilities::getIPAddress(credentials.hostName);
if (serverIp.isEmpty()) {
serverIp = credentials.hostName;
}
2026-02-06 17:14:08 +03:00
// Format IP: replace dots with underscores
2026-02-04 23:03:38 +03:00
QString ipFormatted = serverIp;
ipFormatted.replace(".", "_");
QString script = getContainersBackupScript(containers, ipFormatted);
2026-01-21 12:27:24 +03:00
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 = runHostScript(credentials, script, cbStdOut, cbStdErr);
2026-01-21 12:27:24 +03:00
if (error == ErrorCode::NoError) {
2026-02-06 17:14:08 +03:00
// Parse created backup name from output: format "IP_ADDRESS - DD-MM-YYYY_HH-MM-SS.tgz"
2026-02-04 23:03:38 +03:00
QRegularExpression re("([\\d_]+)\\s+-\\s+(\\d{2}-\\d{2}-\\d{4})_(\\d{2}-\\d{2}-\\d{2})\\.tgz");
2026-01-21 12:27:24 +03:00
QRegularExpressionMatch match = re.match(m_currentOutput);
if (match.hasMatch()) {
2026-02-04 23:03:38 +03:00
QString backupFilename = match.captured(1) + " - " + match.captured(2) + "_" + match.captured(3) + ".tgz";
2026-01-21 12:27:24 +03:00
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 = runHostScript(credentials, script, cbStdOut, cbStdErr);
2026-01-21 12:27:24 +03:00
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,
2026-02-04 23:03:38 +03:00
const QStringList &containers,
bool replaceMode)
2026-01-21 12:27:24 +03:00
{
if (m_status == InProgress) {
emit errorOccurred("Another operation is in progress", ErrorCode::AmneziaServiceConnectionFailed);
return;
}
setStatus(InProgress);
2026-02-04 23:03:38 +03:00
QString modeText = replaceMode ? tr("replace mode") : tr("add mode");
setProgress(0, tr("Starting restore from %1 (%2)...").arg(backupFilename, modeText));
2026-01-21 12:27:24 +03:00
m_currentOutput.clear();
m_currentError.clear();
2026-02-04 23:03:38 +03:00
QString script = getRestoreScript(backupFilename, containers, replaceMode);
2026-01-21 12:27:24 +03:00
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 = runHostScript(credentials, script, cbStdOut, cbStdErr);
2026-01-21 12:27:24 +03:00
2026-02-06 17:14:08 +03:00
// Check output for errors, even if script exited with code 0
2026-02-04 23:03:38 +03:00
bool hasError = m_currentOutput.contains("[ERROR]") ||
m_currentOutput.contains("Failed to extract backup") ||
m_currentError.contains("[ERROR]") ||
m_currentError.contains("Failed to extract backup");
if (error == ErrorCode::NoError && !hasError) {
2026-02-06 17:14:08 +03:00
// Check that restore actually completed successfully
2026-02-04 23:03:38 +03:00
if (m_currentOutput.contains("Restore completed successfully")) {
setStatus(Success);
setProgress(100, tr("Backup restored successfully"));
emit backupRestored();
} else {
setStatus(Failed);
emit errorOccurred(tr("Backup restore did not complete successfully. Output: %1").arg(m_currentOutput), ErrorCode::InternalError);
}
2026-01-21 12:27:24 +03:00
} else {
setStatus(Failed);
2026-02-04 23:03:38 +03:00
QString errorMessage = hasError ?
tr("Failed to restore backup: %1").arg(m_currentOutput + "\n" + m_currentError) :
tr("Failed to restore backup: %1").arg(m_currentError);
emit errorOccurred(errorMessage, error);
2026-01-21 12:27:24 +03:00
}
}
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 = runHostScript(credentials, script, cbStdOut, cbStdErr);
2026-01-21 12:27:24 +03:00
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
2026-02-06 17:14:08 +03:00
// On Android use public Download folder (via JNI)
2026-01-21 12:27:24 +03:00
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
2026-02-06 17:14:08 +03:00
// On Desktop use Documents (as regular backup)
2026-01-21 12:27:24 +03:00
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 = downloadFileFromHost(credentials, remotePath, actualLocalPath);
2026-01-21 12:27:24 +03:00
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"));
2026-02-06 17:14:08 +03:00
// If auto-delete from server enabled, start it
if (m_autoDeleteAfterDownload && !m_lastCreatedBackupFilename.isEmpty()) {
qDebug() << "Auto-deleting backup from server after download:" << m_lastCreatedBackupFilename;
m_autoDeleteAfterDownload = false; // Reset flag
deleteBackup(credentials, m_lastCreatedBackupFilename);
m_lastCreatedBackupFilename.clear();
}
2026-01-21 12:27:24 +03:00
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,
2026-02-04 23:03:38 +03:00
const QString &localPath,
bool replaceMode)
2026-01-21 12:27:24 +03:00
{
if (m_status == InProgress) {
emit errorOccurred("Another operation is in progress", ErrorCode::AmneziaServiceConnectionFailed);
return;
}
m_sshSession.resetConnection();
2026-02-06 17:14:08 +03:00
// Save restore mode for later use
2026-02-04 23:03:38 +03:00
m_restoreReplaceMode = replaceMode;
QString actualLocalPath = localPath;
QString filename;
#ifdef Q_OS_ANDROID
2026-02-06 17:14:08 +03:00
// For Android URI need to get filename and use file descriptor
2026-02-04 23:03:38 +03:00
if (localPath.startsWith("content://")) {
2026-02-06 17:14:08 +03:00
// Get filename from URI
2026-02-04 23:03:38 +03:00
filename = AndroidController::instance()->getFileName(localPath);
if (filename.isEmpty()) {
2026-02-06 17:14:08 +03:00
// Fallback: extract name from URI
2026-02-04 23:03:38 +03:00
QStringList parts = localPath.split('/');
if (!parts.isEmpty()) {
filename = parts.last();
2026-02-06 17:14:08 +03:00
// Decode URL-encoded characters
2026-02-04 23:03:38 +03:00
if (filename.contains('%')) {
filename = QUrl::fromPercentEncoding(filename.toUtf8());
}
}
}
2026-02-06 17:14:08 +03:00
// For Android URI use file descriptor via SystemController::readFile
// But scpFileCopy requires file path, so need to copy file to temp directory
2026-02-04 23:03:38 +03:00
QByteArray fileData;
qDebug() << "Reading Android URI:" << localPath;
if (!SystemController::readFile(localPath, fileData)) {
qDebug() << "Failed to read from Android URI";
emit errorOccurred(tr("Failed to read backup file from Android storage"), ErrorCode::ReadError);
return;
}
qDebug() << "Read" << fileData.size() << "bytes from Android URI";
2026-02-06 17:14:08 +03:00
// Delete previous temp file if exists
2026-02-04 23:03:38 +03:00
if (m_tempUploadFile) {
delete m_tempUploadFile;
m_tempUploadFile = nullptr;
}
2026-02-06 17:14:08 +03:00
// Create temp file (save to class member so it doesn't get deleted)
// Use setAutoRemove(false) so file isn't automatically deleted
2026-02-04 23:03:38 +03:00
m_tempUploadFile = new QTemporaryFile(this);
m_tempUploadFile->setAutoRemove(false);
if (!m_tempUploadFile->open()) {
qDebug() << "Failed to create temporary file";
emit errorOccurred(tr("Failed to create temporary file"), ErrorCode::OpenError);
delete m_tempUploadFile;
m_tempUploadFile = nullptr;
return;
}
qint64 written = m_tempUploadFile->write(fileData);
m_tempUploadFile->flush();
2026-02-06 17:14:08 +03:00
// DON'T close file - it must stay open for SCP
2026-02-04 23:03:38 +03:00
// m_tempUploadFile->close();
actualLocalPath = m_tempUploadFile->fileName();
qDebug() << "Created temp file:" << actualLocalPath << "written:" << written << "bytes, size:" << QFileInfo(actualLocalPath).size();
2026-02-06 17:14:08 +03:00
// Check that file exists and is readable
2026-02-04 23:03:38 +03:00
QFileInfo tempFileInfo(actualLocalPath);
if (!tempFileInfo.exists()) {
qDebug() << "Temp file does not exist after creation!";
emit errorOccurred(tr("Failed to create temporary file"), ErrorCode::OpenError);
delete m_tempUploadFile;
m_tempUploadFile = nullptr;
return;
}
2026-02-06 17:14:08 +03:00
// If filename empty, use name from temp file
2026-02-04 23:03:38 +03:00
if (filename.isEmpty()) {
filename = QFileInfo(actualLocalPath).fileName();
}
} else {
QFileInfo localFileInfo(localPath);
if (!localFileInfo.exists()) {
emit errorOccurred(tr("Local file does not exist: %1").arg(localPath), ErrorCode::InternalError);
return;
}
filename = localFileInfo.fileName();
}
#else
2026-02-06 17:14:08 +03:00
// For other platforms use regular check
2026-01-21 12:27:24 +03:00
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;
}
2026-02-04 23:03:38 +03:00
filename = localFileInfo.fileName();
#endif
2026-01-21 12:27:24 +03:00
setStatus(InProgress);
setProgress(0, tr("Uploading backup..."));
// Sanitize filename: replace spaces with underscores so SCP path has no unquoted spaces
// (libssh passes the path verbatim to the remote shell; spaces would split the scp -t argument)
QString safeFilename = filename;
safeFilename.replace(' ', '_');
// Construct remote file path with sanitized filename
QString remotePath = QString("%1/%2").arg(m_backupDir, safeFilename);
2026-01-21 12:27:24 +03:00
setProgress(25, tr("Starting file transfer..."));
// SCP to /var/backups/amnezia/ requires root ownership. The SSH user may not be root,
// so we upload the file to /tmp/ first (world-writable), then move it to the final
// location via sudo — the same pattern used by all other server-side operations.
QFile localFile(actualLocalPath);
if (!localFile.open(QIODevice::ReadOnly)) {
emit errorOccurred(tr("Failed to read backup file for upload"), ErrorCode::ReadError);
return;
}
const QByteArray fileData = localFile.readAll();
localFile.close();
const QString tmpRemotePath = QString("/tmp/amnezia_restore_%1.tgz").arg(
Utils::getRandomString(8));
ErrorCode error = m_sshSession.uploadFileToHost(credentials, fileData, tmpRemotePath);
qDebug() << "Upload to /tmp result, error code:" << static_cast<int>(error);
if (error == ErrorCode::NoError) {
// Move from /tmp to final destination with sudo
const QString moveScript = QString("mkdir -p \"%1\" && mv \"%2\" \"%3\"")
.arg(m_backupDir, tmpRemotePath, remotePath);
error = runHostScript(credentials, moveScript);
qDebug() << "Move to backup dir result, error code:" << static_cast<int>(error);
if (error != ErrorCode::NoError) {
// Clean up tmp file on failure
runHostScript(credentials, QString("rm -f \"%1\"").arg(tmpRemotePath));
}
}
2026-01-21 12:27:24 +03:00
2026-02-04 23:03:38 +03:00
qDebug() << "Upload result, error code:" << static_cast<int>(error);
2026-01-21 12:27:24 +03:00
if (error == ErrorCode::NoError) {
setStatus(Success);
setProgress(100, tr("Backup uploaded successfully"));
emit backupUploaded(remotePath);
2026-02-04 23:03:38 +03:00
2026-02-06 17:14:08 +03:00
// If auto restore enabled, start it
if (m_autoRestoreAfterUpload) {
m_autoRestoreAfterUpload = false; // Reset flag
qDebug() << "Auto-starting restore after upload";
QString backupFilename = remotePath.split('/').last();
restoreBackup(m_pendingRestoreCredentials, backupFilename, QStringList(), m_restoreReplaceMode);
}
// Delete temp file after successful upload
2026-02-04 23:03:38 +03:00
if (m_tempUploadFile) {
qDebug() << "Removing temp file:" << m_tempUploadFile->fileName();
m_tempUploadFile->remove();
delete m_tempUploadFile;
m_tempUploadFile = nullptr;
}
2026-01-21 12:27:24 +03:00
} else {
setStatus(Failed);
2026-02-04 23:03:38 +03:00
qDebug() << "Upload failed with error code:" << static_cast<int>(error);
2026-01-21 12:27:24 +03:00
emit errorOccurred(tr("Failed to upload backup: error code %1").arg(static_cast<int>(error)), error);
2026-02-04 23:03:38 +03:00
2026-02-06 17:14:08 +03:00
// Delete temp file on error
2026-02-04 23:03:38 +03:00
if (m_tempUploadFile) {
m_tempUploadFile->remove();
delete m_tempUploadFile;
m_tempUploadFile = nullptr;
}
2026-01-21 12:27:24 +03:00
}
}
2026-02-06 17:14:08 +03:00
// Overloaded method for setup wizard with separate credential parameters
2026-02-04 23:03:38 +03:00
void ServersBackupController::uploadBackupWithStrings(const QString &hostname,
const QString &username,
const QString &secretData,
const QString &localPath,
bool replaceMode)
{
2026-02-06 17:14:08 +03:00
// Create ServerCredentials from strings
2026-02-04 23:03:38 +03:00
ServerCredentials credentials;
credentials.hostName = hostname;
credentials.userName = username;
credentials.secretData = secretData;
credentials.port = 22; // Default SSH port
qDebug() << "uploadBackupWithStrings called with hostname:" << hostname << "username:" << username;
2026-02-06 17:14:08 +03:00
// Call main method
2026-02-04 23:03:38 +03:00
uploadBackup(credentials, localPath, replaceMode);
}
2026-02-05 10:29:18 +03:00
QStringList ServersBackupController::scanBackupForContainers(const QString &localPath)
{
QStringList containers;
qDebug() << "Scanning backup file for containers:" << localPath;
2026-02-06 17:14:08 +03:00
// For Android URI or regular path use tar to view contents
2026-02-05 10:29:18 +03:00
#ifdef Q_OS_ANDROID
QString actualPath = localPath;
if (localPath.startsWith("content://")) {
2026-02-06 17:14:08 +03:00
// For Android URI need to read file first
2026-02-05 10:29:18 +03:00
int fd = AndroidController::instance()->getFd(localPath);
if (fd < 0) {
qWarning() << "Failed to get file descriptor for Android URI";
return containers;
}
QFile file;
if (!file.open(fd, QIODevice::ReadOnly)) {
qWarning() << "Failed to open file from descriptor";
AndroidController::instance()->closeFd();
return containers;
}
QByteArray data = file.readAll();
file.close();
AndroidController::instance()->closeFd();
2026-02-06 17:14:08 +03:00
// Save to temporary file
2026-02-05 10:29:18 +03:00
actualPath = QDir::temp().filePath("backup_scan_temp.tgz");
QFile tempFile(actualPath);
if (!tempFile.open(QIODevice::WriteOnly)) {
qWarning() << "Failed to create temp file for scanning";
return containers;
}
tempFile.write(data);
tempFile.close();
}
#else
QString actualPath = localPath;
#endif
2026-02-11 12:12:10 +03:00
#ifdef Q_OS_IOS
// iOS sandbox does not allow spawning external processes (tar).
// Return all known container names that may appear in backup; server-side restore
// will only restore what is actually in the archive.
containers << QStringLiteral("amnezia-awg2")
<< QStringLiteral("amnezia-wireguard")
<< QStringLiteral("amnezia-xray")
<< QStringLiteral("amnezia-openvpn-cloak")
<< QStringLiteral("amnezia-awg");
qDebug() << "Found containers in backup (iOS fallback):" << containers;
return containers;
#endif
#if !defined(Q_OS_IOS)
// Execute tar command to view contents (desktop, Android with temp file).
// QProcess is not available on iOS (sandbox does not allow spawning processes).
2026-02-05 10:29:18 +03:00
QProcess process;
process.start("tar", QStringList() << "-tzf" << actualPath);
process.waitForFinished(5000);
2026-02-11 12:12:10 +03:00
2026-02-05 10:29:18 +03:00
if (process.exitCode() != 0) {
qWarning() << "Failed to read backup archive:" << process.readAllStandardError();
return containers;
}
2026-02-11 12:12:10 +03:00
2026-02-05 10:29:18 +03:00
QString output = process.readAllStandardOutput();
QStringList lines = output.split('\n', Qt::SkipEmptyParts);
2026-02-11 12:12:10 +03:00
2026-02-06 17:14:08 +03:00
// Find container directories (amnezia-*)
2026-02-05 10:29:18 +03:00
QSet<QString> foundContainers;
for (const QString &line : lines) {
if (line.contains("amnezia-")) {
2026-02-06 17:14:08 +03:00
// Extract container name from path
2026-02-05 10:29:18 +03:00
QStringList parts = line.split('/');
for (const QString &part : parts) {
if (part.startsWith("amnezia-")) {
foundContainers.insert(part);
break;
}
}
}
}
2026-02-11 12:12:10 +03:00
2026-02-05 10:29:18 +03:00
containers = foundContainers.values();
qDebug() << "Found containers in backup:" << containers;
2026-02-11 12:12:10 +03:00
#endif
2026-02-05 10:29:18 +03:00
return containers;
}
2026-01-21 12:27:24 +03:00
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..."));
2026-02-06 17:14:08 +03:00
// Escape filename for safe use in bash
2026-02-04 23:03:38 +03:00
QString escapedFilename = backupFilename;
2026-02-06 17:14:08 +03:00
escapedFilename.replace("'", "'\\''"); // Escape single quotes
2026-02-04 23:03:38 +03:00
QString script = QString("sudo rm -f '%1/%2'").arg(m_backupDir).arg(escapedFilename);
2026-01-21 12:27:24 +03:00
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 = runHostScript(credentials, script, cbStdOut, cbStdErr);
2026-01-21 12:27:24 +03:00
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);
}
}
// ============================================================================
2026-02-06 17:14:08 +03:00
// EMBEDDED BASH SCRIPTS
2026-01-21 12:27:24 +03:00
// ============================================================================
2026-02-04 23:03:38 +03:00
QString ServersBackupController::getBackupScript(const QString &ipAddress) const
2026-01-21 12:27:24 +03:00
{
2026-02-06 17:14:08 +03:00
// Simplified bash script version, embedded in C++
// Filename format: IP_ADDRESS - DD-MM-YYYY_HH-MM-SS.tgz
2026-01-21 12:27:24 +03:00
return QString(R"(
#!/bin/bash
set -e
BACKUP_DIR=%1
2026-02-04 23:03:38 +03:00
IP_ADDRESS=%2
DATE=$(date +%d-%m-%Y)
TIME=$(date +%H-%M-%S)
BACKUP_FILENAME="$IP_ADDRESS - ${DATE}_${TIME}.tgz"
BACKUP_SUBDIR="$BACKUP_DIR/backup_temp_$$"
2026-01-21 12:27:24 +03:00
echo "[INFO] Starting backup..."
2026-02-06 17:14:08 +03:00
# Create directory
2026-01-21 12:27:24 +03:00
mkdir -p "$BACKUP_SUBDIR"
2026-02-06 17:14:08 +03:00
# List of Amnezia containers
2026-01-21 12:27:24 +03:00
CONTAINERS=(
"amnezia-awg"
"amnezia-awg2"
"amnezia-openvpn"
"amnezia-xray"
"amnezia-wireguard"
"amnezia-ipsec"
"amnezia-cloak"
"amnezia-shadowsocks"
)
2026-02-06 17:14:08 +03:00
# Backup each container (including stopped)
2026-01-21 12:27:24 +03:00
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"
2026-02-06 17:14:08 +03:00
# Copy /opt/amnezia
2026-01-21 12:27:24 +03:00
sudo docker cp "$container:/opt/amnezia" "$BACKUP_SUBDIR/$container/" 2>/dev/null || true
2026-02-06 17:14:08 +03:00
# Save metadata
2026-01-21 12:27:24 +03:00
sudo docker inspect "$container" > "$BACKUP_SUBDIR/$container/container_inspect.json" 2>/dev/null || true
fi
done
2026-02-06 17:14:08 +03:00
# Create archive
2026-01-21 12:27:24 +03:00
cd "$BACKUP_DIR"
2026-02-04 23:03:38 +03:00
tar -czf "$BACKUP_FILENAME" -C "$BACKUP_SUBDIR" . 2>/dev/null
2026-01-21 12:27:24 +03:00
rm -rf "$BACKUP_SUBDIR"
2026-02-04 23:03:38 +03:00
echo "[INFO] Backup created: $BACKUP_FILENAME"
)").arg(m_backupDir, ipAddress);
2026-01-21 12:27:24 +03:00
}
2026-02-04 23:03:38 +03:00
QString ServersBackupController::getContainerBackupScript(DockerContainer container, const QString &ipAddress) const
2026-01-21 12:27:24 +03:00
{
QString containerName = ContainerUtils::containerToString(container);
2026-01-21 12:27:24 +03:00
2026-02-06 17:14:08 +03:00
// Backup specific container directly via docker cp
// Filename format: IP_ADDRESS - DD-MM-YYYY_HH-MM-SS.tgz
2026-01-21 12:27:24 +03:00
return QString(R"(
#!/bin/bash
set -e
BACKUP_DIR=%1
CONTAINER_NAME=%2
2026-02-04 23:03:38 +03:00
IP_ADDRESS=%3
DATE=$(date +%d-%m-%Y)
TIME=$(date +%H-%M-%S)
BACKUP_FILENAME="$IP_ADDRESS - ${DATE}_${TIME}.tgz"
BACKUP_SUBDIR="$BACKUP_DIR/backup_temp_$$"
2026-01-21 12:27:24 +03:00
echo "[INFO] Starting backup for container: $CONTAINER_NAME..."
2026-02-06 17:14:08 +03:00
# Check container exists
2026-01-21 12:27:24 +03:00
if ! sudo docker ps -a --format '{{.Names}}' | grep -q "^$CONTAINER_NAME$"; then
echo "[ERROR] Container $CONTAINER_NAME does not exist"
exit 1
fi
2026-02-06 17:14:08 +03:00
# Create directory
2026-01-21 12:27:24 +03:00
mkdir -p "$BACKUP_SUBDIR/$CONTAINER_NAME"
2026-02-06 17:14:08 +03:00
# Backup configurations from container directly
2026-01-21 12:27:24 +03:00
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..."
2026-02-06 17:14:08 +03:00
# Alternative paths for different container types
2026-01-21 12:27:24 +03:00
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
}
2026-02-06 17:14:08 +03:00
# Save container metadata
2026-01-21 12:27:24 +03:00
echo "[INFO] Saving container metadata..."
sudo docker inspect "$CONTAINER_NAME" > "$BACKUP_SUBDIR/$CONTAINER_NAME/container_inspect.json" 2>/dev/null || true
2026-02-06 17:14:08 +03:00
# Save network configuration
2026-01-21 12:27:24 +03:00
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
2026-02-06 17:14:08 +03:00
# Create archive
2026-01-21 12:27:24 +03:00
cd "$BACKUP_DIR"
2026-02-04 23:03:38 +03:00
tar -czf "$BACKUP_FILENAME" -C "$BACKUP_SUBDIR" . 2>/dev/null
2026-01-21 12:27:24 +03:00
rm -rf "$BACKUP_SUBDIR"
2026-02-04 23:03:38 +03:00
echo "[INFO] Backup created: $BACKUP_FILENAME"
)").arg(m_backupDir, containerName, ipAddress);
2026-01-21 12:27:24 +03:00
}
2026-02-04 23:03:38 +03:00
QString ServersBackupController::getContainersBackupScript(const QList<DockerContainer> &containers, const QString &ipAddress) const
2026-01-21 12:27:24 +03:00
{
QString containersList;
for (const DockerContainer &container : containers) {
QString containerName = ContainerUtils::containerToString(container);
2026-01-21 12:27:24 +03:00
containersList += QString("\"%1\" ").arg(containerName);
}
2026-02-06 17:14:08 +03:00
// Filename format: IP_ADDRESS - DD-MM-YYYY_HH-MM-SS.tgz
2026-01-21 12:27:24 +03:00
return QString(R"(
#!/bin/bash
set -e
BACKUP_DIR=%1
2026-02-04 23:03:38 +03:00
IP_ADDRESS=%2
DATE=$(date +%d-%m-%Y)
TIME=$(date +%H-%M-%S)
BACKUP_FILENAME="$IP_ADDRESS - ${DATE}_${TIME}.tgz"
BACKUP_SUBDIR="$BACKUP_DIR/backup_temp_$$"
2026-01-21 12:27:24 +03:00
echo "[INFO] Starting backup for containers..."
2026-02-06 17:14:08 +03:00
# Create directory
2026-01-21 12:27:24 +03:00
mkdir -p "$BACKUP_SUBDIR"
2026-02-06 17:14:08 +03:00
# List of containers for backup
2026-02-04 23:03:38 +03:00
CONTAINERS=(%3)
2026-01-21 12:27:24 +03:00
2026-02-06 17:14:08 +03:00
# Backup each container
2026-01-21 12:27:24 +03:00
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"
2026-02-06 17:14:08 +03:00
# Copy /opt/amnezia directly from container
2026-01-21 12:27:24 +03:00
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
}
2026-02-06 17:14:08 +03:00
# Save metadata
2026-01-21 12:27:24 +03:00
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
2026-02-06 17:14:08 +03:00
# Create archive
2026-01-21 12:27:24 +03:00
cd "$BACKUP_DIR"
2026-02-04 23:03:38 +03:00
tar -czf "$BACKUP_FILENAME" -C "$BACKUP_SUBDIR" . 2>/dev/null
2026-01-21 12:27:24 +03:00
rm -rf "$BACKUP_SUBDIR"
2026-02-04 23:03:38 +03:00
echo "[INFO] Backup created: $BACKUP_FILENAME"
)").arg(m_backupDir, ipAddress, containersList.trimmed());
2026-01-21 12:27:24 +03:00
}
QString ServersBackupController::getRestoreScript(const QString &backupFilename,
2026-02-04 23:03:38 +03:00
const QStringList &containers,
bool replaceMode) const
2026-01-21 12:27:24 +03:00
{
2026-02-06 17:14:08 +03:00
Q_UNUSED(containers); // TODO: Use for selective restore
2026-01-21 12:27:24 +03:00
2026-02-06 17:14:08 +03:00
// Escape filename for safe use in bash
2026-02-04 23:03:38 +03:00
QString escapedFilename = backupFilename;
2026-02-06 17:14:08 +03:00
escapedFilename.replace("'", "'\\''"); // Escape single quotes
2026-02-04 23:03:38 +03:00
2026-01-21 12:27:24 +03:00
return QString(R"(
#!/bin/bash
set -e
BACKUP_DIR=%1
2026-02-04 23:03:38 +03:00
BACKUP_FILE='%2'
REPLACE_MODE=%3
2026-01-21 12:27:24 +03:00
TEMP_DIR="/tmp/amnezia_restore_$$"
echo "[INFO] Starting restore from $BACKUP_FILE..."
2026-02-04 23:03:38 +03:00
if [ "$REPLACE_MODE" = "1" ]; then
echo "[INFO] Using replace mode: containers will be cleared before restore"
else
echo "[INFO] Using add mode: data will be added to existing containers"
fi
2026-02-06 17:14:08 +03:00
# Check backup file exists
2026-02-04 23:03:38 +03:00
if [ ! -f "$BACKUP_DIR/$BACKUP_FILE" ]; then
echo "[ERROR] Backup file not found: $BACKUP_DIR/$BACKUP_FILE"
exit 1
fi
2026-01-21 12:27:24 +03:00
2026-02-06 17:14:08 +03:00
# Extract backup
2026-01-21 12:27:24 +03:00
mkdir -p "$TEMP_DIR"
2026-02-04 23:03:38 +03:00
EXTRACT_OUTPUT=$(tar -xzf "$BACKUP_DIR/$BACKUP_FILE" -C "$TEMP_DIR" 2>&1)
EXTRACT_EXIT_CODE=$?
if [ $EXTRACT_EXIT_CODE -ne 0 ]; then
echo "[ERROR] Failed to extract backup archive: $EXTRACT_OUTPUT"
rm -rf "$TEMP_DIR"
exit 1
fi
2026-01-21 12:27:24 +03:00
2026-02-06 17:14:08 +03:00
# Find directory with containers (may be backup_temp_* or containers directly)
2026-01-21 12:27:24 +03:00
BACKUP_SUBDIR=$(ls -d "$TEMP_DIR"/backup_* 2>/dev/null | head -1)
2026-02-06 17:14:08 +03:00
# If didn't find backup_*, check if container directories exist directly
2026-01-21 12:27:24 +03:00
if [ -z "$BACKUP_SUBDIR" ]; then
2026-02-06 17:14:08 +03:00
# Check if container directories (amnezia-*) exist directly in TEMP_DIR
2026-02-04 23:03:38 +03:00
if ls -d "$TEMP_DIR"/amnezia-* 2>/dev/null | head -1 > /dev/null; then
BACKUP_SUBDIR="$TEMP_DIR"
else
echo "[ERROR] Failed to extract backup: backup directory not found in archive"
echo "[DEBUG] Contents of $TEMP_DIR:"
ls -la "$TEMP_DIR" || true
rm -rf "$TEMP_DIR"
exit 1
fi
2026-01-21 12:27:24 +03:00
fi
2026-02-06 17:14:08 +03:00
# Restore each container
2026-01-21 12:27:24 +03:00
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..."
2026-02-06 17:14:08 +03:00
# Stop container
2026-01-21 12:27:24 +03:00
sudo docker stop "$container_name" 2>/dev/null || true
2026-02-06 17:14:08 +03:00
# Replace mode: clear container before restore
2026-02-04 23:03:38 +03:00
if [ "$REPLACE_MODE" = "1" ]; then
echo "[INFO] Clearing container $container_name before restore..."
2026-02-06 17:14:08 +03:00
# Create empty directory to clear /opt/amnezia
2026-02-04 23:03:38 +03:00
TEMP_CLEAR_DIR="/tmp/clear_amnezia_$$"
mkdir -p "$TEMP_CLEAR_DIR/amnezia"
2026-02-06 17:14:08 +03:00
# Copy empty directory, which will delete old contents
2026-02-04 23:03:38 +03:00
sudo docker cp "$TEMP_CLEAR_DIR/amnezia" "$container_name:/opt/" 2>/dev/null || true
2026-02-06 17:14:08 +03:00
# Delete temporary directory
2026-02-04 23:03:38 +03:00
rm -rf "$TEMP_CLEAR_DIR"
fi
2026-02-06 17:14:08 +03:00
# Restore /opt/amnezia
2026-01-21 12:27:24 +03:00
if [ -d "$container_dir/amnezia" ]; then
sudo docker cp "$container_dir/amnezia" "$container_name:/opt/" 2>/dev/null || true
fi
2026-02-06 17:14:08 +03:00
# Start container
2026-01-21 12:27:24 +03:00
sudo docker start "$container_name" 2>/dev/null || true
echo "[INFO] $container_name restored"
fi
done
2026-02-06 17:14:08 +03:00
# Cleanup
2026-01-21 12:27:24 +03:00
rm -rf "$TEMP_DIR"
echo "[INFO] Restore completed successfully"
2026-02-04 23:03:38 +03:00
)").arg(m_backupDir).arg(escapedFilename).arg(replaceMode ? "1" : "0");
2026-01-21 12:27:24 +03:00
}
QString ServersBackupController::getCheckStatusScript() const
{
return QString(R"SCRIPT(
#!/bin/bash
BACKUP_DIR=%1
echo "Backup directory: $BACKUP_DIR"
2026-02-06 17:14:08 +03:00
# Check backup availability
2026-01-21 12:27:24 +03:00
BACKUPS=$(ls -t "$BACKUP_DIR"/backup_*.tar.gz 2>/dev/null | wc -l)
echo "Total backups: $BACKUPS"
if [ "$BACKUPS" -gt 0 ]; then
2026-02-06 17:14:08 +03:00
# Last backup information
2026-01-21 12:27:24 +03:00
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
2026-02-06 17:14:08 +03:00
# Check running containers
2026-01-21 12:27:24 +03:00
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);
2026-02-06 17:14:08 +03:00
// Parse ls -lht output
2026-01-21 12:27:24 +03:00
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);
2026-02-06 17:14:08 +03:00
// Parse date from filename
2026-01-21 12:27:24 +03:00
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;
2026-02-06 17:14:08 +03:00
// Parse text output
2026-01-21 12:27:24 +03:00
status["raw_output"] = output;
status["has_backups"] = output.contains("Total backups:");
2026-02-06 17:14:08 +03:00
// Extract backup count
2026-01-21 12:27:24 +03:00
QRegularExpression reTotal("Total backups: (\\d+)");
QRegularExpressionMatch matchTotal = reTotal.match(output);
if (matchTotal.hasMatch()) {
status["total_backups"] = matchTotal.captured(1).toInt();
}
2026-02-06 17:14:08 +03:00
// Extract last backup information
2026-01-21 12:27:24 +03:00
QRegularExpression reLatest("Latest backup: (.+)");
QRegularExpressionMatch matchLatest = reLatest.match(output);
if (matchLatest.hasMatch()) {
status["latest_backup"] = matchLatest.captured(1);
}
return status;
}
// ============================================================================
2026-02-06 17:14:08 +03:00
// HELPER METHODS
2026-01-21 12:27:24 +03:00
// ============================================================================
ErrorCode ServersBackupController::handleStdOut(const QString &data, QString &output)
{
output += data;
qDebug().noquote() << "[BACKUP]" << data;
2026-02-06 17:14:08 +03:00
// Check for errors in output
2026-02-04 23:03:38 +03:00
if (data.contains("[ERROR]") || data.contains("ERROR")) {
2026-02-06 17:14:08 +03:00
// Error detected in stdout, but not critical for handleStdOut
// Main check will be in restoreBackup after script execution
2026-02-04 23:03:38 +03:00
}
2026-02-06 17:14:08 +03:00
// Update progress based on output
2026-01-21 12:27:24 +03:00
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..."));
2026-02-04 23:03:38 +03:00
} else if (data.contains("Starting restore")) {
setProgress(10, tr("Starting restore..."));
} else if (data.contains("Restoring")) {
setProgress(50, tr("Restoring containers..."));
} else if (data.contains("Restore completed successfully")) {
setProgress(90, tr("Finalizing restore..."));
2026-01-21 12:27:24 +03:00
}
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);
}
2026-02-06 17:14:08 +03:00
void ServersBackupController::createBackupWithDownload(bool downloadToDevice, bool deleteFromServer)
{
qDebug() << "createBackupWithDownload: download=" << downloadToDevice << "delete=" << deleteFromServer;
if (!m_serversModel) {
emit errorOccurred(tr("ServersModel is not available"), ErrorCode::InternalError);
return;
}
int serverIndex = m_serversUiController
? m_serversController->indexOfServerId(m_serversUiController->getProcessedServerId())
: -1;
2026-02-06 17:14:08 +03:00
if (serverIndex < 0) {
emit errorOccurred(tr("No server selected"), ErrorCode::InternalError);
return;
}
ServerCredentials credentials = m_serversController->getServerCredentials(m_serversUiController->getProcessedServerId());
2026-02-06 17:14:08 +03:00
// Set flags for automatic download and delete
m_autoDownloadAfterCreate = downloadToDevice;
m_autoDeleteAfterDownload = deleteFromServer;
// Create backup
createBackup(credentials);
}
QVariantMap ServersBackupController::getBackupFileInfo(const QString &backupFilePath)
{
QVariantMap result;
// Get filename
QString fileName;
#ifdef Q_OS_ANDROID
if (backupFilePath.startsWith("content://")) {
fileName = AndroidController::instance()->getFileName(backupFilePath);
}
#endif
2026-02-11 12:12:10 +03:00
2026-02-06 17:14:08 +03:00
if (fileName.isEmpty()) {
QFileInfo fileInfo(backupFilePath);
fileName = fileInfo.fileName();
}
2026-02-11 12:12:10 +03:00
// If filename is empty, use fallback (path component or URL-decoded last segment)
2026-02-06 17:14:08 +03:00
if (fileName.isEmpty()) {
2026-02-11 12:12:10 +03:00
QString path = backupFilePath;
if (path.startsWith("file://")) {
path = QUrl(path).toLocalFile();
}
QStringList pathParts = path.split('/');
2026-02-06 17:14:08 +03:00
if (!pathParts.isEmpty()) {
2026-02-11 12:12:10 +03:00
fileName = QUrl::fromPercentEncoding(pathParts.last().toUtf8());
2026-02-06 17:14:08 +03:00
}
}
2026-02-11 12:12:10 +03:00
2026-02-06 17:14:08 +03:00
if (fileName.isEmpty()) {
fileName = "backup.tgz";
}
2026-02-11 12:12:10 +03:00
2026-02-06 17:14:08 +03:00
// Extract IP from filename (format: 38_99_23_227 - DD-MM-YYYY_HH-MM-SS.tgz)
QString serverIp;
QRegularExpression ipRegex("^([\\d_]+)\\s*-");
QRegularExpressionMatch match = ipRegex.match(fileName);
if (match.hasMatch()) {
serverIp = match.captured(1).replace('_', '.');
}
// If failed to extract IP, use empty string
// QML will decide what to use
result["fileName"] = fileName;
result["serverIp"] = serverIp;
qDebug() << "getBackupFileInfo:" << backupFilePath;
qDebug() << " fileName:" << fileName;
qDebug() << " serverIp:" << serverIp;
return result;
}
void ServersBackupController::prepareRestoreFromBackup(const QString &backupFilePath,
const QString &hostname,
const QString &username,
const QString &secretData)
{
qDebug() << "Preparing restore from backup:" << backupFilePath;
qDebug() << " hostname:" << hostname;
qDebug() << " username:" << username;
// 1. Scan backup to determine containers
QStringList containers = scanBackupForContainers(backupFilePath);
if (containers.isEmpty()) {
qWarning() << "No containers found in backup";
emit errorOccurred(tr("No containers found in backup file"), ErrorCode::InternalError);
return;
}
qDebug() << "Found containers in backup:" << containers;
// 2. Extract information from filename
QString fileName;
#ifdef Q_OS_ANDROID
if (backupFilePath.startsWith("content://")) {
fileName = AndroidController::instance()->getFileName(backupFilePath);
}
#endif
2026-02-11 12:12:10 +03:00
2026-02-06 17:14:08 +03:00
if (fileName.isEmpty()) {
QFileInfo fileInfo(backupFilePath);
fileName = fileInfo.fileName();
}
2026-02-11 12:12:10 +03:00
if (fileName.isEmpty()) {
QString path = backupFilePath.startsWith("file://") ? QUrl(backupFilePath).toLocalFile() : backupFilePath;
QStringList pathParts = path.split('/');
if (!pathParts.isEmpty()) {
fileName = QUrl::fromPercentEncoding(pathParts.last().toUtf8());
}
}
2026-02-06 17:14:08 +03:00
// Extract IP from filename (format: 38_99_23_227 - DD-MM-YYYY_HH-MM-SS.tgz)
QString serverIp = hostname; // Default to hostname
QRegularExpression ipRegex("^([\\d_]+)\\s*-");
QRegularExpressionMatch match = ipRegex.match(fileName);
if (match.hasMatch()) {
serverIp = match.captured(1).replace('_', '.');
}
qDebug() << "Extracted info - fileName:" << fileName << "serverIp:" << serverIp;
// 3. Send signal with data for container installation
// QML will receive this signal and install containers via InstallController
emit readyForRestore(backupFilePath, hostname, username, secretData, serverIp, fileName);
}
void ServersBackupController::startRestore(bool isFromSetupWizard,
const QString &backupFilePath,
bool replaceMode,
const QString &wizardHostname,
const QString &wizardUsername,
const QString &wizardSecretData)
{
qDebug() << "Starting restore: isFromSetupWizard=" << isFromSetupWizard
<< "replaceMode=" << replaceMode
<< "backupFilePath=" << backupFilePath;
// Always auto-restore after upload. Previously the QML onBackupUploaded handler
// in PageSettingsServerData.qml triggered restoreBackup for the regular flow, but
// that handler was removed (it caused double restores). Now C++ handles restore
// automatically in both wizard and regular flows.
2026-02-06 17:14:08 +03:00
m_autoRestoreAfterUpload = true;
// If this is setup wizard with wizard credentials, use them directly
if (isFromSetupWizard && !wizardHostname.isEmpty()) {
qDebug() << "Setup wizard mode, using uploadBackupWithStrings";
qDebug() << " hostname:" << wizardHostname;
qDebug() << " username:" << wizardUsername;
// Save credentials for subsequent restore
m_pendingRestoreCredentials.hostName = wizardHostname;
m_pendingRestoreCredentials.userName = wizardUsername;
m_pendingRestoreCredentials.secretData = wizardSecretData;
uploadBackupWithStrings(wizardHostname, wizardUsername, wizardSecretData, backupFilePath, replaceMode);
} else {
// Regular mode - get credentials from ServersModel
qDebug() << "Regular mode, getting credentials from ServersModel";
if (!m_serversModel) {
qWarning() << "ServersModel is null";
emit errorOccurred(tr("Internal error: ServersModel is not available"), ErrorCode::InternalError);
return;
}
int serverIndex = m_serversUiController
? m_serversController->indexOfServerId(m_serversUiController->getProcessedServerId())
: -1;
2026-02-06 17:14:08 +03:00
qDebug() << " ProcessedServerIndex:" << serverIndex;
qDebug() << " ServersCount:" << m_serversController->getServersCount();
2026-02-06 17:14:08 +03:00
if (serverIndex < 0) {
qWarning() << "No processed server selected, trying default server";
serverIndex = m_serversController->getDefaultServerIndex();
2026-02-06 17:14:08 +03:00
qDebug() << " DefaultServerIndex:" << serverIndex;
2026-02-06 17:14:08 +03:00
if (serverIndex < 0) {
qWarning() << "No default server either";
emit errorOccurred(tr("No server selected"), ErrorCode::InternalError);
return;
}
}
2026-02-06 17:14:08 +03:00
qDebug() << " Using server index:" << serverIndex;
ServerCredentials credentials = m_serversController
? m_serversController->getServerCredentials(m_serversController->getServerId(serverIndex))
: ServerCredentials{};
2026-02-06 17:14:08 +03:00
// Save credentials for subsequent restore
m_pendingRestoreCredentials = credentials;
uploadBackup(credentials, backupFilePath, replaceMode);
}
}
bool ServersBackupController::setDefaultServerAfterRestore(bool isFromSetupWizard)
{
if (!m_serversModel) {
qWarning() << "ServersModel is null, cannot set default server";
return false;
}
if (m_serversController->getServersCount() == 0) {
2026-02-06 17:14:08 +03:00
qWarning() << "No servers in model";
return false;
}
// For setup wizard, set last added server as default
if (isFromSetupWizard) {
int serverIdx = m_serversController->getServersCount() - 1;
2026-02-06 17:14:08 +03:00
qDebug() << "Setting default server after restore:" << serverIdx;
if (m_serversUiController) {
m_serversUiController->setDefaultServerAtIndex(serverIdx);
}
m_serversUiController->setProcessedServerId(m_serversController->getServerId(serverIdx));
2026-02-06 17:14:08 +03:00
// Reset retry counter
m_containerRetryCount = 0;
// Start timer to set default container
QTimer::singleShot(500, this, &ServersBackupController::trySetDefaultContainer);
return true;
}
return false;
}
void ServersBackupController::trySetDefaultContainer()
{
if (!m_serversModel) {
qWarning() << "ServersModel is null";
emit defaultServerAndContainerSet();
return;
}
int serverIdx = m_serversController->getServersCount() - 1;
2026-02-06 17:14:08 +03:00
qDebug() << "Timer: Setting default container (attempt" << m_containerRetryCount + 1 << "/" << m_maxContainerRetries << ")";
// Get installed containers via ServersController
QMap<DockerContainer, ContainerConfig> containers;
if (m_serversController) {
QString serverId = m_serversController->getServerId(serverIdx);
containers = m_serversController->getServerContainersMap(serverId);
}
2026-02-06 17:14:08 +03:00
qDebug() << " Total containers:" << containers.size();
if (!containers.isEmpty()) {
// Find first installed non-None container
for (auto it = containers.begin(); it != containers.end(); ++it) {
DockerContainer container = it.key();
if (container != DockerContainer::None) {
qDebug() << " Setting default container" << static_cast<int>(container) << "for server:" << serverIdx;
if (m_serversUiController) {
m_serversUiController->setDefaultContainerAtIndex(serverIdx, static_cast<int>(container));
}
2026-02-06 17:14:08 +03:00
// Successfully set - send signal
qDebug() << " Default server and container set successfully";
emit defaultServerAndContainerSet();
return;
}
}
2026-02-06 17:14:08 +03:00
// Containers exist, but none match - proceed anyway
qDebug() << " No suitable containers found, but model has data, proceeding";
emit defaultServerAndContainerSet();
return;
}
// Model empty - try again
m_containerRetryCount++;
if (m_containerRetryCount < m_maxContainerRetries) {
qDebug() << " Model is empty, will retry...";
QTimer::singleShot(500, this, &ServersBackupController::trySetDefaultContainer);
} else {
qDebug() << " Max retries reached, proceeding anyway";
m_containerRetryCount = 0;
emit defaultServerAndContainerSet();
}
}
ErrorCode ServersBackupController::runHostScript(const ServerCredentials &credentials, const QString &script,
const std::function<ErrorCode(const QString &, libssh::Client &)> &cbStdOut,
const std::function<ErrorCode(const QString &, libssh::Client &)> &cbStdErr)
{
const QString fileName = "/tmp/amnezia_" + Utils::getRandomString(16) + ".sh";
ErrorCode e = m_sshSession.uploadFileToHost(credentials, script.toUtf8(), fileName);
if (e != ErrorCode::NoError)
return e;
e = m_sshSession.runScript(credentials, QString("sudo bash %1").arg(fileName), cbStdOut, cbStdErr);
m_sshSession.runScript(credentials, QString("sudo rm -f %1").arg(fileName));
return e;
}
ErrorCode ServersBackupController::downloadFileFromHost(const ServerCredentials &credentials, const QString &remotePath, const QString &localPath)
{
libssh::Client sshClient;
ErrorCode e = sshClient.connectToHost(credentials);
if (e != ErrorCode::NoError)
return e;
return sshClient.scpFileDownload(remotePath, localPath);
}
ErrorCode ServersBackupController::uploadFileToHostPublic(const ServerCredentials &credentials, const QString &localPath, const QString &remotePath,
libssh::ScpOverwriteMode overwriteMode)
{
libssh::Client sshClient;
ErrorCode e = sshClient.connectToHost(credentials);
if (e != ErrorCode::NoError)
return e;
return sshClient.scpFileCopy(overwriteMode, localPath, remotePath, "backup_file");
}