C++/LIBSSH - БЕЗГЕМОРРОЙНЫЙ БЭКАП И МИРРОР САЙТА НА ДРУГОМ СЕРВАКЕ
Я иногда просыпаюсь посреди ночи и плачу, когда мне снится что у меня веб-сервер (192.168.88.198) навернулся. И как бы это ни было ужасно - один хрен, все некогда сделать бэкап. По этой причине я думаю, что к данной проблеме необходимо подойти комплексно и решить ее таким образом, чтобы она регулярно решалась сама собой. То есть, процесс необходимо автоматизировать. Подразумевается, что на другом ПК, куда будет отправлен бэкап, уже установлен Линукс, что у него IP 192.168.88.202, что там работает какой-нибудь веб-сервер, будто то apache2 или nginx.
Еще вспоминается история: один приятель, бармен, однажды пригласил меня к себе на работу, напоил пивом, а потом еще туда приперся какой-то нерд, который заметил что у меня на футболке TUX... И тут Вася, тот бармен, мне сказал, кивнув на гостя, что это "тоже программист". У меня тогда был такой себе опыт, практически никакой, но очень хотелось реализовать систему, подобную описанной ниже, только никак не получалось, ведь тогда были донейросеточные времена, на стаковерфлоу за любой вопрос тебя могли застрайкать, а на каком-нибудь друоом форуме еще и хуями обложить в связи с тем, что ты, по их мнению, дурак. Вот так и этот программист, глазенки свои выпучил: дескать, "а нахуя так делать"?! Мол, "неправильно управлять удаленным хостом при помощи программы по ssh", и т. п. Начал херню какую-то накидывать, а потом сказал "не знаю", и предложил почитать мануалы. Сейчас вспоминаю его надменную физиономию и чувствую то же разочарование в людях, которое мне давно уже знакомо в связи с тем что, как оказалось, далеко не все взрослые умны...
🔧 Суть задачи
Сделать программу на C++, которая:
- 📦 Дампит MySQL-базу
mad
(с префиксомx9k26_
) - 📁 Копирует сайт
/webserver/madmentat.ru
- 📤 Передаёт всё это через SSH на
192.168.88.202
в ту же папку - 🧩 На сервере-резерве:
- Распаковывает данные
- Заливает базу в локальный MySQL
- Обновляет рабочую копию сайта
- ⏰ Запускается раз в сутки, например, в 03:00
При сбое — нужно просто переподключить порты 80/443 на nginx. Всё.
✅ Что мы уже знаем:
Параметр | Значение |
---|---|
Основной сервер | 192.168.88.198 |
Резервный сервер | 192.168.88.202 |
SSH-пользователь | madmentat , пароль XXX |
Сайт | /webserver/madmentat.ru |
База данных | MySQL (через MySQLi в Joomla) |
Имя БД | mad |
Префикс таблиц | x9k26_ |
Логин к MySQL | madmentat |
Пароль к MySQL | XXX |
Web-сервер на основном | apache2 |
Web-сервер на резерве | nginx |
Программа | Одна, работает только на 192.168.88.198 |
Язык реализации | C++ (желательно через libssh ) |
📦 Что будет делать наша программа (приблизительно)
main()
{
prepare_local_backup(); // dump DB, tar папки
upload_via_ssh(); // scp или sftp через libssh
remote_restore(); // ssh-команда на удалёнке
}
🧰 Структура архива
/tmp/backup/
├── mad.sql <-- дамп базы
└── madmentat.ru/ <-- содержимое сайта
Создаём так:
mysqldump -u madmentat -pRctybz82 mad > /tmp/backup/mad.sql
rsync -a /webserver/madmentat.ru/ /tmp/backup/madmentat.ru/
🔁 На стороне 192.168.88.202
Принимаем архив, распаковываем и заливаем базу:
mysql -u madmentat -pRctybz82 mad < /tmp/backup/mad.sql
📚 libssh (C++) для работы по SSH
Можно использовать:
- libssh — официальная библиотека для работы по SSH
- или libssh2 — более низкоуровневая альтернатива
Мы можем:
- Создать
ssh_session
- Выполнить
scp_push_file
илиsftp_upload
- Выполнить удалённую команду (через
ssh_channel_exec
)
🔐 Пароль или ключ?
Сейчас можно использовать пароль XXX
, но лучше в будущем перейти на авторизацию по SSH-ключу для безопасности (и удобства автоматизации).
📎 Пример bash-скрипта (для проверки логики)
#!/bin/bash set -e NOW=$(date +%F) TMPDIR="/tmp/backup_$NOW" DST="Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в вашем браузере должен быть включен Javascript." REMOTEDIR="/webserver" mkdir -p "$TMPDIR" # 1. Бэкап базы mysqldump -u madmentat -p'XXX' mad > "$TMPDIR/mad.sql" # 2. Копия сайта rsync -a /webserver/madmentat.ru/ "$TMPDIR/madmentat.ru/" # 3. Передача архива ssh $DST "rm -rf $REMOTEDIR/.backup && mkdir -p $REMOTEDIR/.backup" scp -r "$TMPDIR/"* "$DST:$REMOTEDIR/.backup/" # 4. На удалёнке: заменить сайт и залить базу ssh $DST <<EOF rm -rf $REMOTEDIR/madmentat.ru/* cp -r $REMOTEDIR/.backup/madmentat.ru/* $REMOTEDIR/madmentat.ru/ mysql -u madmentat -p'XXX' mad < $REMOTEDIR/.backup/mad.sql EOF echo "Backup done at $NOW"
📎 Программа для проверки работы библиотки libssh
Сначала установим необходимый пакет:
sudo apt install libssh-4 libssh-dev
Затем создаем файлик
nano ssh_test.cpp
.
И вставляем в него следующий код:
#include <libssh/libssh.h> #include <iostream> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #include <sys/socket.h> #include <cstring>
std::string get_peer_ip(ssh_session session) {
int sock = ssh_get_fd(session);
sockaddr_storage addr {};
socklen_t len = sizeof(addr);
if (getpeername(sock, (sockaddr*)&addr, &len) != 0) {
return "unknown";
}
char ipstr[INET6_ADDRSTRLEN];
if (addr.ss_family == AF_INET) {
sockaddr_in *s = (sockaddr_in *)&addr;
inet_ntop(AF_INET, &s->sin_addr, ipstr, sizeof(ipstr));
} else if (addr.ss_family == AF_INET6) {
sockaddr_in6 *s = (sockaddr_in6 *)&addr;
inet_ntop(AF_INET6, &s->sin6_addr, ipstr, sizeof(ipstr));
} else {
return "unknown";
}
return std::string(ipstr);
}
int main() {
const char* host = "192.168.88.202";
const char* user = "madmentat";
const char* password = "XXX";
const char* remote_path = "/webserver/madmentat.ru";
ssh_session session = ssh_new();
if (session == nullptr) {
std::cerr << "❌ Ошибка создания сессии\n";
return 1;
}
ssh_options_set(session, SSH_OPTIONS_HOST, host);
ssh_options_set(session, SSH_OPTIONS_USER, user);
int rc = ssh_connect(session);
if (rc != SSH_OK) {
std::cerr << "❌ Ошибка подключения: " << ssh_get_error(session) << "\n";
ssh_free(session);
return 1;
}
std::string peer_ip = get_peer_ip(session);
std::cout << "🔌 Подключение к " << peer_ip << " успешно установлено.\n";
std::cout << "🔍 Просматриваем содержимое папки: " << remote_path << "\n\n";
rc = ssh_userauth_password(session, nullptr, password); if (rc != SSH_AUTH_SUCCESS) { std::cerr << "❌ Аутентификация не удалась: " << ssh_get_error(session) << "\n"; ssh_disconnect(session); ssh_free(session); return 1; } ssh_channel channel = ssh_channel_new(session); if (channel == nullptr) { std::cerr << "❌ Не удалось создать канал\n"; ssh_disconnect(session); ssh_free(session); return 1; } rc = ssh_channel_open_session(channel); if (rc != SSH_OK) { std::cerr << "❌ Не удалось открыть SSH-сессию: " << ssh_get_error(session) << "\n"; ssh_channel_free(channel); ssh_disconnect(session); ssh_free(session); return 1; } // Команда без "итого", без . и .. rc = ssh_channel_request_exec(channel, "ls -1A /webserver/madmentat.ru"); if (rc != SSH_OK) { std::cerr << "❌ Ошибка выполнения команды\n"; ssh_channel_close(channel); ssh_channel_free(channel); ssh_disconnect(session); ssh_free(session); return 1; } char buffer[256]; int nbytes; bool has_output = false; while ((nbytes = ssh_channel_read(channel, buffer, sizeof(buffer), 0)) > 0) { has_output = true; std::cout.write(buffer, nbytes); } if (!has_output) {
std::cout << "📂 Каталог пуст или не содержит видимых файлов\n";
} ssh_channel_send_eof(channel); ssh_channel_close(channel); ssh_channel_free(channel); ssh_disconnect(session); ssh_free(session); std::cout << "✅ Завершено успешно\n"; return 0; }
⚙️ Шаг 3. Компиляция
Компилируем программу:
g++ ssh_test.cpp -o ssh_test -lssh
🚀 Шаг 4. Запуск
Запускаем программу:
./ssh_test
Если всё сделано правильно, ты увидишь содержимое каталога /webserver/madmentat.ru
на сервере 192.168.88.202
.
📌 Примечания
- Если будет ошибка
permission denied
, проверь, включён ли SSH-сервер на целевом ПК. - Если хочешь использовать авторизацию по ключу — позже можно заменить
ssh_userauth_password()
на работу с ключами. - Для логирования можно добавить вывод в файл или использовать
syslog
✅ Что дальше?
Теперь, когда мы умеем подключаться и выполнять команды, следующим шагом будет:
- 📤 Передача файлов на сервер
- 📦 Архивация данных (бэкап сайта и БД)
- 🧩 Разворачивание копии сайта на целевой машине
Следующую часть мы посвятим передаче файлов через SCP/SFTP.
🔧 madbackuper: бэкап, выкладка на резервный хост и автопереключение Nginx
Ну хуле титьки мять? Раванем сразу в бой!
Тут у нас промежуточная ранняя версия.
Во-первых, на сервере 198 надо установить утилиту pv
sudo apt install pv -y
Это для прогресс-баров.
Ну а далее создаем файл
nano madbackuper.cpp
Со следующим содержанием:
// madbackuper.cpp // ------------------------------------------------------------ // Бэкап и развёртывание на резервный хост (Ubuntu/Debian). // Новое в этой версии: // • --skip-upload: пропуск загрузки, если файлы уже на удалёнке // • Проверка наличия nginx без зависимости от PATH // • Привилегированные действия через sudo (учёт отдельного remote_sudo_pass) // • Nginx: systemctl restart (а не reload), детальные проверки vhost (link, nginx -T) // • БД (MariaDB/MySQL): понятный лог импорта и число таблиц после заливки // • Прогресс при архивации/передаче, проверка места, bind-mount /webserver // • Автосоздание sudoers и нужных каталогов // Команда компиляции // g++ -std=c++17 madbackuper.cpp -o madbackuper -lssh // Вариант запуска: // sudo ./madbackuper --skip-tar --skip-upload --target-server=nginx --php-version=8.3 // Конфиг-файл с пояснениями: // /etc/madbackuper.conf // ------------------------------------------------------------ #include <libssh/libssh.h> #include <libssh/sftp.h> #include <iostream> #include <fstream> #include <sstream> #include <filesystem> #include <cstdio> #include <cstdlib> #include <ctime> #include <sys/stat.h> #include <fcntl.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #include <sys/socket.h> #include <sys/wait.h> #include <chrono> #include <thread> #include <string> #include <type_traits> #include <iomanip> #include <memory> #include <algorithm> namespace fs = std::filesystem; using clock_ = std::chrono::steady_clock; // --------------------------- Константы --------------------------- static const char* CFG_PATH_PRIMARY = "/etc/madbackuper.conf"; static const char* CFG_PATH_FALLBACK = "/root/madbackuper.conf"; static const char* DEFAULT_TARGET = "nginx"; // или "apache2" static const int DEFAULT_SSH_PORT = 22; static const int DEFAULT_LOCAL_HTTP_PORT = 8080; static const int DEFAULT_LOCAL_HTTPS_PORT = 0; static const bool DEFAULT_SWITCH_TO_LOCAL = true; static const std::string DEFAULT_PHP_VERSION = "8.3"; // ===== base64 encoder (локальный, без зависимостей) ===== static const char B64_TABLE[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; static std::string base64_encode(const std::string &in) { std::string out; size_t i = 0; unsigned char a3[3]{}; unsigned char a4[4]{}; for (unsigned char c : in) { a3[i++] = c; if (i == 3) { a4[0] = (a3[0] & 0xfc) >> 2; a4[1] = ((a3[0] & 0x03) << 4) + ((a3[1] & 0xf0) >> 4); a4[2] = ((a3[1] & 0x0f) << 2) + ((a3[2] & 0xc0) >> 6); a4[3] = a3[2] & 0x3f; for (int j = 0; j < 4; j++) out.push_back(B64_TABLE[a4[j]]); i = 0; } } if (i) { for (size_t j = i; j < 3; j++) a3[j] = '\0'; a4[0] = (a3[0] & 0xfc) >> 2; a4[1] = ((a3[0] & 0x03) << 4) + ((a3[1] & 0xf0) >> 4); a4[2] = ((a3[1] & 0x0f) << 2) + ((a3[2] & 0xc0) >> 6); a4[3] = a3[2] & 0x3f; for (size_t j = 0; j < i + 1; j++) out.push_back(B64_TABLE[a4[j]]); while ((i++ < 3)) out.push_back('='); } return out; } // --------------------------- Конфиг --------------------------- struct Config { std::string target_server = DEFAULT_TARGET; // nginx | apache2 std::string remote_host = "192.168.88.202"; int ssh_port = DEFAULT_SSH_PORT; std::string remote_user = "madmentat"; std::string remote_pass = "XXX"; // SSH-пароль пользователя std::string remote_sudo_pass = "XXX"; // пароль для sudo (если отличается). Пусто => remote_pass std::string remote_root_pass = "XXX"; // пароль для root (если отличается). Пусто => remote_pass // Локальный сайт (что бэкапим) std::string local_site_dir = "/webserver/madmentat.ru"; // Удалённый сайт (куда раскатываем рабочую копию) std::string remote_site_dir = "/webserver/madmentat.ru"; // Папка для хранения бэкапов на удалёнке std::string remote_backup_base = "/webserver/.backup"; // Веб-название (vhost / server_name) std::string server_name = "madmentat.ru"; // PHP std::string php_version = DEFAULT_PHP_VERSION; std::string php_fpm_sock = ""; // если задано, перебивает php_version // База std::string db_user = "madmentat"; std::string db_pass = "XXX"; std::string db_name = "mad"; // Прокси/переключение std::string proxy_target = "192.168.88.198"; int local_http_port = DEFAULT_LOCAL_HTTP_PORT; int local_https_port = DEFAULT_LOCAL_HTTPS_PORT; bool switch_to_local = DEFAULT_SWITCH_TO_LOCAL; // SSL для локального HTTPS (если local_https_port > 0) std::string ssl_cert = ""; std::string ssl_key = ""; }; // --------------------------- Вспомогалки --------------------------- static std::string trim(const std::string& s) { auto l = s.find_first_not_of(" \t\r\n"); auto r = s.find_last_not_of(" \t\r\n"); if (l == std::string::npos) return {}; return s.substr(l, r - l + 1); } static std::string today() { char buf[16]; std::time_t t = std::time(nullptr); std::tm tm{}; localtime_r(&t, &tm); std::strftime(buf, sizeof(buf), "%Y-%m-%d", &tm); return std::string(buf); } static std::string human_size(uint64_t bytes) { const char* u[] = {"B","KB","MB","GB","TB","PB"}; double v = static_cast<double>(bytes); int i = 0; while (v >= 1024.0 && i < 5) { v /= 1024.0; ++i; } char buf[64]; std::snprintf(buf, sizeof(buf), "%.1f %s", v, u[i]); return buf; } static uint64_t dir_size_bytes(const fs::path& root) { uint64_t total = 0; std::error_code ec; for (fs::recursive_directory_iterator it(root, fs::directory_options::skip_permission_denied, ec), end; it != end; ++it) { if (ec) { ec.clear(); continue; } std::error_code ec2; if (fs::is_regular_file(*it, ec2)) total += fs::file_size(*it, ec2); } return total; } static bool has_command(const char* name) { std::string cmd = "command -v "; cmd += name; cmd += " >/dev/null 2>&1"; return std::system(cmd.c_str()) == 0; } // run_local с флагом «печатать команду» static int run_local(const std::string& cmd, bool echo = true) { if (echo) std::cout << "➜ " << cmd << "\n"; int rc = std::system(cmd.c_str()); if (rc != 0) std::cerr << "❌ Команда вернула код " << rc << "\n"; return rc; } // Спиннер для долгих задач (без вывода команды) static int run_with_spinner(const std::string& cmd, const std::string& label) { pid_t pid = fork(); if (pid < 0) { std::cerr << "❌ fork() для: " << cmd << "\n"; return -1; } if (pid == 0) { execl("/bin/sh", "sh", "-lc", cmd.c_str(), (char*)nullptr); _exit(127); } auto start = clock_::now(); const char* frames[] = {"⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏"}; size_t idx = 0; int status = 0; while (true) { pid_t r = waitpid(pid, &status, WNOHANG); if (r == 0) { auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(clock_::now() - start).count(); std::cout << "\r" << label << " " << frames[idx % 10] << " " << elapsed << "s" << std::flush; idx++; std::this_thread::sleep_for(std::chrono::milliseconds(120)); } else break; } std::cout << "\r" << label << " ✓ " << std::endl; if (!WIFEXITED(status)) { std::cerr << "❌ Процесс завершён ненормально\n"; return -1; } int rc = WEXITSTATUS(status); if (rc != 0) std::cerr << "❌ Команда вернула код " << rc << "\n"; return rc; } static std::string peer_ip(ssh_session s) { int sock = ssh_get_fd(s); sockaddr_storage addr{}; socklen_t len = sizeof(addr); if (getpeername(sock, (sockaddr*)&addr, &len) != 0) return "unknown"; char ip[INET6_ADDRSTRLEN]{}; if (addr.ss_family == AF_INET) { auto* a = (sockaddr_in*)&addr; inet_ntop(AF_INET, &a->sin_addr, ip, sizeof(ip)); } else if (addr.ss_family == AF_INET6) { auto* a = (sockaddr_in6*)&addr; inet_ntop(AF_INET6, &a->sin6_addr, ip, sizeof(ip)); } else return "unknown"; return ip; } // --------------------------- SSH helpers --------------------------- static int ssh_exec(ssh_session session, const std::string& cmd, bool print_out = true) { ssh_channel ch = ssh_channel_new(session); if (!ch) { std::cerr << "❌ ssh_channel_new: " << ssh_get_error(session) << "\n"; return -1; } if (ssh_channel_open_session(ch) != SSH_OK) { std::cerr << "❌ ssh_channel_open_session: " << ssh_get_error(session) << "\n"; ssh_channel_free(ch); return -1; } if (ssh_channel_request_exec(ch, cmd.c_str()) != SSH_OK) { std::cerr << "❌ ssh_channel_request_exec: " << ssh_get_error(session) << "\n"; ssh_channel_close(ch); ssh_channel_free(ch); return -1; } if (print_out) { char buf[4096]; int n; while ((n = ssh_channel_read(ch, buf, sizeof(buf), 0)) > 0) std::cout.write(buf, n); while ((n = ssh_channel_read(ch, buf, sizeof(buf), 1)) > 0) std::cerr.write(buf, n); } else { char buf[4096]; int n; while ((n = ssh_channel_read(ch, buf, sizeof(buf), 0)) > 0) {} while ((n = ssh_channel_read(ch, buf, sizeof(buf), 1)) > 0) {} } ssh_channel_send_eof(ch); int exit_status = ssh_channel_get_exit_status(ch); ssh_channel_close(ch); ssh_channel_free(ch); return exit_status; } static int ssh_exec_capture(ssh_session session, const std::string& cmd, std::string& out, std::string* err=nullptr) { out.clear(); if (err) err->clear(); ssh_channel ch = ssh_channel_new(session); if (!ch) return -1; if (ssh_channel_open_session(ch) != SSH_OK) { ssh_channel_free(ch); return -1; } if (ssh_channel_request_exec(ch, cmd.c_str()) != SSH_OK) { ssh_channel_close(ch); ssh_channel_free(ch); return -1; } char buf[4096]; int n; while ((n = ssh_channel_read(ch, buf, sizeof(buf), 0)) > 0) out.append(buf, n); if (err) while ((n = ssh_channel_read(ch, buf, sizeof(buf), 1)) > 0) err->append(buf, n); ssh_channel_send_eof(ch); int exit_status = ssh_channel_get_exit_status(ch); ssh_channel_close(ch); ssh_channel_free(ch); return exit_status; } // --------------------------- Конфиг I/O --------------------------- static const char* CFG_HEADER = R"(# madbackuper.conf # ------------------------------------------------------------ # Конфигурация для madbackuper (автогенерируется при первом запуске). # Все параметры можно переопределить CLI-ключами. # ------------------------------------------------------------ # Основные параметры: # target_server=nginx # допустимо: nginx | apache2 # remote_host=192.168.88.202 # ssh_port=22 # remote_user=madmentat # remote_pass=XXX # пароль для SSH-подключения пользователя # remote_sudo_pass= # пароль, который спрашивает sudo; если пусто — берётся remote_pass # remote_root_pass= # пароль, ... # Пути: # local_site_dir=/webserver/madmentat.ru # локально: что архивируем # remote_site_dir=/webserver/madmentat.ru # удалённо: куда разворачиваем # remote_backup_base=/webserver/.backup # удалённо: где хранить бэкапы # # Веб-название: # server_name=madmentat.ru # # PHP: # php_version=8.3 # # или: # # php_fpm_sock=/run/php/php8.3-fpm.sock # # База: # db_user=madmentat # db_pass=XXX # db_name=mad # # Прокси/переключение (nginx): # proxy_target=192.168.88.198 # local_http_port=8080 # local_https_port=0 # switch_to_local=true # ssl_cert=/path/to/cert.crt # ssl_key=/path/to/key.key # # Примеры: # ./madbackuper --target-server=nginx --switch-to-local=true # ./madbackuper --skip-tar # ./madbackuper --skip-upload # ------------------------------------------------------------ )"; static void write_default_config(const std::string& path) { std::ofstream out(path); out << CFG_HEADER << "target_server=" << DEFAULT_TARGET << "\n" << "remote_host=" << "192.168.88.202" << "\n" << "ssh_port=" << DEFAULT_SSH_PORT << "\n" << "remote_user=" << "madmentat" << "\n" << "remote_pass=" << "XXX" << "\n" << "remote_root_pass=" << "XXX" << "\n" << "local_site_dir=" << "/webserver/madmentat.ru" << "\n" << "remote_site_dir=" << "/webserver/madmentat.ru" << "\n" << "remote_backup_base=" << "/webserver/.backup" << "\n" << "server_name=" << "madmentat.ru" << "\n" << "php_version=" << DEFAULT_PHP_VERSION << "\n" << "php_fpm_sock=" << "" << "\n" << "db_user=" << "madmentat" << "\n" << "db_pass=" << "XXX" << "\n" << "db_name=" << "mad" << "\n" << "proxy_target=" << "192.168.88.198" << "\n" << "local_http_port=" << DEFAULT_LOCAL_HTTP_PORT << "\n" << "local_https_port=" << DEFAULT_LOCAL_HTTPS_PORT << "\n" << "switch_to_local=" << "true" << "\n" << "ssl_cert=" << "" << "\n" << "ssl_key=" << "" << "\n"; out.close(); } static void load_kv_file(const std::string& path, Config& cfg) { std::ifstream in(path); if (!in) return; std::string line; while (std::getline(in, line)) { line = trim(line); if (line.empty() || line[0]=='#' || line[0]==';') continue; auto eq = line.find('='); if (eq == std::string::npos) continue; auto k = trim(line.substr(0, eq)); auto v = trim(line.substr(eq+1)); if (k=="target_server") cfg.target_server = v; else if (k=="remote_host") cfg.remote_host = v; else if (k=="ssh_port") cfg.ssh_port = std::stoi(v); else if (k=="remote_user") cfg.remote_user = v; else if (k=="remote_pass") cfg.remote_pass = v; else if (k=="remote_root_pass") cfg.remote_root_pass = v; else if (k=="remote_sudo_pass") cfg.remote_sudo_pass = v; else if (k=="local_site_dir") cfg.local_site_dir = v; else if (k=="remote_site_dir") cfg.remote_site_dir = v; else if (k=="remote_backup_base") cfg.remote_backup_base = v; else if (k=="server_name") cfg.server_name = v; else if (k=="php_version") cfg.php_version = v; else if (k=="php_fpm_sock") cfg.php_fpm_sock = v; else if (k=="db_user") cfg.db_user = v; else if (k=="db_pass") cfg.db_pass = v; else if (k=="db_name") cfg.db_name = v; else if (k=="proxy_target") cfg.proxy_target = v; else if (k=="local_http_port") cfg.local_http_port = std::stoi(v); else if (k=="local_https_port") cfg.local_https_port = std::stoi(v); else if (k=="switch_to_local") cfg.switch_to_local = (v=="true" || v=="1" || v=="yes"); else if (k=="ssl_cert") cfg.ssl_cert = v; else if (k=="ssl_key") cfg.ssl_key = v; } } static void apply_cli_kv(int argc, char** argv, Config& cfg) { auto eat = [&](const std::string& arg, const char* key, auto& dst) { std::string p = std::string("--") + key + "="; if (arg.rfind(p, 0) == 0) { std::string val = arg.substr(p.size()); if constexpr(std::is_same_v<decltype(dst), int&>) dst = std::stoi(val); else if constexpr(std::is_same_v<decltype(dst), bool&>) dst = (val=="true" || val=="1" || val=="yes"); else dst = val; return true; } return false; }; for (int i=1; i<argc; ++i) { std::string a = argv[i]; if (eat(a, "target-server", cfg.target_server)) continue; if (eat(a, "remote-host", cfg.remote_host)) continue; if (eat(a, "ssh-port", cfg.ssh_port)) continue; if (eat(a, "remote-user", cfg.remote_user)) continue; if (eat(a, "remote-pass", cfg.remote_pass)) continue; if (eat(a, "remote-root-pass", cfg.remote_pass)) continue; if (eat(a, "remote-sudo-pass", cfg.remote_sudo_pass)) continue; if (eat(a, "local-site-dir", cfg.local_site_dir)) continue; if (eat(a, "remote-site-dir", cfg.remote_site_dir)) continue; if (eat(a, "remote-backup-base", cfg.remote_backup_base)) continue; if (eat(a, "server-name", cfg.server_name)) continue; if (eat(a, "php-version", cfg.php_version)) continue; if (eat(a, "php-fpm-sock", cfg.php_fpm_sock)) continue; if (eat(a, "db-user", cfg.db_user)) continue; if (eat(a, "db-pass", cfg.db_pass)) continue; if (eat(a, "db-name", cfg.db_name)) continue; if (eat(a, "proxy-target", cfg.proxy_target)) continue; if (eat(a, "local-http-port", cfg.local_http_port)) continue; if (eat(a, "local-https-port", cfg.local_https_port)) continue; if (eat(a, "switch-to-local", cfg.switch_to_local)) continue; if (eat(a, "ssl-cert", cfg.ssl_cert)) continue; if (eat(a, "ssl-key", cfg.ssl_key)) continue; } } // --------------------------- Проверка параметров --------------------------- static bool validate(const Config& c, std::string& err) { auto notEmpty = [&](const std::string& v, const char* name)->bool { if (v.empty()) { err = std::string("Параметр пуст: ") + name; return false; } return true; }; if (!(c.target_server=="nginx" || c.target_server=="apache2")) { err = "target_server: nginx|apache2"; return false; } if (!notEmpty(c.remote_host, "remote_host")) return false; if (!notEmpty(c.remote_user, "remote_user")) return false; if (!notEmpty(c.remote_pass, "remote_pass")) return false; // remote_sudo_pass может быть пуст — возьмём remote_pass if (!notEmpty(c.local_site_dir, "local_site_dir")) return false; if (!notEmpty(c.remote_site_dir, "remote_site_dir")) return false; if (!notEmpty(c.remote_backup_base, "remote_backup_base")) return false; if (!notEmpty(c.server_name, "server_name")) return false; if (c.php_fpm_sock.empty() && !notEmpty(c.php_version, "php_version")) return false; if (!notEmpty(c.db_user, "db_user")) return false; if (!notEmpty(c.db_pass, "db_pass")) return false; if (!notEmpty(c.db_name, "db_name")) return false; if (!notEmpty(c.proxy_target, "proxy_target")) return false; if (c.local_http_port <= 0 || c.local_http_port == 80 || c.local_http_port == 443) { err = "local_http_port >0 и не 80/443"; return false; } if (c.local_https_port > 0) { if (!notEmpty(c.ssl_cert, "ssl_cert")) return false; if (!notEmpty(c.ssl_key, "ssl_key")) return false; if (c.local_https_port == 80 || c.local_https_port == 443 || c.local_https_port == c.local_http_port) { err = "local_https_port >0, не 80/443 и ≠ local_http_port"; return false; } } return true; } // --------------------------- SFTP helpers --------------------------- static const char* sftp_errname(int code) { switch (code) { case SSH_FX_OK: return "OK"; case SSH_FX_EOF: return "EOF"; case SSH_FX_NO_SUCH_FILE: return "NO_SUCH_FILE"; case SSH_FX_PERMISSION_DENIED: return "PERMISSION_DENIED"; case SSH_FX_FAILURE: return "FAILURE"; case SSH_FX_BAD_MESSAGE: return "BAD_MESSAGE"; case SSH_FX_NO_CONNECTION: return "NO_CONNECTION"; case SSH_FX_CONNECTION_LOST: return "CONNECTION_LOST"; case SSH_FX_OP_UNSUPPORTED: return "OP_UNSUPPORTED"; default: return "UNKNOWN"; } } static int sftp_mkdirs(ssh_session session, sftp_session sftp, const std::string& path, mode_t mode = 0755) { if (path.empty()) return SSH_OK; std::string cur; for (size_t i = 1; i <= path.size(); ++i) { if (i==path.size() || path[i]=='/') { cur = path.substr(0, i); if (cur.empty()) continue; int rc = sftp_mkdir(sftp, cur.c_str(), mode); if (rc != SSH_OK) { int err = sftp_get_error(sftp); if (err != SSH_FX_FILE_ALREADY_EXISTS) { std::cerr << "❌ sftp_mkdir(" << cur << "): err " << err << " (" << sftp_errname(err) << ") - " << ssh_get_error(session) << "\n"; return rc; } } } } return SSH_OK; } static void print_progress_line(const std::string& label, uint64_t sent, uint64_t total, double mbps) { double ratio = total ? (double)sent / (double)total : 0.0; int width = 28; int fill = (int)(ratio * width); uint64_t remain = total > sent ? (total - sent) : 0; double eta = (mbps>0) ? (remain/1024.0/1024.0)/mbps : 0.0; std::cout << "\r" << label << " ["; for (int i=0;i<width;i++) std::cout << (i<fill ? "█" : " "); std::cout << "] " << std::fixed << std::setprecision(1) << (ratio*100.0) << "% " << std::setprecision(2) << mbps << " MB/s " << human_size(sent) << "/" << human_size(total) << " ETA " << std::setprecision(0) << eta << "s" << std::flush; } // SFTP-передача файла (64 KB блоки) с прогрессом static int sftp_upload_file_progress(ssh_session session, sftp_session sftp, const std::string& local, const std::string& remote, const std::string& label, int* out_err=nullptr, mode_t mode = 0644) { if (out_err) *out_err = SSH_FX_OK; std::ifstream in(local, std::ios::binary); if (!in) { std::cerr << "❌ Не открыть локальный файл: " << local << "\n"; return -1; } uint64_t total = 0; try { total = fs::file_size(local); } catch (...) { total = 0; } sftp_file f = sftp_open(sftp, remote.c_str(), O_WRONLY | O_CREAT | O_TRUNC, mode); if (!f) { int err = sftp_get_error(sftp); if (out_err) *out_err = err; std::cerr << "❌ sftp_open(" << remote << "): " << ssh_get_error(session) << " | SFTP err=" << err << " (" << sftp_errname(err) << ")\n"; return -1; } const size_t BUFSZ = 64 * 1024; // безопасно для любых реализаций SFTP std::unique_ptr<char[]> buf(new char[BUFSZ]); uint64_t sent = 0; auto t0 = clock_::now(); auto last = t0; while (in) { in.read(buf.get(), BUFSZ); std::streamsize left = in.gcount(); char* p = buf.get(); while (left > 0) { ssize_t wr = sftp_write(f, p, left); if (wr < 0) { int err = sftp_get_error(sftp); if (out_err) *out_err = err; std::cerr << "\n❌ sftp_write(" << remote << "): " << ssh_get_error(session) << " | SFTP err=" << err << " (" << sftp_errname(err) << ")\n"; sftp_close(f); return -1; } p += wr; left -= wr; sent += wr; auto now = clock_::now(); auto dt_ms = std::chrono::duration_cast<std::chrono::milliseconds>(now - last).count(); if (dt_ms >= 250 || sent == total) { double secs = std::chrono::duration<double>(now - t0).count(); double mbps = secs>0 ? (sent/1024.0/1024.0)/secs : 0.0; print_progress_line(label, sent, total, mbps); last = now; } } } sftp_close(f); std::cout << "\r" << label << " [████████████████████████████] 100.0% ✓ " << human_size(sent) << " " << std::endl; return 0; } // Резервная передача через обычный SSH-канал (cat > file.part), тоже с прогрессом static int ssh_stream_upload(ssh_session session, const std::string& local, const std::string& remote, const std::string& label) { std::string remote_part = remote + ".part"; ssh_channel ch = ssh_channel_new(session); if (!ch) { std::cerr << "❌ ssh_channel_new\n"; return -1; } if (ssh_channel_open_session(ch) != SSH_OK) { std::cerr << "❌ ssh_channel_open_session: " << ssh_get_error(session) << "\n"; ssh_channel_free(ch); return -1; } std::string cmd = "sh -lc 'umask 022; cat > " + remote_part + "'"; if (ssh_channel_request_exec(ch, cmd.c_str()) != SSH_OK) { std::cerr << "❌ ssh_channel_request_exec (cat): " << ssh_get_error(session) << "\n"; ssh_channel_close(ch); ssh_channel_free(ch); return -1; } std::ifstream in(local, std::ios::binary); if (!in) { std::cerr << "❌ Не открыть локальный файл: " << local << "\n"; ssh_channel_close(ch); ssh_channel_free(ch); return -1; } uint64_t total = 0; try { total = fs::file_size(local); } catch (...) { total = 0; } const size_t BUFSZ = 128 * 1024; // 128 KB std::unique_ptr<char[]> buf(new char[BUFSZ]); uint64_t sent = 0; auto t0 = clock_::now(); auto last = t0; while (in) { in.read(buf.get(), BUFSZ); std::streamsize n = in.gcount(); if (n <= 0) break; const char* p = buf.get(); while (n > 0) { int wr = ssh_channel_write(ch, p, (uint32_t)n); if (wr == SSH_ERROR) { std::cerr << "\n❌ ssh_channel_write: " << ssh_get_error(session) << "\n"; ssh_channel_send_eof(ch); ssh_channel_close(ch); ssh_channel_free(ch); return -1; } p += wr; n -= wr; sent += wr; auto now = clock_::now(); auto dt_ms = std::chrono::duration_cast<std::chrono::milliseconds>(now - last).count(); if (dt_ms >= 250 || sent == total) { double secs = std::chrono::duration<double>(now - t0).count(); double mbps = secs>0 ? (sent/1024.0/1024.0)/secs : 0.0; print_progress_line(label, sent, total, mbps); last = now; } } } ssh_channel_send_eof(ch); int exit_status = ssh_channel_get_exit_status(ch); ssh_channel_close(ch); ssh_channel_free(ch); std::cout << "\r" << label << " [████████████████████████████] 100.0% ✓ " << human_size(sent) << " " << std::endl; if (exit_status != 0) { std::cerr << "❌ Удалённая команда cat завершилась с кодом " << exit_status << "\n"; return -1; } // атомарное переименование .part -> финальное имя { std::ostringstream mv; mv << "sh -lc 'mv -f " << remote_part << " " << remote << "'"; if (ssh_exec(session, mv.str(), true) != 0) { std::cerr << "❌ Не удалось переименовать " << remote_part << " -> " << remote << "\n"; return -1; } } return 0; } // --------------------------- Префикс sudo и пр. --------------------------- static std::string sh_escape_single(const std::string& s) { std::string out; out.reserve(s.size()+8); for (char c: s) { if (c=='\'') out += "'\\''"; else out += c; } return out; } static std::string sudo_prefix(ssh_session session, const Config& C) { if (C.remote_user == "root") return ""; // 1) Пробуем без пароля (NOPASSWD) { ssh_channel ch = ssh_channel_new(session); if (ch && ssh_channel_open_session(ch) == SSH_OK && ssh_channel_request_exec(ch, "sudo -n true") == SSH_OK) { ssh_channel_send_eof(ch); int rc = ssh_channel_get_exit_status(ch); ssh_channel_close(ch); ssh_channel_free(ch); if (rc == 0) return "sudo -n"; } else if (ch) { ssh_channel_close(ch); ssh_channel_free(ch); } } // 2) Fallback: пароль через stdin (без TTY) std::string sudopw = C.remote_sudo_pass.empty() ? C.remote_pass : C.remote_sudo_pass; return "echo '" + sh_escape_single(sudopw) + "' | sudo -S -p ''"; } // --------------------------- Проверка места и bootstrap --------------------------- static uint64_t remote_bytes_avail(ssh_session session, const std::string& path) { std::string out, err; int rc = ssh_exec_capture(session, "df -B1 --output=avail '" + path + "' 2>/dev/null | tail -n1 | tr -d '[:space:]'", out, &err); if (rc != 0 || out.empty()) return 0; try { return std::stoull(trim(out)); } catch (...) { return 0; } } static int ensure_space_and_bind_mount(ssh_session session, const Config& C, uint64_t need_bytes) { std::cout << "💽 Проверка свободного места на удалёнке...\n"; uint64_t avail = remote_bytes_avail(session, "/webserver"); if (avail == 0) { std::string SUDO = sudo_prefix(session, C); ssh_exec(session, (SUDO.empty()? "" : (SUDO + " ")) + "mkdir -p /webserver", false); avail = remote_bytes_avail(session, "/webserver"); } uint64_t headroom = std::max<uint64_t>(need_bytes / 10, 512ULL*1024*1024); // 10% или 512MB uint64_t need_total = need_bytes + headroom; std::cout << " Нужно ~" << human_size(need_total) << ", доступно ~" << human_size(avail) << "\n"; if (avail >= need_total) { std::cout << "✅ Места достаточно на текущем разделе.\n"; return 0; } std::cout << "⚠️ Места недостаточно — bind-mount $HOME/webserver -> /webserver...\n"; std::string SUDO = sudo_prefix(session, C); std::string home; { std::string out; if (ssh_exec_capture(session, "printf %s \"$HOME\"", out, nullptr) == 0 && !trim(out).empty()) home = trim(out); else { std::string out2; if (ssh_exec_capture(session, "getent passwd \"$USER\" | cut -d: -f6", out2, nullptr) == 0 && !trim(out2).empty()) home = trim(out2); } if (home.empty()) home = "/home/" + C.remote_user; } std::string prep = (SUDO.empty()? "" : (SUDO + " ")) + "mkdir -p '" + home + "/webserver' /webserver && " + (C.remote_user=="root" ? "true" : (SUDO.empty()? "" : (SUDO + " ")) + "chown -R " + C.remote_user + ":" + C.remote_user + " '" + home + "/webserver'") + " && " + (SUDO.empty()? "" : (SUDO + " ")) + "mountpoint -q /webserver || " + (SUDO.empty()? "" : (SUDO + " ")) + "mount --bind '" + home + "/webserver' /webserver && " + "echo '" + home + "/webserver /webserver none bind 0 0' | " + (SUDO.empty()? "" : (SUDO + " ")) + "tee -a /etc/fstab >/dev/null"; if (ssh_exec(session, prep, true) != 0) { std::cerr << "❌ Не удалось выполнить bind-mount /webserver\n"; return 1; } uint64_t avail2 = remote_bytes_avail(session, "/webserver"); std::cout << " После монтирования доступно ~" << human_size(avail2) << "\n"; if (avail2 < need_total) { std::cerr << "❌ Даже после bind-mount места недостаточно.\n"; return 1; } std::cout << "✅ Места достаточно после bind-mount.\n"; return 0; } static int bootstrap_remote(ssh_session session, const Config& C) { std::cout << "🛠️ Подготовка удалённого хоста...\n"; std::string SUDO = sudo_prefix(session, C); // sudoers if (C.remote_user != "root") { const std::string sudoers = "/etc/sudoers.d/madbackuper"; std::ostringstream content; content << "Cmnd_Alias MADBACKUP_CMDS = " << "/usr/sbin/nginx, /usr/sbin/nginx -t, " << "/bin/systemctl reload nginx, /bin/systemctl restart nginx, " << "/usr/sbin/apache2ctl, /bin/systemctl reload apache2, /bin/systemctl restart apache2, " << "/usr/bin/tee, /bin/mkdir, /bin/chown, /bin/chmod, /bin/ln, /bin/cp, /bin/mv, /bin/tar, /usr/bin/find, " << "/bin/mount, /bin/umount, /bin/mountpoint, /usr/bin/grep, /usr/bin/cut, /usr/bin/getent, " << "/usr/bin/mysql, /usr/bin/mysqldump\n" << C.remote_user << " ALL=(root) NOPASSWD: MADBACKUP_CMDS\n"; std::ostringstream ensure; ensure << "if [ -f '" << sudoers << "' ]; then " << " if ! grep -q 'MADBACKUP_CMDS' '" << sudoers << "'; then NEED=1; else " << " for k in nginx mount mountpoint systemctl tar tee mkdir chown chmod ln mv cp find mysql mysqldump; do " << " grep -q \"$k\" '" << sudoers << "' || { NEED=1; break; }; " << " done; " << " fi; " << "else NEED=1; fi; " << "if [ \"$NEED\" = 1 ]; then " << " printf '%s' '" << sh_escape_single(content.str()) << "' | " << SUDO << " tee '" << sudoers << "' >/dev/null && " << SUDO << " chmod 440 '" << sudoers << "' && " << SUDO << " visudo -cf '" << sudoers << "'; " << "else echo '→ sudoers уже корректный: " << sudoers << "'; fi"; ssh_exec(session, ensure.str(), true); } // каталоги и права { std::ostringstream cmd; cmd << (SUDO.empty()? "" : (SUDO + " ")) << "mkdir -p '" << C.remote_backup_base << "' '" << C.remote_site_dir << "' && "; if (C.remote_user != "root") { std::string chown_cmd = (SUDO.empty()? "" : (SUDO + " ")); chown_cmd += "chown -R " + C.remote_user + ":" + C.remote_user + " '" + C.remote_backup_base + "'"; cmd << chown_cmd; } else { cmd << "true"; } if (ssh_exec(session, cmd.str(), true) != 0) { std::cerr << "❌ Не удалось подготовить каталоги на удалёнке\n"; return 1; } } std::cout << "✅ Удалённый хост подготовлен\n"; return 0; } // --------------------------- Развёртывание командой (NGINX) --------------------------- static std::string build_nginx_deploy_cmd(const Config& C, const std::string& remote_tar, const std::string& remote_sql, const std::string& remote_day) { auto dq = [](const std::string& s) { std::string r; r.reserve(s.size()*2); for (char c : s) { if (c == '\\' || c == '"') r.push_back('\\'); r.push_back(c); } return r; }; // Пути к нашим (больше не используем для генерации vhost'ов вручную) const std::string php_sock_cfg = C.php_fpm_sock.empty() ? ("/run/php/php" + C.php_version + "-fpm.sock") : C.php_fpm_sock; std::ostringstream root; root << "set -e; umask 022;\n" << "echo \"🔑 Переключаюсь на пользователя root\" 1>&2;\n" << "echo \"🔧 Проверяю окружение (nginx/mysql/tar)\" 1>&2;\n" << "command -v tar >/dev/null || { echo \"❌ tar не установлен\" 1>&2; exit 1; }\n" << "command -v mysql >/dev/null || { echo \"❌ mysql клиент не установлен\" 1>&2; exit 1; }\n" << "NGINX_OK=0; if [ -x /usr/sbin/nginx ]; then NGINX_OK=1; fi;\n" << "command -v nginx >/dev/null 2>&1 && NGINX_OK=1;\n" << "systemctl -q is-active nginx >/dev/null 2>&1 && NGINX_OK=1;\n" << "[ -d /etc/nginx ] && NGINX_OK=1;\n" << "if [ \"$NGINX_OK\" -ne 1 ]; then echo \"❌ nginx не установлен\" 1>&2; exit 1; fi;\n" << "echo \"📦 Подготавливаю директории сайта\" 1>&2;\n" << "mkdir -p \"" << dq(C.remote_site_dir) << "\"\n" << "rm -rf \"" << dq(C.remote_site_dir) << ".old\"\n" << "mv \"" << dq(C.remote_site_dir) << "\" \"" << dq(C.remote_site_dir) << ".old\" 2>/dev/null || true\n" << "mkdir -p \"" << dq(C.remote_site_dir) << "\"\n" << "echo \"📤 Распаковка архива сайта\" 1>&2;\n" << "if command -v pv >/dev/null 2>&1; then " "pv -f -p -t -e -r -b \"" << dq(remote_tar) << "\" | tar -xzf - -C \"" << dq(C.remote_site_dir) << "\"; " "else " "tar -xzf \"" << dq(remote_tar) << "\" -C \"" << dq(C.remote_site_dir) << "\" --checkpoint=500 --checkpoint-action=echo=. ; echo; " "fi\n" << "REF=$(mktemp); touch \"$REF\"; " "find \"" << dq(C.remote_site_dir) << "\" \\( -type f -o -type d \\) -newer \"$REF\" -print0 | xargs -0 -r touch -r \"$REF\"; " "rm -f \"$REF\"\n" << "echo \"🧰 Выставляю права\" 1>&2;\n" << "chown -R www-data:www-data \"" << dq(C.remote_site_dir) << "\" || true\n" << "find \"" << dq(C.remote_site_dir) << "\" -type d -exec chmod 755 {} \\; || true\n" << "find \"" << dq(C.remote_site_dir) << "\" -type f -exec chmod 644 {} \\; || true\n" // === БД (как было) === << "echo \"🗄️ Подготавливаю БД и пользователя (MariaDB)\" 1>&2;\n" << "/usr/bin/mysql -uroot <<\\SQL\n" "CREATE DATABASE IF NOT EXISTS `" << C.db_name << "` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;\n" "CREATE USER IF NOT EXISTS '" << C.db_user << "'@'localhost' IDENTIFIED BY '" << C.db_pass << "';\n" "CREATE USER IF NOT EXISTS '" << C.db_user << "'@'127.0.0.1' IDENTIFIED BY '" << C.db_pass << "';\n" "GRANT ALL ON `" << C.db_name << "`.* TO '" << C.db_user << "'@'localhost';\n" "GRANT ALL ON `" << C.db_name << "`.* TO '" << C.db_user << "'@'127.0.0.1';\n" "FLUSH PRIVILEGES;\n" "SQL\n" << "echo \"📦 Бэкап прежней БД (мягко)\" 1>&2;\n" << "/usr/bin/mysqldump \"" << dq(C.db_name) << "\" > \"" << dq(remote_day) << "/db_old.sql\" 2>/dev/null || true\n" << "DB_IMPORTED=0; TABLES_AFTER=0;\n" << "if [ -s \"" << dq(remote_sql) << "\" ]; then " "echo \"⬇️ Импортирую дамп\" 1>&2; " "/usr/bin/mysql \"" << dq(C.db_name) << "\" < \"" << dq(remote_sql) << "\" && DB_IMPORTED=1; " "else " "echo \"⚠️ Дамп не найден или пуст — пропуск\" 1>&2; " "fi\n" << "TABLES_AFTER=$(/usr/bin/mysql -NBe \"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='" << dq(C.db_name) << "'\" 2>/dev/null || echo 0)\n" << "echo \" 📊 Таблиц в БД после импорта: $TABLES_AFTER\" 1>&2;\n" // === Проверка/установка setup_madmentat_nginx.sh === << "echo \"🧩 Проверяю /root/setup_madmentat_nginx.sh\" 1>&2;\n" << "if [ ! -x /root/setup_madmentat_nginx.sh ]; then\n" << " echo \"✍️ Пишу /root/setup_madmentat_nginx.sh\" 1>&2;\n" << " umask 077; cat > /root/setup_madmentat_nginx.sh <<'EOS'\n" << "#!/usr/bin/env bash\n" "set -euo pipefail\n" "ts(){ date +\"[%H:%M:%S]\"; }\n" "log(){ echo \"$(ts) $*\"; }\n" "DOMAIN=\"" << dq(C.server_name) << "\"; WWW=\"www.${DOMAIN}\"\n" "WEBROOT=\"" << dq(C.remote_site_dir) << "\"\n" "FRONT_AVAIL=\"/etc/nginx/sites-available/${DOMAIN}.conf\"\n" "FRONT_ENABLED=\"/etc/nginx/sites-enabled/${DOMAIN}.conf\"\n" "LOCAL_AVAIL=\"/etc/nginx/sites-available/${DOMAIN}.local.conf\"\n" "LOCAL_ENABLED=\"/etc/nginx/sites-enabled/${DOMAIN}.local.conf\"\n" "BACKEND_CONF=\"/etc/nginx/conf.d/${DOMAIN}.backend.conf\"\n" "BACKEND_LOCAL=\"http://127.0.0.1:8081\"\n" "BACKEND_REMOTE=\"http://192.168.88.198\"\n" "pick_cert_dir(){ for d in \"/etc/letsencrypt/live/${DOMAIN}-0002\" \"/etc/letsencrypt/live/${DOMAIN}\"; do\n" " [ -s \"$d/fullchain.pem\" ] && [ -s \"$d/privkey.pem\" ] && { echo \"$d\"; return 0; }\n" "done; return 1; }\n" "detect_php_sock(){ for s in /run/php/php*-fpm.sock; do [ -S \"$s\" ] && { echo \"$s\"; return 0; }; done; echo \"/run/php/php-fpm.sock\"; }\n" "CERT_DIR=\"$(pick_cert_dir || true)\"; [ -z \"${CERT_DIR:-}\" ] && CERT_DIR=\"/etc/letsencrypt/live/${DOMAIN}-0002\"\n" "CERT_FULL=\"${CERT_DIR}/fullchain.pem\"; CERT_KEY=\"${CERT_DIR}/privkey.pem\"\n" "PHP_SOCK=\"$(detect_php_sock)\"\n" "MODE=\"${1:-status}\"; OVERRIDE_REMOTE=\"${2:-}\"\n" "case \"$MODE\" in\n" " local) TARGET=\"$BACKEND_LOCAL\" ;;\n" " remote) TARGET=\"${OVERRIDE_REMOTE:-$BACKEND_REMOTE}\" ;;\n" " status) TARGET=\"\" ;;\n" " *) echo \"Usage: $0 {local|remote|status} [remote_url]\"; exit 1 ;;\n" "esac\n" "mkdir -p /etc/nginx/disabled\n" "for f in \\\n" " \"/etc/nginx/sites-enabled/${DOMAIN}.local.conf\" \\\n" " \"/etc/nginx/sites-available/${DOMAIN}.local.conf\" \\\n" " \"/etc/nginx/sites-enabled/setup_${DOMAIN}_nginx.sh\" \\\n" " \"/etc/nginx/sites-available/setup_${DOMAIN}_nginx.sh\" \\\n" " ; do\n" " [ -e \"$f\" ] || continue\n" " log \"Карантин: $f\"; mv -f \"$f\" \"/etc/nginx/disabled/$(basename \"$f\").$(date +%Y%m%d-%H%M%S)\"\n" "done\n" "if [ -n \"$TARGET\" ]; then\n" " log \"Пишу ${BACKEND_CONF} -> ${TARGET}\"; echo \"set \\$mad_backend ${TARGET};\" > \"${BACKEND_CONF}\"\n" "else\n" " log \"Статус: BACKEND не меняю (${BACKEND_CONF})\"\n" "fi\n" "log \"Готовлю локальный backend ${LOCAL_AVAIL} (127.0.0.1:8081)\"\n" "cat > \"${LOCAL_AVAIL}\" <<EOF\n" "server {\n" " listen 127.0.0.1:8081;\n" " server_name _;\n" " root ${WEBROOT};\n" " index index.php index.html;\n" " access_log /var/log/nginx/${DOMAIN}.local.access.log;\n" " error_log /var/log/nginx/${DOMAIN}.local.error.log;\n" " location / { try_files \\$uri \\$uri/ /index.php?\\$args; }\n" " location ~ \\.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:${PHP_SOCK}; }\n" " client_max_body_size 1024m;\n" "}\n" "EOF\n" "ln -sf \"${LOCAL_AVAIL}\" \"${LOCAL_ENABLED}\"\n" "log \"Пишу фронт ${FRONT_AVAIL}\"\n" "cat > \"${FRONT_AVAIL}\" <<EOF\n" "server {\n" " listen 80;\n" " listen [::]:80;\n" " server_name ${DOMAIN} ${WWW};\n" " client_max_body_size 1024m;\n" " return 301 https://\\$host\\$request_uri;\n" "}\n" "server {\n" " listen 443 ssl http2;\n" " listen [::]:443 ssl http2;\n" " server_name ${DOMAIN} ${WWW};\n" " ssl_certificate ${CERT_FULL};\n" " ssl_certificate_key ${CERT_KEY};\n" " include /etc/letsencrypt/options-ssl-nginx.conf;\n" " ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;\n" " include ${BACKEND_CONF};\n" " location / {\n" " proxy_pass \\$mad_backend;\n" " proxy_set_header Host \\$host;\n" " proxy_set_header X-Real-IP \\$remote_addr;\n" " proxy_set_header X-Forwarded-For \\$proxy_add_x_forwarded_for;\n" " proxy_set_header X-Forwarded-Proto \\$scheme;\n" " }\n" " client_max_body_size 1024m;\n" "}\n" "EOF\n" "ln -sf \"${FRONT_AVAIL}\" \"${FRONT_ENABLED}\"\n" "log \"Чищу конфликты server_name с ${DOMAIN}\"\n" "shopt -s nullglob\n" "for f in /etc/nginx/sites-available/* /etc/nginx/sites-enabled/* /etc/nginx/conf.d/*.conf; do\n" " [ -f \"$f\" ] || continue\n" " case \"$f\" in\n" " \"$FRONT_AVAIL\"|\"$FRONT_ENABLED\"|\"$LOCAL_AVAIL\"|\"$LOCAL_ENABLED\"|\"$BACKEND_CONF\") continue ;;\n" " esac\n" " if grep -Eq '^\\s*server_name\\s+.*('" << dq(C.server_name) << "|www\\." << dq(C.server_name) << ")\\b' \"$f\"; then\n" " log \"→ Убираю ${DOMAIN} из $f\"\n" " sed -ri '\n" " /^\\s*server_name/{\n" " s/[[:space:]]+" << dq(C.server_name) << "\\b//g;\n" " s/[[:space:]]+www\\." << dq(C.server_name) << "\\b//g;\n" " s/server_name[[:space:]]*;$/server_name _;/;\n" " }' \"$f\"\n" " fi\n" "done\n" "shopt -u nullglob\n" "if ! grep -Eq 'include\\s+/etc/nginx/sites-enabled/\\*;' /etc/nginx/nginx.conf; then\n" " log \"Добавляю include /etc/nginx/sites-enabled/*; в nginx.conf (внутрь http{})\"\n" " sed -ri '/http\\s*\\{/a\\ include /etc/nginx/sites-enabled/*;' /etc/nginx/nginx.conf\n" "fi\n" "log \"Проверяю конфиг nginx\"; nginx -t\n" "log \"Перегружаю nginx\"; systemctl reload nginx\n" "if ss -lntp 2>/dev/null | grep -q ':8081 '; then log \"Локальный backend 8081 слушается\"; else log \"⚠️ 8081 не слушается — проверь ${LOCAL_AVAIL} и error_log\"; fi\n" "current_backend=\"$(awk '/^\\s*set\\s+\\$mad_backend/{print $3}' \"${BACKEND_CONF}\" 2>/dev/null | tr -d ' ;' || true)\"; log \"Текущий backend: ${current_backend:-unknown}\"\n" "if command -v openssl >/dev/null 2>&1; then\n" " log \"Проверяю SNI-сертификат для ${DOMAIN}\"\n" " openssl s_client -connect 127.0.0.1:443 -servername \"${DOMAIN}\" </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer | sed 's/^/ /'\n" "fi\n" "log \"Готово. Переключение:\"\n" "echo \" sudo $0 local # прокси на 127.0.0.1:8081\"\n" "echo \" sudo $0 remote # прокси на 192.168.88.198\"\n" << "EOS\n" << " chmod 700 /root/setup_madmentat_nginx.sh\n" << "else\n" << " echo \"✅ Нашёл /root/setup_madmentat_nginx.sh\" 1>&2;\n" << "fi\n" // === Запускаем переключение на remote === << "echo \"🔀 Запускаю переключение фронта на remote\" 1>&2;\n" << "/root/setup_madmentat_nginx.sh remote || { echo \"❌ Ошибка переключения\" 1>&2; exit 1; }\n" // === Вачдог === << "echo \"🐶 Обновляю /webserver/wachdog\" 1>&2;\n" << "mkdir -p /webserver || true\n" << "WD=/webserver/wachdog\n" << "if [ -f \"$WD\" ]; then\n" << " if grep -Eq '^madmentat\\.ru_update=false\\b' \"$WD\"; then\n" << " sed -ri 's/^madmentat\\.ru_update=false\\b/madmentat.ru_update=true/' \"$WD\"\n" << " echo \" → madmentat.ru_update=true (из false)\" 1>&2;\n" << " else\n" << " if ! grep -Eq '^madmentat\\.ru_update=' \"$WD\"; then echo 'madmentat.ru_update=true' >> \"$WD\"; echo \" → добавил madmentat.ru_update=true\" 1>&2; else echo \" → уже true или отсутствует false\" 1>&2; fi\n" << " fi\n" << "else\n" << " echo 'madmentat.ru_update=true' > \"$WD\"\n" << " echo \" → создан $WD с madmentat.ru_update=true\" 1>&2;\n" << "fi\n" // === Сводка === << "echo \"——— Итог ———\" 1>&2;\n" << "echo \"✅ Импорт БД: ${DB_IMPORTED}\" 1>&2;\n" << "echo \"📊 Таблиц в БД: ${TABLES_AFTER}\" 1>&2;\n" << "echo \"✅ Скрипт nginx: /root/setup_madmentat_nginx.sh\" 1>&2;\n" << "echo \"✅ Фронт: remote (см. /etc/nginx/conf.d/" << dq(C.server_name) << ".backend.conf)\" 1>&2;\n" << "echo \"✅ Вачдог: $(grep -E '^madmentat\\.ru_update=' \"$WD\" || echo '-')\" 1>&2;\n"; const std::string sudopw = C.remote_sudo_pass.empty() ? C.remote_pass : C.remote_sudo_pass; const std::string pass_b64 = base64_encode(sudopw); std::ostringstream wrapper; wrapper << "PW_B64='" << pass_b64 << "'; " << "if sudo -n true 2>/dev/null; then " "sudo -p '' bash -se <<'ROOT'\n" << root.str() << "ROOT\n" << "else " "( printf '%s\\n' \"$(printf '%s' \"$PW_B64\" | base64 -d)\"; cat <<'ROOT'\n" << root.str() << "ROOT\n" << ") | sudo -S -p '' bash -se; " << "fi"; return wrapper.str(); } // --------------------------- Развёртывание командой (APACHE) --------------------------- static std::string build_apache_deploy_cmd(const Config& C, const std::string& remote_tar, const std::string& remote_sql, const std::string& remote_day) { const std::string conf_avail = "/etc/apache2/sites-available/" + C.server_name + ".local.conf"; std::string php_sock = C.php_fpm_sock.empty() ? ("/run/php/php" + C.php_version + "-fpm.sock") : C.php_fpm_sock; // Apache vhost (php-fpm через unix-сокет) std::ostringstream tpl; tpl << "<VirtualHost *:" << C.local_http_port << ">\n" << " ServerName " << C.server_name << "\n" << " DocumentRoot " << C.remote_site_dir << "\n" << " <Directory " << C.remote_site_dir << ">\n" << " Options Indexes FollowSymLinks\n" << " AllowOverride All\n" << " Require all granted\n" << " </Directory>\n" << " ErrorLog ${APACHE_LOG_DIR}/" << C.server_name << "_error.log\n" << " CustomLog ${APACHE_LOG_DIR}/" << C.server_name << "_access.log combined\n" << " <FilesMatch \\.php$>\n" << " SetHandler \"proxy:unix:" << php_sock << "|fcgi://localhost/\"\n" << " </FilesMatch>\n" << "</VirtualHost>\n"; // SUDO и sudo-пароль std::ostringstream cmd; cmd << "set -e; " << "SUDO='sudo -n'; if ! $SUDO true 2>/dev/null; then SUDO=\"echo '" << sh_escape_single(C.remote_sudo_pass.empty() ? C.remote_pass : C.remote_sudo_pass) << "' | sudo -S -p ''\"; fi; " << "echo '→ Проверка окружения (apache2/php-fpm/mysql/tar)'; " << "command -v apache2ctl >/dev/null || { echo '❌ apache2ctl не найден'; exit 1; }; " << "command -v tar >/dev/null || { echo '❌ tar не установлен'; exit 1; }; " << "command -v mysql >/dev/null || { echo '❌ mysql клиент не установлен'; exit 1; }; " << "if ! command -v php-fpm >/dev/null && ! command -v php-fpm" << C.php_version << " >/dev/null; then " "echo '❌ PHP-FPM не найден'; exit 1; " "fi; " << "PHP_SOCK='" << php_sock << "'; " << "[ -S \"$PHP_SOCK\" ] || { echo \"❌ Нет сокета PHP-FPM: $PHP_SOCK\"; ls -l /run/php || true; exit 1; }; " << "echo '→ Обновление рабочей копии'; " << "mkdir -p '" << C.remote_site_dir << "'; " << "rm -rf '" << C.remote_site_dir << "'.old; " << "mv '" << C.remote_site_dir << "' '" << C.remote_site_dir << ".old' 2>/dev/null || true; " << "mkdir -p '" << C.remote_site_dir << "'; " // РАСПАКОВКА С ПРОГРЕССОМ << "echo '→ Распаковка сайта (прогресс)'; " << "if command -v pv >/dev/null 2>&1; then " "pv -f -p -t -e -r -b '" << remote_tar << "' | tar -xzf - -C '" << C.remote_site_dir << "'; " "else " "echo ' pv не найден — индикатор точками'; " "tar -xzf '" << remote_tar << "' -C '" << C.remote_site_dir << "' --checkpoint=500 --checkpoint-action=echo=. ; echo; " "fi; " << "echo '→ Права'; " << "$SUDO /bin/chown -R www-data:www-data '" << C.remote_site_dir << "' || true; " << "find '" << C.remote_site_dir << "' -type d -exec /bin/chmod 755 {} \\; || true; " << "find '" << C.remote_site_dir << "' -type f -exec /bin/chmod 644 {} \\; || true; " // Запись vhost без tee-пайпа << "echo '→ Apache vhost'; " << "TMPCONF=$(mktemp) && printf '%s' '" << sh_escape_single(tpl.str()) << "' > \"$TMPCONF\" && " "$SUDO /bin/mv \"$TMPCONF\" '" << conf_avail << "'; " << "$SUDO /usr/sbin/a2enmod proxy proxy_fcgi setenvif rewrite >/dev/null || true; " << "$SUDO /usr/sbin/a2ensite '" << C.server_name << ".local.conf' >/dev/null || true; " << "echo '→ apache2ctl configtest'; $SUDO /usr/sbin/apache2ctl configtest; " // БД через root << "echo '→ Подготовка БД и пользователя (через root)'; " << "$SUDO /usr/bin/mysql -uroot -e \"" "CREATE DATABASE IF NOT EXISTS \\`" << C.db_name << "\\` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; " "CREATE USER IF NOT EXISTS '" << C.db_user << "'@'localhost' IDENTIFIED BY '" << C.db_pass << "'; " "CREATE USER IF NOT EXISTS '" << C.db_user << "'@'127.0.0.1' IDENTIFIED BY '" << C.db_pass << "'; " "GRANT ALL ON \\`" << C.db_name << "\\`.* TO " "'" << C.db_user << "'@'localhost', " "'" << C.db_user << "'@'127.0.0.1'; " "FLUSH PRIVILEGES;\"; " << "echo '→ Бэкап текущей БД на резерве (мягко)'; " << "$SUDO /usr/bin/mysqldump " << C.db_name << " > '" << remote_day << "/db_old.sql' 2>/dev/null || true; " << "echo '→ Импорт новой БД (через root)'; " << "$SUDO /usr/bin/mysql " << C.db_name << " < '" << remote_sql << "'; " << "echo '→ Перезапуск apache2'; $SUDO /bin/systemctl restart apache2; " << "echo '→ Ротация'; " << "find '" << C.remote_backup_base << "' -maxdepth 1 -type d " "-regextype posix-extended -regex '.*/[0-9]{4}-[0-9]{2}-[0-9]{2}$' -mtime +7 -exec rm -rf {} + ; " << "echo '✓ Apache: развёртывание завершено';"; return cmd.str(); } // --------------------------- MAIN --------------------------- int main(int argc, char** argv) { std::cout << "🔧 madbackuper: SCP/SFTP бэкап и развертывание\n"; // Конфиг: создать при первом запуске std::string cfg_path = CFG_PATH_PRIMARY; if (!fs::exists(cfg_path)) { std::cerr << "ℹ️ Конфиг не найден: " << cfg_path << " — генерирую по умолчанию...\n"; write_default_config(cfg_path); if (!fs::exists(cfg_path)) { cfg_path = CFG_PATH_FALLBACK; std::cerr << "⚠️ Нет прав на /etc. Пишу конфиг сюда: " << cfg_path << "\n"; write_default_config(cfg_path); if (!fs::exists(cfg_path)) { std::cerr << "❌ Не удалось создать конфиг\n"; return 1; } } std::cout << "✅ Создан конфиг: " << cfg_path << "\n"; std::cout << "⚠️ Отредактируй его при необходимости и запусти программу снова.\n"; return 0; } // Загрузка конфига + CLI Config cfg; load_kv_file(cfg_path, cfg); apply_cli_kv(argc, argv, cfg); bool skip_tar = false, skip_upload = false; for (int i=1;i<argc;++i) { std::string a = argv[i]; if (a=="--skip-tar") skip_tar = true; if (a=="--skip-upload") skip_upload = true; } // Валидация std::string verr; if (!validate(cfg, verr)) { std::cerr << "❌ Ошибка параметров: " << verr << "\n"; return 1; } // Пролог std::cout << "📁 Локальный сайт: " << cfg.local_site_dir << "\n"; std::cout << "🛢️ База: " << cfg.db_name << " (user: " << cfg.db_user << ")\n"; std::cout << "🌐 Резерв: " << cfg.remote_user << "@" << cfg.remote_host << ":" << cfg.remote_site_dir << " (" << cfg.target_server << ")\n"; std::cout << "🔄 Локальные порты сайта: " << cfg.local_http_port << (cfg.local_https_port>0?("/"+std::to_string(cfg.local_https_port)):"") << " | switch_to_local=" << (cfg.switch_to_local ? "yes":"no") << "\n"; if (!fs::exists(cfg.local_site_dir)) { std::cerr << "❌ Нет каталога: " << cfg.local_site_dir << "\n"; return 1; } // Артефакты const std::string date = today(); const std::string tmp_dir = "/tmp/madbackuper_" + date; const std::string tar_path = tmp_dir + "/site_" + date + ".tar.gz"; const std::string sql_path = tmp_dir + "/db_" + date + ".sql"; const std::string cnf_path = tmp_dir + "/db.cnf"; fs::create_directories(tmp_dir); // DB creds (0600) { std::ofstream cnf(cnf_path); cnf << "[client]\nuser=" << cfg.db_user << "\npassword=" << cfg.db_pass << "\n"; cnf.close(); chmod(cnf_path.c_str(), 0600); } // Архивация std::cout << "📦 Архивация сайта...\n"; if (skip_tar && fs::exists(tar_path) && fs::file_size(tar_path) > 0) { std::cout << "⏭ --skip-tar: найден готовый архив: " << tar_path << " (" << human_size((uint64_t)fs::file_size(tar_path)) << "), пропускаю упаковку.\n"; } else { uint64_t total_bytes = dir_size_bytes(cfg.local_site_dir); std::cout << " Размер каталога: ~" << human_size(total_bytes) << "\n"; int rc_archive = 0; if (has_command("pv")) { std::cout << " Использую pv для прогресса…\n"; std::ostringstream cmd; cmd << "tar -C '" << cfg.local_site_dir << "' -cf - . | pv -s " << total_bytes << " | gzip > '" << tar_path << "'"; rc_archive = run_local(cmd.str()); } else { std::cout << " ℹ️ pv не найден — покажу спиннер (sudo apt install pv)\n"; std::ostringstream cmd; cmd << "tar -czf '" << tar_path << "' -C '" << cfg.local_site_dir << "' ."; rc_archive = run_with_spinner(cmd.str(), "Архивация"); } if (rc_archive != 0) { unlink(cnf_path.c_str()); return 1; } std::cout << "✅ Архив: " << tar_path << "\n"; } // Дамп БД (тихий) std::cout << "🛢️ Дамп базы...\n"; { std::string cmd = "mysqldump --defaults-extra-file='" + cnf_path + "' " "--single-transaction --quick --routines --triggers --events --no-tablespaces " + cfg.db_name + " > '" + sql_path + "' 2> '" + tmp_dir + "/mysqldump.err'"; if (run_local(cmd, /*echo=*/false) != 0) { unlink(cnf_path.c_str()); return 1; } std::cout << "✅ Дамп: " << sql_path << "\n"; } unlink(cnf_path.c_str()); // SSH-сессия ssh_session session = ssh_new(); if (!session) { std::cerr << "❌ ssh_new\n"; return 1; } int timeout = 30; ssh_options_set(session, SSH_OPTIONS_TIMEOUT, &timeout); ssh_options_set(session, SSH_OPTIONS_HOST, cfg.remote_host.c_str()); ssh_options_set(session, SSH_OPTIONS_USER, cfg.remote_user.c_str()); ssh_options_set(session, SSH_OPTIONS_PORT, &cfg.ssh_port); std::cout << "🔌 Подключаемся к " << cfg.remote_host << ":" << cfg.ssh_port << "...\n"; if (ssh_connect(session) != SSH_OK) { std::cerr << "❌ ssh_connect: " << ssh_get_error(session) << "\n"; ssh_free(session); return 1; } std::cout << "✅ Соединение установлено (" << peer_ip(session) << ")\n"; if (ssh_userauth_password(session, nullptr, cfg.remote_pass.c_str()) != SSH_AUTH_SUCCESS) { std::cerr << "❌ Аутентификация: " << ssh_get_error(session) << "\n"; ssh_disconnect(session); ssh_free(session); return 1; } // Проверка места uint64_t need_bytes = 0; try { need_bytes += fs::file_size(tar_path); } catch (...) {} try { need_bytes += fs::file_size(sql_path); } catch (...) {} if (ensure_space_and_bind_mount(session, cfg, need_bytes) != 0) { ssh_disconnect(session); ssh_free(session); return 1; } // Bootstrap if (bootstrap_remote(session, cfg) != 0) std::cerr << "⚠️ Подготовка удалёнки с предупреждениями.\n"; // SFTP sftp_session sftp = sftp_new(session); if (!sftp) { std::cerr << "❌ sftp_new\n"; ssh_disconnect(session); ssh_free(session); return 1; } if (sftp_init(sftp) != SSH_OK) { std::cerr << "❌ sftp_init: " << ssh_get_error(session) << "\n"; sftp_free(sftp); ssh_disconnect(session); ssh_free(session); return 1; } const std::string remote_day = cfg.remote_backup_base + "/" + date; const std::string remote_tar = remote_day + "/site_" + date + ".tar.gz"; const std::string remote_sql = remote_day + "/db_" + date + ".sql"; std::cout << "📂 Готовим удалённую папку: " << remote_day << "\n"; if (sftp_mkdirs(session, sftp, remote_day) != SSH_OK) { std::cerr << "❌ Не удалось создать каталог на удалёнке (SFTP)\n"; sftp_free(sftp); ssh_disconnect(session); ssh_free(session); return 1; } auto remote_file_nonzero = [&](const std::string& path)->bool{ sftp_attributes st = sftp_stat(sftp, path.c_str()); if (!st) return false; bool ok = (st->type == SSH_FILEXFER_TYPE_REGULAR && st->size > 0); sftp_attributes_free(st); return ok; }; bool skip_tar_flag = skip_upload; bool skip_sql_flag = skip_upload; bool need_upload_tar = true, need_upload_sql = true; if (skip_upload) { bool has_tar = remote_file_nonzero(remote_tar); bool has_sql = remote_file_nonzero(remote_sql); if (has_tar && has_sql) { std::cout << "⏭ --skip-upload: на удалёнке уже есть архив и дамп за сегодня, пропускаю загрузку.\n"; need_upload_tar = need_upload_sql = false; } else { if (!has_tar) std::cout << "→ На удалёнке нет архива — придётся загрузить.\n"; if (!has_sql) std::cout << "→ На удалёнке нет дампа — придётся загрузить.\n"; } } if (need_upload_tar) { std::cout << "🚚 Отправка архива...\n"; int sftp_err = SSH_FX_OK; if (sftp_upload_file_progress(session, sftp, tar_path, remote_tar, "Архив", &sftp_err) != 0) { std::cerr << "⚠️ SFTP-сбой (err=" << sftp_err << " " << sftp_errname(sftp_err) << "). Перехожу на SSH-поток.\n"; if (ssh_stream_upload(session, tar_path, remote_tar, "Архив(ssh)") != 0) { std::cerr << "❌ Передача архива по SSH тоже не удалась\n"; sftp_free(sftp); ssh_disconnect(session); ssh_free(session); return 1; } } } else { std::cout << "⏭ Архив: пропущено (файл уже на удалёнке)\n"; } if (need_upload_sql) { std::cout << "🚚 Отправка дампа БД...\n"; int sftp_err = SSH_FX_OK; if (sftp_upload_file_progress(session, sftp, sql_path, remote_sql, "Дамп ", &sftp_err) != 0) { std::cerr << "⚠️ SFTP-сбой (err=" << sftp_err << " " << sftp_errname(sftp_err) << "). Перехожу на SSH-поток.\n"; if (ssh_stream_upload(session, sql_path, remote_sql, "Дамп(ssh)") != 0) { std::cerr << "❌ Передача дампа по SSH тоже не удалась\n"; sftp_free(sftp); ssh_disconnect(session); ssh_free(session); return 1; } } } else { std::cout << "⏭ Дамп: пропущено (файл уже на удалёнке)\n"; } // Развёртывание std::cout << "🧩 Развёртывание на удалённом хосте (" << cfg.target_server << ")...\n"; std::string deploy_cmd = (cfg.target_server == "nginx") ? build_nginx_deploy_cmd(cfg, remote_tar, remote_sql, remote_day) : build_apache_deploy_cmd(cfg, remote_tar, remote_sql, remote_day); if (ssh_exec(session, deploy_cmd, true) != 0) std::cerr << "⚠️ Развёртывание завершилось с ошибкой. Проверь вывод выше.\n"; // Завершение sftp_free(sftp); ssh_disconnect(session); ssh_free(session); std::cout << "\n🎉 Бэкап и передача завершены.\n"; std::cout << "ℹ️ Файлы на удалёнке: " << remote_day << "\n"; return 0; }
🧰 Итого: madbackuper — бэкап и развёртывание на резервный хост
Программа выполняет архивирование сайта и дамп БД локально, передаёт артефакты на хост 192.168.88.202, разворачивает копию, готовит Nginx и переключает фронт-энд на «remote» (обратная прокси) с помощью скрипта /root/setup_madmentat_nginx.sh
. Дополнительно выставляется флаг в вачдоге /webserver/wachdog
для ночной автопереключалки.
Подключаемся по SSH как пользователь madmentat
, затем повышаем права через sudo
. Программа сперва пробует sudo -n
(NOPASSWD), иначе передаёт пароль в stdin для sudo -S
. Для работы создаётся минимальный sudoers в /etc/sudoers.d/madbackuper
(только нужные команды).
Если нет /root/setup_madmentat_nginx.sh
, программа создаёт его из шаблона, делает исполняемым (chmod 700
) и запускает с параметром remote
. Скрипт пишет /etc/nginx/conf.d/madmentat.ru.backend.conf
с переменной $mad_backend
и гарантирует рабочий локальный бэкенд на 127.0.0.1:8081
.
В /webserver/wachdog
ставится madmentat.ru_update=true
. Ночной демон на «202-м» сервере в 03:15 проверяет флаг: если true
— сбрасывает в false
; если уже false
— запускает /root/setup_madmentat_nginx.sh local
(возврат прокси на локальный бэкенд).
🗺️ Схема работы (шаги)
- 📦 Архивация каталога сайта и дамп БД локально (
/tmp/madbackuper_YYYY-MM-DD/
). - 💽 Проверка места на
/webserver
удалёнки. Если мало — программа делает bind-mount$HOME/webserver → /webserver
и дописывает в/etc/fstab
. - 🚚 Передача по SFTP (c прогрессом), при сбое — потоковый SSH-аплоад с прогрессом.
- 🛠️ Подготовка sudoers и каталогов на удалёнке.
- 🧩 Развёртывание на сервере:
- Распаковка архива в
/webserver/madmentat.ru
, праваwww-data
. - MariaDB: CREATE DATABASE/USER/GRANT, мягкий бэкап прежней БД, импорт дампа, счётчик таблиц.
- Nginx: создать/обновить
/root/setup_madmentat_nginx.sh
при отсутствии и запуститьremote
. - Вачдог:
madmentat.ru_update=true
в/webserver/wachdog
.
- Распаковка архива в
🔐 Нюансы авторизации на 202-м сервере
- SSH:
Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в вашем браузере должен быть включен Javascript.
(порт по умолчанию 22). - Повышение прав: сначала попытка
sudo -n
(без пароля). Если NOPASSWD не настроен, программа подаёт пароль на stdin (sudo -S -p ''
), не требуя TTY. - Программа автоматически создаёт минимальный файл
/etc/sudoers.d/madbackuper
с разрешёнными командами (nginx/systemctl/tar/mysql/mount и т.д.) и проверяет егоvisudo -cf
. Это устраняет «затык» с правами root.
⚙️ Файлы и каталоги
/etc/madbackuper.conf
— конфиг (создаётся при первом запуске). Права на пароли: 600./webserver/.backup/YYYY-MM-DD/
— артефакты на удалёнке:site_*.tar.gz
,db_*.sql
./root/setup_madmentat_nginx.sh
— переключалка фронта (remote/local), автогенерируется при необходимости./webserver/wachdog
— флагmadmentat.ru_update=true|false
для ночного демона переключалки.
📝 Конфигурация
Конфиг генерируется автоматически. Любой параметр можно переопределить CLI-ключами (--ключ=значение
).
# /etc/madbackuper.conf (фрагменты)
target_server=nginx
remote_host=192.168.88.202
ssh_port=22
remote_user=madmentat
remote_pass=<пароль пользователя SSH>
remote_sudo_pass=<пароль для sudo> # если пусто → = remote_pass
local_site_dir=/webserver/madmentat.ru
remote_site_dir=/webserver/madmentat.ru
remote_backup_base=/webserver/.backup
server_name=madmentat.ru
php_version=8.3
# php_fpm_sock=/run/php/php8.3-fpm.sock # опционально
db_user=madmentat
db_pass=<пароль БД>
db_name=mad
proxy_target=192.168.88.198
local_http_port=8080
local_https_port=0
switch_to_local=true
🔨 Сборка и запуск
# зависимости (пример для Debian/Ubuntu):
apt install g++ libssh-dev mariadb-client pv nginx-full
# сборка:
g++ -std=c++17 madbackuper.cpp -o /usr/local/bin/madbackuper -lssh
# первый запуск создаст конфиг и выйдет:
/usr/local/bin/madbackuper
# пример рабочего запуска:
sudo /usr/local/bin/madbackuper --skip-tar --skip-upload --target-server=nginx --php-version=8.3
💡 Ключи: --skip-tar
— не архивировать, если архив уже есть; --skip-upload
— не загружать, если файлы уже на удалёнке (по сегодняшней дате).
🌐 Переключалка Nginx (кратко)
remote
: фронт на :443 проксирует на$mad_backend
, записанный в/etc/nginx/conf.d/madmentat.ru.backend.conf
. Значение — обычноhttp://192.168.88.198
.local
: фронт проксирует наhttp://127.0.0.1:8081
, а сам локальный бэкенд обслуживаетroot
сайта через PHP-FPM (unix-socket autodetect).- Внутри скрипта добавляется
include /etc/nginx/sites-enabled/*;
вnginx.conf
при отсутствии, чистятся конфликтыserver_name madmentat.ru
в других файлах, делаетсяnginx -t
иsystemctl reload nginx
.
🎉Новая версия!
С доработочками. Здесь все еще не реализована работа с Апачем на mirror-сервере, но за то юнит system.d на webmain с переключалкой рабочего веб-сервера уже работает! То есть, можно запустить и забыть. В дальнейшем я думаю все-таки справлюсь с конфигом под Апач... Благо опыт уже есть. Посмотрю, что можно сделать с режимами revers_proxy / standalone... Ну и наверно в целом сделаю программу более гибкой и универсальной. Держим пальчики крестиками, чтобы мне не стало лень в связи с тем, что оно и так работает.
// madbackuper.cpp // ------------------------------------------------------------ // Бэкап и развёртывание на резервный хост (Ubuntu/Debian). // Новое в этой версии: // • --skip-upload: пропуск загрузки, если файлы уже на удалёнке // • Проверка наличия nginx без зависимости от PATH // • Привилегированные действия через sudo (учёт отдельного mirror_sudo_pass) // • Nginx: systemctl restart (а не reload), детальные проверки vhost (link, nginx -T) // • БД (MariaDB/MySQL): понятный лог импорта и число таблиц после заливки // • Прогресс при архивации/передаче, проверка места, bind-mount /webserver // • Автосоздание sudoers и нужных каталогов // • Daemon mode для мониторинга и авто-бэкапа // Команда компиляции // g++ -std=c++17 madbackuper.cpp -o madbackuper -lssh // Вариант запуска: // sudo ./madbackuper --skip-tar --skip-upload --target-server=nginx --php-version=8.3 // sudo ./madbackuper --daemon-install // Конфиг-файл с пояснениями: // /etc/madbackuper.conf // ------------------------------------------------------------ #include <libssh/libssh.h> #include <libssh/sftp.h> #include <iostream> #include <fstream> #include <sstream> #include <filesystem> #include <cstdio> #include <cstdlib> #include <ctime> #include <sys/stat.h> #include <fcntl.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #include <sys/socket.h> #include <sys/wait.h> #include <chrono> #include <thread> #include <string> #include <type_traits> #include <iomanip> #include <memory> #include <algorithm> #include <sys/types.h> // for pid_t #include <signal.h> // for daemon signals if needed #include <ctime> // for time #include <cstring> // for strstr namespace fs = std::filesystem; using clock_ = std::chrono::steady_clock; // --------------------------- Константы --------------------------- static const char* CFG_PATH_PRIMARY = "/etc/madbackuper.conf"; static const char* CFG_PATH_FALLBACK = "/root/madbackuper.conf"; static const char* DEFAULT_TARGET = "nginx"; // или "apache2" static const int DEFAULT_MIRROR_SSH_PORT = 22; static const int DEFAULT_MIRROR_HTTP_PORT = 8081; static const int DEFAULT_WEBMAIN_HTTP_PORT = 80; static const int DEFAULT_MIRROR_HTTPS_PORT = 0; static const bool DEFAULT_SWITCH_TO_MIRROR = true; static const std::string DEFAULT_PHP_VERSION = "8.3"; static const std::string DEFAULT_MIRROR_MODE = "reverse_proxy"; // new static const std::string DAEMON_LOG_PATH = "/var/log/madbackuper.log"; static const std::string DAEMON_STATE_PATH = "/var/run/madbackuper.state"; static const std::string SERVICE_FILE = "/etc/systemd/system/madbackuper.service"; static const int DEFAULT_DAEMON_CHECK_DELAY = 60; // new static const int BACKUP_INTERVAL_SEC = 3600; // 1 hour // ===== base64 encoder (локальный, без зависимостей) ===== static const char B64_TABLE[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; static std::string base64_encode(const std::string &in) { std::string out; size_t i = 0; unsigned char a3[3]{}; unsigned char a4[4]{}; for (unsigned char c : in) { a3[i++] = c; if (i == 3) { a4[0] = (a3[0] & 0xfc) >> 2; a4[1] = ((a3[0] & 0x03) << 4) + ((a3[1] & 0xf0) >> 4); a4[2] = ((a3[1] & 0x0f) << 2) + ((a3[2] & 0xc0) >> 6); a4[3] = a3[2] & 0x3f; for (int j = 0; j < 4; j++) out.push_back(B64_TABLE[a4[j]]); i = 0; } } if (i) { for (size_t j = i; j < 3; j++) a3[j] = '\0'; a4[0] = (a3[0] & 0xfc) >> 2; a4[1] = ((a3[0] & 0x03) << 4) + ((a3[1] & 0xf0) >> 4); a4[2] = ((a3[1] & 0x0f) << 2) + ((a3[2] & 0xc0) >> 6); a4[3] = a3[2] & 0x3f; for (size_t j = 0; j < i + 1; j++) out.push_back(B64_TABLE[a4[j]]); while ((i++ < 3)) out.push_back('='); } return out; } // --------------------------- Конфиг --------------------------- struct Config { std::string target_server_name = DEFAULT_TARGET; // nginx | apache2 std::string mirror_host = "192.168.88.202"; int mirror_ssh_port = DEFAULT_MIRROR_SSH_PORT; std::string mirror_user = "madmentat"; std::string mirror_user_pass = "Rctybz82"; // SSH-пароль пользователя std::string mirror_sudo_pass = "Rctybz82"; // пароль для sudo (если отличается). Пусто => mirror_user_pass std::string mirror_root_pass = "Rctybz82"; // пароль для root (если отличается). Пусто => mirror_user_pass // Локальный сайт (что бэкапим на webmain) std::string webmain_site_dir = "/webserver/madmentat.ru"; // Удалённый сайт (куда раскатываем на mirror) std::string mirror_site_dir = "/webserver/madmentat.ru"; // Папка для хранения бэкапов на mirror std::string mirror_backup_base = "/webserver/.backup"; // Веб-название (vhost / server_name) std::string server_name = "madmentat.ru"; // PHP std::string php_version = DEFAULT_PHP_VERSION; std::string php_fpm_sock = ""; // если задано, перебивает php_version // База std::string db_user = "madmentat"; std::string db_pass = "Rctybz82"; std::string db_name = "mad"; // Прокси/переключение std::string webmain_ip = "192.168.88.198"; int webmain_http_port = DEFAULT_WEBMAIN_HTTP_PORT; int mirror_http_port = DEFAULT_MIRROR_HTTP_PORT; int mirror_https_port = DEFAULT_MIRROR_HTTPS_PORT; bool switch_to_mirror = DEFAULT_SWITCH_TO_MIRROR; // SSL для HTTPS на mirror (если mirror_https_port > 0) std::string ssl_cert = ""; std::string ssl_key = ""; // New for daemon and mode std::string mirror_server_mode = DEFAULT_MIRROR_MODE; // reverse_proxy | standalone bool no_switch = false; // for periodic backup without switching int daemon_check_delay = DEFAULT_DAEMON_CHECK_DELAY; // new }; // --------------------------- Вспомогалки --------------------------- static std::string trim(const std::string& s) { auto l = s.find_first_not_of(" \t\r\n"); auto r = s.find_last_not_of(" \t\r\n"); if (l == std::string::npos) return {}; return s.substr(l, r - l + 1); } static std::string today() { char buf[16]; std::time_t t = std::time(nullptr); std::tm tm{}; localtime_r(&t, &tm); std::strftime(buf, sizeof(buf), "%Y-%m-%d", &tm); return std::string(buf); } static std::string human_size(uint64_t bytes) { const char* u[] = {"B","KB","MB","GB","TB","PB"}; double v = static_cast<double>(bytes); int i = 0; while (v >= 1024.0 && i < 5) { v /= 1024.0; ++i; } char buf[64]; std::snprintf(buf, sizeof(buf), "%.1f %s", v, u[i]); return buf; } static uint64_t dir_size_bytes(const fs::path& root) { uint64_t total = 0; std::error_code ec; for (fs::recursive_directory_iterator it(root, fs::directory_options::skip_permission_denied, ec), end; it != end; ++it) { if (ec) { ec.clear(); continue; } std::error_code ec2; if (fs::is_regular_file(*it, ec2)) total += fs::file_size(*it, ec2); } return total; } static bool has_command(const char* name) { std::string cmd = "command -v "; cmd += name; cmd += " >/dev/null 2>&1"; return std::system(cmd.c_str()) == 0; } // run_local с флагом «печатать команду» static int run_local(const std::string& cmd, bool echo = true) { if (echo) std::cout << "➜ " << cmd << "\n"; int rc = std::system(cmd.c_str()); if (rc != 0) std::cerr << "❌ Команда вернула код " << rc << "\n"; return rc; } // Спиннер для долгих задач (без вывода команды) static int run_with_spinner(const std::string& cmd, const std::string& label) { pid_t pid = fork(); if (pid < 0) { std::cerr << "❌ fork() для: " << cmd << "\n"; return -1; } if (pid == 0) { execl("/bin/sh", "sh", "-lc", cmd.c_str(), (char*)nullptr); _exit(127); } auto start = clock_::now(); const char* frames[] = {"⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏"}; size_t idx = 0; int status = 0; while (true) { pid_t r = waitpid(pid, &status, WNOHANG); if (r == 0) { auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(clock_::now() - start).count(); std::cout << "\r" << label << " " << frames[idx % 10] << " " << elapsed << "s" << std::flush; idx++; std::this_thread::sleep_for(std::chrono::milliseconds(120)); } else break; } std::cout << "\r" << label << " ✓ " << std::endl; if (!WIFEXITED(status)) { std::cerr << "❌ Процесс завершён ненормально\n"; return -1; } int rc = WEXITSTATUS(status); if (rc != 0) std::cerr << "❌ Команда вернула код " << rc << "\n"; return rc; } static std::string peer_ip(ssh_session s) { int sock = ssh_get_fd(s); sockaddr_storage addr{}; socklen_t len = sizeof(addr); if (getpeername(sock, (sockaddr*)&addr, &len) != 0) return "unknown"; char ip[INET6_ADDRSTRLEN]{}; if (addr.ss_family == AF_INET) { auto* a = (sockaddr_in*)&addr; inet_ntop(AF_INET, &a->sin_addr, ip, sizeof(ip)); } else if (addr.ss_family == AF_INET6) { auto* a = (sockaddr_in6*)&addr; inet_ntop(AF_INET6, &a->sin6_addr, ip, sizeof(ip)); } else return "unknown"; return ip; } // --------------------------- SSH helpers --------------------------- static int ssh_exec(ssh_session session, const std::string& cmd, bool print_out = true) { ssh_channel ch = ssh_channel_new(session); if (!ch) { std::cerr << "❌ ssh_channel_new: " << ssh_get_error(session) << "\n"; return -1; } if (ssh_channel_open_session(ch) != SSH_OK) { std::cerr << "❌ ssh_channel_open_session: " << ssh_get_error(session) << "\n"; ssh_channel_free(ch); return -1; } if (ssh_channel_request_exec(ch, cmd.c_str()) != SSH_OK) { std::cerr << "❌ ssh_channel_request_exec: " << ssh_get_error(session) << "\n"; ssh_channel_close(ch); ssh_channel_free(ch); return -1; } if (print_out) { char buf[4096]; int n; while ((n = ssh_channel_read(ch, buf, sizeof(buf), 0)) > 0) std::cout.write(buf, n); while ((n = ssh_channel_read(ch, buf, sizeof(buf), 1)) > 0) std::cerr.write(buf, n); } else { char buf[4096]; int n; while ((n = ssh_channel_read(ch, buf, sizeof(buf), 0)) > 0) {} while ((n = ssh_channel_read(ch, buf, sizeof(buf), 1)) > 0) {} } ssh_channel_send_eof(ch); int exit_status = ssh_channel_get_exit_status(ch); ssh_channel_close(ch); ssh_channel_free(ch); return exit_status; } static int ssh_exec_capture(ssh_session session, const std::string& cmd, std::string& out, std::string* err=nullptr) { out.clear(); if (err) err->clear(); ssh_channel ch = ssh_channel_new(session); if (!ch) return -1; if (ssh_channel_open_session(ch) != SSH_OK) { ssh_channel_free(ch); return -1; } if (ssh_channel_request_exec(ch, cmd.c_str()) != SSH_OK) { ssh_channel_close(ch); ssh_channel_free(ch); return -1; } char buf[4096]; int n; while ((n = ssh_channel_read(ch, buf, sizeof(buf), 0)) > 0) out.append(buf, n); if (err) while ((n = ssh_channel_read(ch, buf, sizeof(buf), 1)) > 0) err->append(buf, n); ssh_channel_send_eof(ch); int exit_status = ssh_channel_get_exit_status(ch); ssh_channel_close(ch); ssh_channel_free(ch); return exit_status; } // --------------------------- Конфиг I/O --------------------------- static const char* CFG_HEADER = R"(# madbackuper.conf # ------------------------------------------------------------ # Конфигурация для madbackuper (автогенерируется при первом запуске). # Все параметры можно переопределить CLI-ключами. # ------------------------------------------------------------ # Основные параметры: # target_server_name=nginx # допустимо: nginx | apache2 # mirror_host=192.168.88.202 # mirror_ssh_port=22 # mirror_user=madmentat # mirror_user_pass=Rctybz82 # пароль для SSH-подключения пользователя # mirror_sudo_pass= # пароль, который спрашивает sudo; если пусто — берётся mirror_user_pass # mirror_root_pass= # пароль для root (если отличается). Пусто => mirror_user_pass # mirror_server_mode=reverse_proxy # reverse_proxy | standalone — режим mirror: прокси с failover или standalone # daemon_check_delay=60 # интервал проверки в демоне (сек) # Пути: # webmain_site_dir=/webserver/madmentat.ru # на webmain: что архивируем # mirror_site_dir=/webserver/madmentat.ru # на mirror: куда разворачиваем # mirror_backup_base=/webserver/.backup # на mirror: где хранить бэкапы # # Веб-название: # server_name=madmentat.ru # # PHP: # php_version=8.3 # # или: # # php_fpm_sock=/run/php/php8.3-fpm.sock # # База: # db_user=madmentat # db_pass=Rctybz82 # db_name=mad # # Прокси/переключение (nginx): # webmain_ip=192.168.88.198 # webmain_http_port=80 # mirror_http_port=8081 # mirror_https_port=0 # switch_to_mirror=true # ssl_cert=/path/to/cert.crt # ssl_key=/path/to/key.key # # Примеры: # ./madbackuper --target-server=nginx --switch-to-mirror=true # ./madbackuper --skip-tar # ./madbackuper --skip-upload # ./madbackuper --daemon-install # ------------------------------------------------------------ )"; static void write_default_config(const std::string& path) { std::ofstream out(path); out << CFG_HEADER << "target_server_name=" << DEFAULT_TARGET << "\n" << "mirror_host=" << "192.168.88.202" << "\n" << "mirror_ssh_port=" << DEFAULT_MIRROR_SSH_PORT << "\n" << "mirror_user=" << "madmentat" << "\n" << "mirror_user_pass=" << "Rctybz82" << "\n" << "mirror_root_pass=" << "Rctybz82" << "\n" << "mirror_server_mode=" << DEFAULT_MIRROR_MODE << "\n" << "daemon_check_delay=" << DEFAULT_DAEMON_CHECK_DELAY << "\n" << "webmain_site_dir=" << "/webserver/madmentat.ru" << "\n" << "mirror_site_dir=" << "/webserver/madmentat.ru" << "\n" << "mirror_backup_base=" << "/webserver/.backup" << "\n" << "server_name=" << "madmentat.ru" << "\n" << "php_version=" << DEFAULT_PHP_VERSION << "\n" << "php_fpm_sock=" << "" << "\n" << "db_user=" << "madmentat" << "\n" << "db_pass=" << "Rctybz82" << "\n" << "db_name=" << "mad" << "\n" << "webmain_ip=" << "192.168.88.198" << "\n" << "webmain_http_port=" << DEFAULT_WEBMAIN_HTTP_PORT << "\n" << "mirror_http_port=" << DEFAULT_MIRROR_HTTP_PORT << "\n" << "mirror_https_port=" << DEFAULT_MIRROR_HTTPS_PORT << "\n" << "switch_to_mirror=" << "true" << "\n" << "ssl_cert=" << "" << "\n" << "ssl_key=" << "" << "\n"; out.close(); } static void load_kv_file(const std::string& path, Config& cfg) { std::ifstream in(path); if (!in) return; std::string line; while (std::getline(in, line)) { line = trim(line); if (line.empty() || line[0]=='#' || line[0]==';') continue; auto eq = line.find('='); if (eq == std::string::npos) continue; auto k = trim(line.substr(0, eq)); auto v = trim(line.substr(eq+1)); if (k=="target_server_name") cfg.target_server_name = v; else if (k=="mirror_host") cfg.mirror_host = v; else if (k=="mirror_ssh_port") cfg.mirror_ssh_port = std::stoi(v); else if (k=="mirror_user") cfg.mirror_user = v; else if (k=="mirror_user_pass") cfg.mirror_user_pass = v; else if (k=="mirror_root_pass") cfg.mirror_root_pass = v; else if (k=="mirror_sudo_pass") cfg.mirror_sudo_pass = v; else if (k=="mirror_server_mode") cfg.mirror_server_mode = v; else if (k=="daemon_check_delay") cfg.daemon_check_delay = std::stoi(v); else if (k=="webmain_site_dir") cfg.webmain_site_dir = v; else if (k=="mirror_site_dir") cfg.mirror_site_dir = v; else if (k=="mirror_backup_base") cfg.mirror_backup_base = v; else if (k=="server_name") cfg.server_name = v; else if (k=="php_version") cfg.php_version = v; else if (k=="php_fpm_sock") cfg.php_fpm_sock = v; else if (k=="db_user") cfg.db_user = v; else if (k=="db_pass") cfg.db_pass = v; else if (k=="db_name") cfg.db_name = v; else if (k=="webmain_ip") cfg.webmain_ip = v; else if (k=="webmain_http_port") cfg.webmain_http_port = std::stoi(v); else if (k=="mirror_http_port") cfg.mirror_http_port = std::stoi(v); else if (k=="mirror_https_port") cfg.mirror_https_port = std::stoi(v); else if (k=="switch_to_mirror") cfg.switch_to_mirror = (v=="true" || v=="1" || v=="yes"); else if (k=="ssl_cert") cfg.ssl_cert = v; else if (k=="ssl_key") cfg.ssl_key = v; } } static void apply_cli_kv(int argc, char** argv, Config& cfg) { auto eat = [&](const std::string& arg, const char* key, auto& dst) { std::string p = std::string("--") + key + "="; if (arg.rfind(p, 0) == 0) { std::string val = arg.substr(p.size()); if constexpr(std::is_same_v<decltype(dst), int&>) dst = std::stoi(val); else if constexpr(std::is_same_v<decltype(dst), bool&>) dst = (val=="true" || val=="1" || val=="yes"); else dst = val; return true; } return false; }; for (int i=1; i<argc; ++i) { std::string a = argv[i]; if (eat(a, "target-server", cfg.target_server_name)) continue; if (eat(a, "mirror-host", cfg.mirror_host)) continue; if (eat(a, "ssh-port", cfg.mirror_ssh_port)) continue; if (eat(a, "mirror-user", cfg.mirror_user)) continue; if (eat(a, "mirror-pass", cfg.mirror_user_pass)) continue; if (eat(a, "mirror-root-pass", cfg.mirror_root_pass)) continue; if (eat(a, "mirror-sudo-pass", cfg.mirror_sudo_pass)) continue; if (eat(a, "mirror-server-mode", cfg.mirror_server_mode)) continue; if (eat(a, "daemon-check-delay", cfg.daemon_check_delay)) continue; if (eat(a, "webmain-site-dir", cfg.webmain_site_dir)) continue; if (eat(a, "mirror-site-dir", cfg.mirror_site_dir)) continue; if (eat(a, "mirror-backup-base", cfg.mirror_backup_base)) continue; if (eat(a, "server-name", cfg.server_name)) continue; if (eat(a, "php-version", cfg.php_version)) continue; if (eat(a, "php-fpm-sock", cfg.php_fpm_sock)) continue; if (eat(a, "db-user", cfg.db_user)) continue; if (eat(a, "db-pass", cfg.db_pass)) continue; if (eat(a, "db-name", cfg.db_name)) continue; if (eat(a, "webmain-ip", cfg.webmain_ip)) continue; if (eat(a, "webmain-http-port", cfg.webmain_http_port)) continue; if (eat(a, "mirror-http-port", cfg.mirror_http_port)) continue; if (eat(a, "mirror-https-port", cfg.mirror_https_port)) continue; if (eat(a, "switch-to-mirror", cfg.switch_to_mirror)) continue; if (eat(a, "ssl-cert", cfg.ssl_cert)) continue; if (eat(a, "ssl-key", cfg.ssl_key)) continue; } } // --------------------------- Проверка параметров --------------------------- static bool validate(const Config& c, std::string& err) { auto notEmpty = [&](const std::string& v, const char* name)->bool { if (v.empty()) { err = std::string("Параметр пуст: ") + name; return false; } return true; }; if (!(c.target_server_name=="nginx" || c.target_server_name=="apache2")) { err = "target_server_name: nginx|apache2"; return false; } if (!notEmpty(c.mirror_host, "mirror_host")) return false; if (!notEmpty(c.mirror_user, "mirror_user")) return false; if (!notEmpty(c.mirror_user_pass, "mirror_user_pass")) return false; // mirror_sudo_pass может быть пуст — возьмём mirror_user_pass if (!notEmpty(c.webmain_site_dir, "webmain_site_dir")) return false; if (!notEmpty(c.mirror_site_dir, "mirror_site_dir")) return false; if (!notEmpty(c.mirror_backup_base, "mirror_backup_base")) return false; if (!notEmpty(c.server_name, "server_name")) return false; if (c.php_fpm_sock.empty() && !notEmpty(c.php_version, "php_version")) return false; if (!notEmpty(c.db_user, "db_user")) return false; if (!notEmpty(c.db_pass, "db_pass")) return false; if (!notEmpty(c.db_name, "db_name")) return false; if (!notEmpty(c.webmain_ip, "webmain_ip")) return false; if (c.webmain_http_port <= 0 || c.webmain_http_port == 443) { err = "webmain_http_port >0 и не 443"; return false; } if (c.mirror_http_port <= 0 || c.mirror_http_port == 80 || c.mirror_http_port == 443) { err = "mirror_http_port >0 и не 80/443"; return false; } if (c.mirror_https_port > 0) { if (!notEmpty(c.ssl_cert, "ssl_cert")) return false; if (!notEmpty(c.ssl_key, "ssl_key")) return false; if (c.mirror_https_port == 80 || c.mirror_https_port == 443 || c.mirror_https_port == c.mirror_http_port) { err = "mirror_https_port >0, не 80/443 и ≠ mirror_http_port"; return false; } } if (!(c.mirror_server_mode == "reverse_proxy" || c.mirror_server_mode == "standalone")) { err = "mirror_server_mode: reverse_proxy|standalone"; return false; } if (c.daemon_check_delay < 1) { err = "daemon_check_delay >0"; return false; } return true; } // --------------------------- SFTP helpers --------------------------- static const char* sftp_errname(int code) { switch (code) { case SSH_FX_OK: return "OK"; case SSH_FX_EOF: return "EOF"; case SSH_FX_NO_SUCH_FILE: return "NO_SUCH_FILE"; case SSH_FX_PERMISSION_DENIED: return "PERMISSION_DENIED"; case SSH_FX_FAILURE: return "FAILURE"; case SSH_FX_BAD_MESSAGE: return "BAD_MESSAGE"; case SSH_FX_NO_CONNECTION: return "NO_CONNECTION"; case SSH_FX_CONNECTION_LOST: return "CONNECTION_LOST"; case SSH_FX_OP_UNSUPPORTED: return "OP_UNSUPPORTED"; default: return "UNKNOWN"; } } static int sftp_mkdirs(ssh_session session, sftp_session sftp, const std::string& path, mode_t mode = 0755) { if (path.empty()) return SSH_OK; std::string cur; for (size_t i = 1; i <= path.size(); ++i) { if (i==path.size() || path[i]=='/') { cur = path.substr(0, i); if (cur.empty()) continue; int rc = sftp_mkdir(sftp, cur.c_str(), mode); if (rc != SSH_OK) { int err = sftp_get_error(sftp); if (err != SSH_FX_FILE_ALREADY_EXISTS) { std::cerr << "❌ sftp_mkdir(" << cur << "): err " << err << " (" << sftp_errname(err) << ") - " << ssh_get_error(session) << "\n"; return rc; } } } } return SSH_OK; } static void print_progress_line(const std::string& label, uint64_t sent, uint64_t total, double mbps) { double ratio = total ? (double)sent / (double)total : 0.0; int width = 28; int fill = (int)(ratio * width); uint64_t remain = total > sent ? (total - sent) : 0; double eta = (mbps>0) ? (remain/1024.0/1024.0)/mbps : 0.0; std::cout << "\r" << label << " ["; for (int i=0;i<width;i++) std::cout << (i<fill ? "█" : " "); std::cout << "] " << std::fixed << std::setprecision(1) << (ratio*100.0) << "% " << std::setprecision(2) << mbps << " MB/s " << human_size(sent) << "/" << human_size(total) << " ETA " << std::setprecision(0) << eta << "s" << std::flush; } // SFTP-передача файла (64 KB блоки) с прогрессом static int sftp_upload_file_progress(ssh_session session, sftp_session sftp, const std::string& local, const std::string& remote, const std::string& label, int* out_err=nullptr, mode_t mode = 0644) { if (out_err) *out_err = SSH_FX_OK; std::ifstream in(local, std::ios::binary); if (!in) { std::cerr << "❌ Не открыть локальный файл: " << local << "\n"; return -1; } uint64_t total = 0; try { total = fs::file_size(local); } catch (...) { total = 0; } sftp_file f = sftp_open(sftp, remote.c_str(), O_WRONLY | O_CREAT | O_TRUNC, mode); if (!f) { int err = sftp_get_error(sftp); if (out_err) *out_err = err; std::cerr << "❌ sftp_open(" << remote << "): " << ssh_get_error(session) << " | SFTP err=" << err << " (" << sftp_errname(err) << ")\n"; return -1; } const size_t BUFSZ = 64 * 1024; // безопасно для любых реализаций SFTP std::unique_ptr<char[]> buf(new char[BUFSZ]); uint64_t sent = 0; auto t0 = clock_::now(); auto last = t0; while (in) { in.read(buf.get(), BUFSZ); std::streamsize left = in.gcount(); char* p = buf.get(); while (left > 0) { ssize_t wr = sftp_write(f, p, left); if (wr < 0) { int err = sftp_get_error(sftp); if (out_err) *out_err = err; std::cerr << "\n❌ sftp_write(" << remote << "): " << ssh_get_error(session) << " | SFTP err=" << err << " (" << sftp_errname(err) << ")\n"; sftp_close(f); return -1; } p += wr; left -= wr; sent += wr; auto now = clock_::now(); auto dt_ms = std::chrono::duration_cast<std::chrono::milliseconds>(now - last).count(); if (dt_ms >= 250 || sent == total) { double secs = std::chrono::duration<double>(now - t0).count(); double mbps = secs>0 ? (sent/1024.0/1024.0)/secs : 0.0; print_progress_line(label, sent, total, mbps); last = now; } } } sftp_close(f); std::cout << "\r" << label << " [████████████████████████████] 100.0% ✓ " << human_size(sent) << " " << std::endl; return 0; } // Резервная передача через обычный SSH-канал (cat > file.part), тоже с прогрессом static int ssh_stream_upload(ssh_session session, const std::string& local, const std::string& remote, const std::string& label) { std::string remote_part = remote + ".part"; ssh_channel ch = ssh_channel_new(session); if (!ch) { std::cerr << "❌ ssh_channel_new\n"; return -1; } if (ssh_channel_open_session(ch) != SSH_OK) { std::cerr << "❌ ssh_channel_open_session: " << ssh_get_error(session) << "\n"; ssh_channel_free(ch); return -1; } std::string cmd = "sh -lc 'umask 022; cat > " + remote_part + "'"; if (ssh_channel_request_exec(ch, cmd.c_str()) != SSH_OK) { std::cerr << "❌ ssh_channel_request_exec (cat): " << ssh_get_error(session) << "\n"; ssh_channel_close(ch); ssh_channel_free(ch); return -1; } std::ifstream in(local, std::ios::binary); if (!in) { std::cerr << "❌ Не открыть локальный файл: " << local << "\n"; ssh_channel_close(ch); ssh_channel_free(ch); return -1; } uint64_t total = 0; try { total = fs::file_size(local); } catch (...) { total = 0; } const size_t BUFSZ = 128 * 1024; // 128 KB std::unique_ptr<char[]> buf(new char[BUFSZ]); uint64_t sent = 0; auto t0 = clock_::now(); auto last = t0; while (in) { in.read(buf.get(), BUFSZ); std::streamsize n = in.gcount(); if (n <= 0) break; const char* p = buf.get(); while (n > 0) { int wr = ssh_channel_write(ch, p, (uint32_t)n); if (wr == SSH_ERROR) { std::cerr << "\n❌ ssh_channel_write: " << ssh_get_error(session) << "\n"; ssh_channel_send_eof(ch); ssh_channel_close(ch); ssh_channel_free(ch); return -1; } p += wr; n -= wr; sent += wr; auto now = clock_::now(); auto dt_ms = std::chrono::duration_cast<std::chrono::milliseconds>(now - last).count(); if (dt_ms >= 250 || sent == total) { double secs = std::chrono::duration<double>(now - t0).count(); double mbps = secs>0 ? (sent/1024.0/1024.0)/secs : 0.0; print_progress_line(label, sent, total, mbps); last = now; } } } ssh_channel_send_eof(ch); int exit_status = ssh_channel_get_exit_status(ch); ssh_channel_close(ch); ssh_channel_free(ch); std::cout << "\r" << label << " [████████████████████████████] 100.0% ✓ " << human_size(sent) << " " << std::endl; if (exit_status != 0) { std::cerr << "❌ Удалённая команда cat завершилась с кодом " << exit_status << "\n"; return -1; } // атомарное переименование .part -> финальное имя { std::ostringstream mv; mv << "sh -lc 'mv -f " << remote_part << " " << remote << "'"; if (ssh_exec(session, mv.str(), true) != 0) { std::cerr << "❌ Не удалось переименовать " << remote_part << " -> " << remote << "\n"; return -1; } } return 0; } // --------------------------- Префикс sudo и пр. --------------------------- static std::string sh_escape_single(const std::string& s) { std::string out; out.reserve(s.size()+8); for (char c: s) { if (c=='\'') out += "'\\''"; else out += c; } return out; } static std::string sudo_prefix(ssh_session session, const Config& C) { if (C.mirror_user == "root") return ""; // 1) Пробуем без пароля (NOPASSWD) { ssh_channel ch = ssh_channel_new(session); if (ch && ssh_channel_open_session(ch) == SSH_OK && ssh_channel_request_exec(ch, "sudo -n true") == SSH_OK) { ssh_channel_send_eof(ch); int rc = ssh_channel_get_exit_status(ch); ssh_channel_close(ch); ssh_channel_free(ch); if (rc == 0) return "sudo -n"; } else if (ch) { ssh_channel_close(ch); ssh_channel_free(ch); } } // 2) Fallback: пароль через stdin (без TTY) std::string sudopw = C.mirror_sudo_pass.empty() ? C.mirror_user_pass : C.mirror_sudo_pass; return "echo '" + sh_escape_single(sudopw) + "' | sudo -S -p ''"; } // --------------------------- Проверка места и bootstrap --------------------------- static uint64_t remote_bytes_avail(ssh_session session, const std::string& path) { std::string out, err; int rc = ssh_exec_capture(session, "df -B1 --output=avail '" + path + "' 2>/dev/null | tail -n1 | tr -d '[:space:]'", out, &err); if (rc != 0 || out.empty()) return 0; try { return std::stoull(trim(out)); } catch (...) { return 0; } } static int ensure_space_and_bind_mount(ssh_session session, const Config& C, uint64_t need_bytes) { std::cout << "💽 Проверка свободного места на mirror...\n"; uint64_t avail = remote_bytes_avail(session, "/webserver"); if (avail == 0) { std::string SUDO = sudo_prefix(session, C); ssh_exec(session, (SUDO.empty()? "" : (SUDO + " ")) + "mkdir -p /webserver", false); avail = remote_bytes_avail(session, "/webserver"); } uint64_t headroom = std::max<uint64_t>(need_bytes / 10, 512ULL*1024*1024); // 10% или 512MB uint64_t need_total = need_bytes + headroom; std::cout << " Нужно ~" << human_size(need_total) << ", доступно ~" << human_size(avail) << "\n"; if (avail >= need_total) { std::cout << "✅ Места достаточно на текущем разделе.\n"; return 0; } std::cout << "⚠️ Места недостаточно — bind-mount $HOME/webserver -> /webserver...\n"; std::string SUDO = sudo_prefix(session, C); std::string home; { std::string out; if (ssh_exec_capture(session, "printf %s \"$HOME\"", out, nullptr) == 0 && !trim(out).empty()) home = trim(out); else { std::string out2; if (ssh_exec_capture(session, "getent passwd \"$USER\" | cut -d: -f6", out2, nullptr) == 0 && !trim(out2).empty()) home = trim(out2); } if (home.empty()) home = "/home/" + C.mirror_user; } std::string prep = (SUDO.empty()? "" : (SUDO + " ")) + "mkdir -p '" + home + "/webserver' /webserver && " + (C.mirror_user=="root" ? "true" : (SUDO.empty()? "" : (SUDO + " ")) + "chown -R " + C.mirror_user + ":" + C.mirror_user + " '" + home + "/webserver'") + " && " + (SUDO.empty()? "" : (SUDO + " ")) + "mountpoint -q /webserver || " + (SUDO.empty()? "" : (SUDO + " ")) + "mount --bind '" + home + "/webserver' /webserver && " + "echo '" + home + "/webserver /webserver none bind 0 0' | " + (SUDO.empty()? "" : (SUDO + " ")) + "tee -a /etc/fstab >/dev/null"; if (ssh_exec(session, prep, true) != 0) { std::cerr << "❌ Не удалось выполнить bind-mount /webserver\n"; return 1; } uint64_t avail2 = remote_bytes_avail(session, "/webserver"); std::cout << " После монтирования доступно ~" << human_size(avail2) << "\n"; if (avail2 < need_total) { std::cerr << "❌ Даже после bind-mount места недостаточно.\n"; return 1; } std::cout << "✅ Места достаточно после bind-mount.\n"; return 0; } static int bootstrap_remote(ssh_session session, const Config& C) { std::cout << "🛠️ Подготовка mirror хоста...\n"; std::string SUDO = sudo_prefix(session, C); // sudoers if (C.mirror_user != "root") { const std::string sudoers = "/etc/sudoers.d/madbackuper"; std::ostringstream content; content << "Cmnd_Alias MADBACKUP_CMDS = " << "/usr/sbin/nginx, /usr/sbin/nginx -t, " << "/bin/systemctl reload nginx, /bin/systemctl restart nginx, " << "/usr/sbin/apache2ctl, /bin/systemctl reload apache2, /bin/systemctl restart apache2, " << "/usr/bin/tee, /bin/mkdir, /bin/chown, /bin/chmod, /bin/ln, /bin/cp, /bin/mv, /bin/tar, /usr/bin/find, " << "/bin/mount, /bin/umount, /bin/mountpoint, /usr/bin/grep, /usr/bin/cut, /usr/bin/getent, " << "/usr/bin/mysql, /usr/bin/mysqldump\n" << C.mirror_user << " ALL=(root) NOPASSWD: MADBACKUP_CMDS\n"; std::ostringstream ensure; ensure << "if [ -f '" << sudoers << "' ]; then " << " if ! grep -q 'MADBACKUP_CMDS' '" << sudoers << "'; then NEED=1; else " << " for k in nginx mount mountpoint systemctl tar tee mkdir chown chmod ln mv cp find mysql mysqldump; do " << " grep -q \"$k\" '" << sudoers << "' || { NEED=1; break; }; " << " done; " << " fi; " << "else NEED=1; fi; " << "if [ \"$NEED\" = 1 ]; then " << " printf '%s' '" << sh_escape_single(content.str()) << "' | " << SUDO << " tee '" << sudoers << "' >/dev/null && " << SUDO << " chmod 440 '" << sudoers << "' && " << SUDO << " visudo -cf '" << sudoers << "'; " << "else echo '→ sudoers уже корректный: " << sudoers << "'; fi"; ssh_exec(session, ensure.str(), true); } // каталоги и права { std::ostringstream cmd; cmd << (SUDO.empty()? "" : (SUDO + " ")) << "mkdir -p '" << C.mirror_backup_base << "' '" << C.mirror_site_dir << "' && "; if (C.mirror_user != "root") { std::string chown_cmd = (SUDO.empty()? "" : (SUDO + " ")); chown_cmd += "chown -R " + C.mirror_user + ":" + C.mirror_user + " '" + C.mirror_backup_base + "'"; cmd << chown_cmd; } else { cmd << "true"; } if (ssh_exec(session, cmd.str(), true) != 0) { std::cerr << "❌ Не удалось подготовить каталоги на mirror\n"; return 1; } } std::cout << "✅ Mirror хост подготовлен\n"; return 0; } // --------------------------- Развёртывание командой (NGINX) --------------------------- static std::string build_nginx_deploy_cmd(const Config& C, const std::string& remote_tar, const std::string& remote_sql, const std::string& remote_day) { auto dq = [](const std::string& s) { std::string r; r.reserve(s.size()*2); for (char c : s) { if (c == '\\' || c == '"') r.push_back('\\'); r.push_back(c); } return r; }; // Пути к нашим (больше не используем для генерации vhost'ов вручную) const std::string php_sock_cfg = C.php_fpm_sock.empty() ? ("/run/php/php" + C.php_version + "-fpm.sock") : C.php_fpm_sock; std::ostringstream root; root << "set -e; umask 022;\n" << "echo \"🔑 Переключаюсь на пользователя root\" 1>&2;\n" << "echo \"🔧 Проверяю окружение (nginx/mysql/tar)\" 1>&2;\n" << "command -v tar >/dev/null || { echo \"❌ tar не установлен\" 1>&2; exit 1; }\n" << "command -v mysql >/dev/null || { echo \"❌ mysql клиент не установлен\" 1>&2; exit 1; }\n" << "NGINX_OK=0; if [ -x /usr/sbin/nginx ]; then NGINX_OK=1; fi;\n" << "command -v nginx >/dev/null 2>&1 && NGINX_OK=1;\n" << "systemctl -q is-active nginx >/dev/null 2>&1 && NGINX_OK=1;\n" << "[ -d /etc/nginx ] && NGINX_OK=1;\n" << "if [ \"$NGINX_OK\" -ne 1 ]; then echo \"❌ nginx не установлен\" 1>&2; exit 1; fi;\n" << "echo \"📦 Подготавливаю директории сайта\" 1>&2;\n" << "mkdir -p \"" << dq(C.mirror_site_dir) << "\"\n" << "rm -rf \"" << dq(C.mirror_site_dir) << ".old\"\n" << "mv \"" << dq(C.mirror_site_dir) << "\" \"" << dq(C.mirror_site_dir) << ".old\" 2>/dev/null || true\n" << "mkdir -p \"" << dq(C.mirror_site_dir) << "\"\n" << "echo \"📤 Распаковка архива сайта\" 1>&2;\n" << "if command -v pv >/dev/null 2>&1; then " "pv -f -p -t -e -r -b \"" << dq(remote_tar) << "\" | tar -xzf - -C \"" << dq(C.mirror_site_dir) << "\"; " "else " "tar -xzf \"" << dq(remote_tar) << "\" -C \"" << dq(C.mirror_site_dir) << "\" --checkpoint=500 --checkpoint-action=echo=. ; echo; " "fi\n" << "REF=$(mktemp); touch \"$REF\"; " "find \"" << dq(C.mirror_site_dir) << "\" \\( -type f -o -type d \\) -newer \"$REF\" -print0 | xargs -0 -r touch -r \"$REF\"; " "rm -f \"$REF\"\n" << "echo \"🧰 Выставляю права\" 1>&2;\n" << "chown -R www-data:www-data \"" << dq(C.mirror_site_dir) << "\" || true\n" << "find \"" << dq(C.mirror_site_dir) << "\" -type d -exec chmod 755 {} \\; || true\n" << "find \"" << dq(C.mirror_site_dir) << "\" -type f -exec chmod 644 {} \\; || true\n" // === БД (как было) === << "echo \"🗄️ Подготавливаю БД и пользователя (MariaDB)\" 1>&2;\n" << "/usr/bin/mysql -uroot <<\\SQL\n" "CREATE DATABASE IF NOT EXISTS `" << C.db_name << "` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;\n" "CREATE USER IF NOT EXISTS '" << C.db_user << "'@'localhost' IDENTIFIED BY '" << C.db_pass << "';\n" "CREATE USER IF NOT EXISTS '" << C.db_user << "'@'127.0.0.1' IDENTIFIED BY '" << C.db_pass << "';\n" "GRANT ALL ON `" << C.db_name << "`.* TO '" << C.db_user << "'@'localhost';\n" "GRANT ALL ON `" << C.db_name << "`.* TO '" << C.db_user << "'@'127.0.0.1';\n" "FLUSH PRIVILEGES;\n" "SQL\n" << "echo \"📦 Бэкап прежней БД (мягко)\" 1>&2;\n" << "/usr/bin/mysqldump \"" << dq(C.db_name) << "\" > \"" << dq(remote_day) << "/db_old.sql\" 2>/dev/null || true\n" << "DB_IMPORTED=0; TABLES_AFTER=0;\n" << "if [ -s \"" << dq(remote_sql) << "\" ]; then " "echo \"⬇️ Импортирую дамп\" 1>&2; " "/usr/bin/mysql \"" << dq(C.db_name) << "\" < \"" << dq(remote_sql) << "\" && DB_IMPORTED=1; " "else " "echo \"⚠️ Дамп не найден или пуст — пропуск\" 1>&2; " "fi\n" << "TABLES_AFTER=$(/usr/bin/mysql -NBe \"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='" << dq(C.db_name) << "'\" 2>/dev/null || echo 0)\n" << "echo \" 📊 Таблиц в БД после импорта: $TABLES_AFTER\" 1>&2;\n" // === Генерация vhost === << "echo \"🧩 Генерирую nginx конфиг\" 1>&2;\n" << "cat > /etc/nginx/sites-available/" << dq(C.server_name) << ".conf <<'EOF'\n" << "server {\n" << " listen 80;\n" << " listen [::]:80;\n" << " server_name " << C.server_name << " www." << C.server_name << ";\n" << " client_max_body_size 1024m;\n" << " return 301 https://$host$request_uri;\n" << "}\n" << "server {\n" << " listen 443 ssl http2;\n" << " listen [::]:443 ssl http2;\n" << " server_name " << C.server_name << " www." << C.server_name << ";\n" << " ssl_certificate /etc/letsencrypt/live/" << C.server_name << "-0002/fullchain.pem;\n" << " ssl_certificate_key /etc/letsencrypt/live/" << C.server_name << "-0002/privkey.pem;\n" << " include /etc/letsencrypt/options-ssl-nginx.conf;\n" << " ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;\n"; if (C.mirror_server_mode == "reverse_proxy") { root << " upstream mad_backend {\n" << " server " << C.webmain_ip << ":" << C.webmain_http_port << " max_fails=3 fail_timeout=10s;\n" << " server 127.0.0.1:" << C.mirror_http_port << " backup;\n" << " }\n" << " location / {\n" << " proxy_pass http://mad_backend;\n" << " proxy_set_header Host $host;\n" << " proxy_set_header X-Real-IP $remote_addr;\n" << " proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n" << " proxy_set_header X-Forwarded-Proto $scheme;\n" << " }\n"; } else { // standalone root << " root " << C.mirror_site_dir << ";\n" << " index index.php index.html;\n" << " location / { try_files $uri $uri/ /index.php?$args; }\n" << " location ~ \\.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:" << php_sock_cfg << "; }\n"; } root << " client_max_body_size 1024m;\n" << "}\n" << "EOF\n" << "ln -sf /etc/nginx/sites-available/" << dq(C.server_name) << ".conf /etc/nginx/sites-enabled/\n" << "nginx -t && systemctl restart nginx\n"; if (!C.no_switch) { // === Вачдог === root << "echo \"🐶 Обновляю /webserver/wachdog\" 1>&2;\n" << "mkdir -p /webserver || true\n" << "WD=/webserver/wachdog\n" << "if [ -f \"$WD\" ]; then\n" << " if grep -Eq '^madmentat\\.ru_update=false\\b' \"$WD\"; then\n" << " sed -ri 's/^madmentat\\.ru_update=false\\b/madmentat.ru_update=true/' \"$WD\"\n" << " echo \" → madmentat.ru_update=true (из false)\" 1>&2;\n" << " else\n" << " if ! grep -Eq '^madmentat\\.ru_update=' \"$WD\"; then echo 'madmentat.ru_update=true' >> \"$WD\"; echo \" → добавил madmentat.ru_update=true\" 1>&2; else echo \" → уже true или отсутствует false\" 1>&2; fi\n" << " fi\n" << "else\n" << " echo 'madmentat.ru_update=true' > \"$WD\"\n" << " echo \" → создан $WD с madmentat.ru_update=true\" 1>&2;\n" << "fi\n"; } // === Сводка === root << "echo \"——— Итог ———\" 1>&2;\n" << "echo \"✅ Импорт БД: ${DB_IMPORTED}\" 1>&2;\n" << "echo \"📊 Таблиц в БД: ${TABLES_AFTER}\" 1>&2;\n" << "echo \"✅ Фронт: " << C.mirror_server_mode << "\" 1>&2;\n" << "echo \"✅ Вачдог: $(grep -E '^madmentat\\.ru_update=' \"$WD\" || echo '-')\" 1>&2;\n"; const std::string sudopw = C.mirror_sudo_pass.empty() ? C.mirror_user_pass : C.mirror_sudo_pass; const std::string pass_b64 = base64_encode(sudopw); std::ostringstream wrapper; wrapper << "PW_B64='" << pass_b64 << "'; " << "if sudo -n true 2>/dev/null; then " "sudo -p '' bash -se <<'ROOT'\n" << root.str() << "ROOT\n" << "else " "( printf '%s\\n' \"$(printf '%s' \"$PW_B64\" | base64 -d)\"; cat <<'ROOT'\n" << root.str() << "ROOT\n" << ") | sudo -S -p '' bash -se; " << "fi"; return wrapper.str(); } // --------------------------- Развёртывание командой (APACHE) --------------------------- static std::string build_apache_deploy_cmd(const Config& C, const std::string& remote_tar, const std::string& remote_sql, const std::string& remote_day) { if (C.mirror_server_mode == "reverse_proxy") { std::cerr << "Debug: Apache2 not supported for reverse_proxy, skipping" << std::endl; return "echo '❌ Apache2 not supported for reverse_proxy mode' 1>&2; exit 1;"; } const std::string conf_avail = "/etc/apache2/sites-available/" + C.server_name + ".local.conf"; std::string php_sock = C.php_fpm_sock.empty() ? ("/run/php/php" + C.php_version + "-fpm.sock") : C.php_fpm_sock; // Apache vhost (php-fpm через unix-сокет) std::ostringstream tpl; tpl << "<VirtualHost *:" << C.mirror_http_port << ">\n" << " ServerName " << C.server_name << "\n" << " DocumentRoot " << C.mirror_site_dir << "\n" << " <Directory " << C.mirror_site_dir << ">\n" << " Options Indexes FollowSymLinks\n" << " AllowOverride All\n" << " Require all granted\n" << " </Directory>\n" << " ErrorLog ${APACHE_LOG_DIR}/" << C.server_name << "_error.log\n" << " CustomLog ${APACHE_LOG_DIR}/" << C.server_name << "_access.log combined\n" << " <FilesMatch \\.php$>\n" << " SetHandler \"proxy:unix:" << php_sock << "|fcgi://localhost/\"\n" << " </FilesMatch>\n" << "</VirtualHost>\n"; // SUDO и sudo-пароль std::ostringstream cmd; cmd << "set -e; " << "SUDO='sudo -n'; if ! $SUDO true 2>/dev/null; then SUDO=\"echo '" << sh_escape_single(C.mirror_sudo_pass.empty() ? C.mirror_user_pass : C.mirror_sudo_pass) << "' | sudo -S -p ''\"; fi; " << "echo '→ Проверка окружения (apache2/php-fpm/mysql/tar)'; " << "command -v apache2ctl >/dev/null || { echo '❌ apache2ctl не найден'; exit 1; }; " << "command -v tar >/dev/null || { echo '❌ tar не установлен'; exit 1; }; " << "command -v mysql >/dev/null || { echo '❌ mysql клиент не установлен'; exit 1; }; " << "if ! command -v php-fpm >/dev/null && ! command -v php-fpm" << C.php_version << " >/dev/null; then " "echo '❌ PHP-FPM не найден'; exit 1; " "fi; " << "PHP_SOCK='" << php_sock << "'; " << "[ -S \"$PHP_SOCK\" ] || { echo \"❌ Нет сокета PHP-FPM: $PHP_SOCK\"; ls -l /run/php || true; exit 1; }; " << "echo '→ Обновление рабочей копии'; " << "mkdir -p '" << C.mirror_site_dir << "'; " << "rm -rf '" << C.mirror_site_dir << "'.old; " << "mv '" << C.mirror_site_dir << "' '" << C.mirror_site_dir << ".old' 2>/dev/null || true; " << "mkdir -p '" << C.mirror_site_dir << "'; " // РАСПАКОВКА С ПРОГРЕССОМ << "echo '→ Распаковка сайта (прогресс)'; " << "if command -v pv >/dev/null 2>&1; then " "pv -f -p -t -e -r -b '" << remote_tar << "' | tar -xzf - -C '" << C.mirror_site_dir << "'; " "else " "echo ' pv не найден — индикатор точками'; " "tar -xzf '" << remote_tar << "' -C '" << C.mirror_site_dir << "' --checkpoint=500 --checkpoint-action=echo=. ; echo; " "fi; " << "echo '→ Права'; " << "$SUDO /bin/chown -R www-data:www-data '" << C.mirror_site_dir << "' || true; " << "find '" << C.mirror_site_dir << "' -type d -exec /bin/chmod 755 {} \\; || true; " << "find '" << C.mirror_site_dir << "' -type f -exec chmod 644 {} \\; || true; " // Запись vhost без tee-пайпа << "echo '→ Apache vhost'; " << "TMPCONF=$(mktemp) && printf '%s' '" << sh_escape_single(tpl.str()) << "' > \"$TMPCONF\" && " "$SUDO /bin/mv \"$TMPCONF\" '" << conf_avail << "'; " << "$SUDO /usr/sbin/a2enmod proxy proxy_fcgi setenvif rewrite >/dev/null || true; " << "$SUDO /usr/sbin/a2ensite '" << C.server_name << ".local.conf' >/dev/null || true; " << "echo '→ apache2ctl configtest'; $SUDO /usr/sbin/apache2ctl configtest; " // БД через root << "echo '→ Подготовка БД и пользователя (через root)'; " << "$SUDO /usr/bin/mysql -uroot -e \"" "CREATE DATABASE IF NOT EXISTS \\`" << C.db_name << "\\` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; " "CREATE USER IF NOT EXISTS '" << C.db_user << "'@'localhost' IDENTIFIED BY '" << C.db_pass << "'; " "CREATE USER IF NOT EXISTS '" << C.db_user << "'@'127.0.0.1' IDENTIFIED BY '" << C.db_pass << "'; " "GRANT ALL ON \\`" << C.db_name << "\\`.* TO " "'" << C.db_user << "'@'localhost', " "'" << C.db_user << "'@'127.0.0.1'; " "FLUSH PRIVILEGES;\"; " << "echo '→ Бэкап текущей БД на mirror (мягко)'; " << "$SUDO /usr/bin/mysqldump " << C.db_name << " > '" << remote_day << "/db_old.sql' 2>/dev/null || true; " << "echo '→ Импорт новой БД (через root)'; " << "$SUDO /usr/bin/mysql " << C.db_name << " < '" << remote_sql << "'; " << "echo '→ Перезапуск apache2'; $SUDO /bin/systemctl restart apache2; " << "echo '→ Ротация'; " << "find '" << C.mirror_backup_base << "' -maxdepth 1 -type d " "-regextype posix-extended -regex '.*/[0-9]{4}-[0-9]{2}-[0-9]{2}$' -mtime +7 -exec rm -rf {} + ; " << "echo '✓ Apache: развёртывание завершено';"; return cmd.str(); } // --------------------------- Daemon funcs --------------------------- static void daemon_install(const char* exec_path) { // Load config for mirror details Config cfg; load_kv_file(CFG_PATH_PRIMARY, cfg); // Setup SSH to mirror to deploy the setup script ssh_session session = ssh_new(); if (!session) { std::cerr << "❌ ssh_new during daemon install\n"; return; } int timeout = 30; ssh_options_set(session, SSH_OPTIONS_TIMEOUT, &timeout); ssh_options_set(session, SSH_OPTIONS_HOST, cfg.mirror_host.c_str()); ssh_options_set(session, SSH_OPTIONS_USER, cfg.mirror_user.c_str()); ssh_options_set(session, SSH_OPTIONS_PORT, &cfg.mirror_ssh_port); std::cout << "🔌 Connecting to mirror for setup script deployment...\n"; if (ssh_connect(session) != SSH_OK) { std::cerr << "❌ ssh_connect to mirror: " << ssh_get_error(session) << "\n"; ssh_free(session); return; } if (ssh_userauth_password(session, nullptr, cfg.mirror_user_pass.c_str()) != SSH_AUTH_SUCCESS) { std::cerr << "❌ Auth to mirror: " << ssh_get_error(session) << "\n"; ssh_disconnect(session); ssh_free(session); return; } // Generate setup_madmentat_nginx.sh content std::ostringstream script; script << "#!/bin/bash\n\n" << "MODE=\"$1\" # local или remote\n\n" << "if [ \"$MODE\" != \"local\" ] && [ \"$MODE\" != \"remote\" ]; then\n" << " echo \"Usage: $0 local|remote\"\n" << " exit 1\n" << "fi\n\n" << "CONF=\"/etc/nginx/sites-available/" << cfg.server_name << ".conf\"\n\n" << "cat > \"$CONF\" <<EOF\n" << "server {\n" << " listen 80;\n" << " listen [::]:80;\n" << " server_name " << cfg.server_name << " www." << cfg.server_name << ";\n" << " client_max_body_size 1024m;\n" << " return 301 https://\\$host\\$request_uri;\n" << "}\n\n" << "server {\n" << " listen 443 ssl http2;\n" << " listen [::]:443 ssl http2;\n" << " server_name " << cfg.server_name << " www." << cfg.server_name << ";\n" << " ssl_certificate /etc/letsencrypt/live/" << cfg.server_name << "-0002/fullchain.pem;\n" << " ssl_certificate_key /etc/letsencrypt/live/" << cfg.server_name << "-0002/privkey.pem;\n" << " include /etc/letsencrypt/options-ssl-nginx.conf;\n" << " ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;\n\n" << " upstream mad_backend {\n"; script << "EOF\n\n"; script << "if [ \"$MODE\" == \"remote\" ]; then\n" << " echo \" server " << cfg.webmain_ip << ":" << cfg.webmain_http_port << " max_fails=3 fail_timeout=10s;\" >> \"$CONF\"\n" << " echo \" server 127.0.0.1:" << cfg.mirror_http_port << " backup;\" >> \"$CONF\"\n" << "else # local\n" << " echo \" server 127.0.0.1:" << cfg.mirror_http_port << " max_fails=3 fail_timeout=10s;\" >> \"$CONF\"\n" << " echo \" server " << cfg.webmain_ip << ":" << cfg.webmain_http_port << " backup;\" >> \"$CONF\"\n" << "fi\n\n"; script << "cat >> \"$CONF\" <<EOF\n" << " }\n\n" << " location / {\n" << " proxy_pass http://mad_backend;\n" << " proxy_set_header Host \\$host;\n" << " proxy_set_header X-Real-IP \\$remote_addr;\n" << " proxy_set_header X-Forwarded-For \\$proxy_add_x_forwarded_for;\n" << " proxy_set_header X-Forwarded-Proto \\$scheme;\n" << " }\n\n" << " client_max_body_size 1024m;\n" << "}\n" << "EOF\n\n" << "# Симлинк и рестарт\n" << "ln -sf \"$CONF\" /etc/nginx/sites-enabled/\n" << "nginx -t && systemctl restart nginx\n" << "echo \"Switched to $MODE mode\"\n"; // Deploy the script to mirror std::string SUDO = sudo_prefix(session, cfg); std::string deploy_cmd = SUDO + " tee /root/setup_madmentat_nginx.sh > /dev/null <<'EOF'\n" + script.str() + "\nEOF\n && " + SUDO + " chmod +x /root/setup_madmentat_nginx.sh"; if (ssh_exec(session, deploy_cmd, true) != 0) { std::cerr << "❌ Failed to deploy setup script to mirror\n"; } else { std::cout << "✅ Setup script deployed to mirror: /root/setup_madmentat_nginx.sh\n"; } // Cleanup SSH ssh_disconnect(session); ssh_free(session); // Proceed with service file std::cout << "➜ install -m 0755 '" << exec_path << "' /usr/local/bin/madbackuper\n"; run_local("install -m 0755 '" + std::string(exec_path) + "' /usr/local/bin/madbackuper"); std::ofstream service(SERVICE_FILE); service << "[Unit]\n" << "Description=madbackuper daemon (health-check + backup scheduler)\n" << "After=network-online.target\n" << "Wants=network-online.target\n" << "[Service]\n" << "Type=simple\n" << "ExecStart=/usr/local/bin/madbackuper --daemon-mode\n" << "Restart=always\n" << "RestartSec=5\n" << "User=root\n" << "StandardOutput=journal\n" << "StandardError=journal\n" << "Environment=LANG=C.UTF-8\n" << "[Install]\n" << "WantedBy=multi-user.target\n"; service.close(); run_local("systemctl daemon-reload"); run_local("systemctl enable madbackuper.service"); run_local("systemctl restart madbackuper.service"); std::cout << "✅ Установка демона завершена. Логи: journalctl -u madbackuper -f\n"; } static void daemon_uninstall() { run_local("systemctl stop madbackuper.service >/dev/null 2>&1"); run_local("systemctl disable madbackuper.service >/dev/null 2>&1"); run_local("rm -f /etc/systemd/system/madbackuper.service"); run_local("systemctl daemon-reload"); std::cout << "✅ Демон отключён локально. Для проверки: systemctl status madbackuper\n"; } static void daemon_status() { run_local("systemctl status madbackuper.service"); } static bool check_site_available(const std::string& host, int port) { int sock = socket(AF_INET, SOCK_STREAM, 0); if (sock < 0) return false; struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(port); inet_pton(AF_INET, host.c_str(), &addr.sin_addr); int timeout = 5; // sec struct timeval tv; tv.tv_sec = timeout; tv.tv_usec = 0; setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); if (connect(sock, (sockaddr*)&addr, sizeof(addr)) < 0) { close(sock); return false; } std::string req = "HEAD / HTTP/1.1\r\nHost: " + host + "\r\nConnection: close\r\n\r\n"; send(sock, req.c_str(), req.size(), 0); char buf[1024]; int n = recv(sock, buf, sizeof(buf), 0); close(sock); return (n > 0 && strstr(buf, "200 OK") != nullptr); } static void switch_to_mirror(ssh_session session, const Config& C) { ssh_exec(session, "sudo /root/setup_madmentat_nginx.sh local", true); } static void switch_to_webmain(ssh_session session, const Config& C) { ssh_exec(session, "sudo /root/setup_madmentat_nginx.sh remote", true); } static int mirror_unit_deploy(ssh_session session, const Config& C) { std::string SUDO = sudo_prefix(session, C); std::ostringstream script; script << "#!/bin/bash\n" << "while true; do\n" << " if curl -s -I http://" << C.webmain_ip << " | grep -q '200 OK'; then\n" << " /root/setup_madmentat_nginx.sh remote\n" << " else\n" << " /root/setup_madmentat_nginx.sh local\n" << " fi\n" << " sleep 60\n" << "done\n"; std::string cmd = SUDO + " echo '" + sh_escape_single(script.str()) + "' > /root/mirror_check.sh && " + SUDO + " chmod +x /root/mirror_check.sh"; if (ssh_exec(session, cmd, true) != 0) return 1; std::ostringstream unit; unit << "[Unit]\nDescription=Madbackuper Mirror Check\nAfter=network.target\n\n" << "[Service]\nExecStart=/root/mirror_check.sh\nRestart=always\n\n[Install]\nWantedBy=multi-user.target\n"; cmd = SUDO + " echo '" + sh_escape_single(unit.str()) + "' > /etc/systemd/system/madbackuper-mirror.service && " + SUDO + " systemctl daemon-reload && " + SUDO + " systemctl enable --now madbackuper-mirror"; if (ssh_exec(session, cmd, true) != 0) return 1; return 0; } // Extract backup/deploy logic to func for periodic static int do_backup_and_deploy(const Config& cfg, ssh_session session, sftp_session sftp) { const std::string date = today(); const std::string tmp_dir = "/tmp/madbackuper_" + date; fs::create_directories(tmp_dir); const std::string tar_path = tmp_dir + "/site_" + date + ".tar.gz"; const std::string sql_path = tmp_dir + "/db_" + date + ".sql"; const std::string cnf_path = tmp_dir + "/db.cnf"; { std::ofstream cnf(cnf_path); cnf << "[client]\nuser=" << cfg.db_user << "\npassword=" << cfg.db_pass << "\n"; cnf.close(); chmod(cnf_path.c_str(), 0600); } std::ostringstream tar_cmd; tar_cmd << "tar -czf '" << tar_path << "' -C '" << cfg.webmain_site_dir << "' ."; run_with_spinner(tar_cmd.str(), "tar site"); std::ostringstream dump_cmd; dump_cmd << "mysqldump --defaults-extra-file='" << cnf_path << "' " << cfg.db_name << " > '" << sql_path << "'"; run_with_spinner(dump_cmd.str(), "dump db"); unlink(cnf_path.c_str()); const std::string remote_day = cfg.mirror_backup_base + "/" + date; if (sftp_mkdirs(session, sftp, remote_day) != SSH_OK) return 1; const std::string remote_tar = remote_day + "/site_" + date + ".tar.gz"; const std::string remote_sql = remote_day + "/db_" + date + ".sql"; sftp_upload_file_progress(session, sftp, tar_path, remote_tar, "upload tar"); sftp_upload_file_progress(session, sftp, sql_path, remote_sql, "upload sql"); std::string deploy_cmd = build_nginx_deploy_cmd(cfg, remote_tar, remote_sql, remote_day); ssh_exec(session, deploy_cmd, true); return 0; } static void periodic_backup(const Config& cfg) { ssh_session session = ssh_new(); // setup and connect as in main sftp_session sftp = sftp_new(session); // init sftp do_backup_and_deploy(cfg, session, sftp); // cleanup } // --------------------------- MAIN --------------------------- int main(int argc, char** argv) { bool daemon_install_flag = false, daemon_uninstall_flag = false, daemon_mode_flag = false, daemon_status_flag = false, skip_tar = false, skip_upload = false; for (int i=1;i<argc;++i) { std::string a = argv[i]; if (a=="--daemon-install") daemon_install_flag = true; if (a=="--daemon-uninstall") daemon_uninstall_flag = true; if (a=="--daemon-mode") daemon_mode_flag = true; if (a=="--daemon-status") daemon_status_flag = true; if (a=="--skip-tar") skip_tar = true; if (a=="--skip-upload") skip_upload = true; } if (daemon_install_flag) { daemon_install(argv[0]); return 0; } if (daemon_uninstall_flag) { daemon_uninstall(); return 0; } if (daemon_status_flag) { daemon_status(); return 0; } if (daemon_mode_flag) { Config cfg; load_kv_file(CFG_PATH_PRIMARY, cfg); apply_cli_kv(argc, argv, cfg); std::ofstream log(DAEMON_LOG_PATH, std::ios::app); std::cout.rdbuf(log.rdbuf()); std::cerr.rdbuf(log.rdbuf()); std::cout << "Debug: Daemon mode started on webmain (192.168.88.198)" << std::endl; bool last_down = false; time_t last_backup = time(nullptr); bool is_mirrored = false; std::ifstream state(DAEMON_STATE_PATH); if (state) { state >> is_mirrored; state.close(); std::cout << "Debug: Loaded state, site is on " << (is_mirrored ? "mirror" : "webmain") << std::endl; } ssh_session session = ssh_new(); if (!session) { std::cerr << "❌ ssh_new in daemon mode\n"; return 1; } int timeout = 30; ssh_options_set(session, SSH_OPTIONS_TIMEOUT, &timeout); ssh_options_set(session, SSH_OPTIONS_HOST, cfg.mirror_host.c_str()); ssh_options_set(session, SSH_OPTIONS_USER, cfg.mirror_user.c_str()); ssh_options_set(session, SSH_OPTIONS_PORT, &cfg.mirror_ssh_port); std::cout << "Debug: Connecting to mirror (" << cfg.mirror_host << ":" << cfg.mirror_ssh_port << ")" << std::endl; if (ssh_connect(session) != SSH_OK) { std::cerr << "❌ ssh_connect in daemon mode: " << ssh_get_error(session) << "\n"; ssh_free(session); return 1; } std::cout << "Debug: Connected to mirror (" << peer_ip(session) << ")" << std::endl; if (ssh_userauth_password(session, nullptr, cfg.mirror_user_pass.c_str()) != SSH_AUTH_SUCCESS) { std::cerr << "❌ Auth in daemon mode: " << ssh_get_error(session) << "\n"; ssh_disconnect(session); ssh_free(session); return 1; } while (true) { std::cout << "Debug: Checking webmain site (127.0.0.1:" << cfg.webmain_http_port << ")" << std::endl; bool down = !check_site_available("127.0.0.1", cfg.webmain_http_port); if (down && !last_down) { std::cout << "Debug: Webmain site is down, attempting restart of " << cfg.target_server_name << std::endl; run_local("sudo systemctl restart " + cfg.target_server_name); sleep(10); down = !check_site_available("127.0.0.1", cfg.webmain_http_port); if (down && !is_mirrored) { std::cout << "Debug: Webmain still down, switching to mirror" << std::endl; switch_to_mirror(session, cfg); is_mirrored = true; } } else if (!down && is_mirrored) { std::cout << "Debug: Webmain site recovered, switching to webmain" << std::endl; switch_to_webmain(session, cfg); is_mirrored = false; } last_down = down; // Save state std::ofstream state_out(DAEMON_STATE_PATH); state_out << (is_mirrored ? "mirror" : "webmain"); state_out.close(); std::cout << "Debug: Saved state: site on " << (is_mirrored ? "mirror" : "webmain") << std::endl; if (difftime(time(nullptr), last_backup) > BACKUP_INTERVAL_SEC) { std::cout << "Debug: Time for periodic backup" << std::endl; periodic_backup(cfg); last_backup = time(nullptr); } std::cout << "Debug: Sleeping for " << cfg.daemon_check_delay << " seconds" << std::endl; sleep(cfg.daemon_check_delay); } ssh_disconnect(session); ssh_free(session); return 0; } std::cout << "🔧 madbackuper: SCP/SFTP бэкап и развертывание\n"; // Конфиг: создать при первом запуске std::string cfg_path = CFG_PATH_PRIMARY; if (!fs::exists(cfg_path)) { std::cerr << "ℹ️ Конфиг не найден: " << cfg_path << " — генерирую по умолчанию...\n"; write_default_config(cfg_path); if (!fs::exists(cfg_path)) { cfg_path = CFG_PATH_FALLBACK; std::cerr << "⚠️ Нет прав на /etc. Пишу конфиг сюда: " << cfg_path << "\n"; write_default_config(cfg_path); if (!fs::exists(cfg_path)) { std::cerr << "❌ Не удалось создать конфиг\n"; return 1; } } std::cout << "✅ Создан конфиг: " << cfg_path << "\n"; std::cout << "⚠️ Отредактируй его при необходимости и запусти программу снова.\n"; return 0; } // Загрузка конфига + CLI Config cfg; load_kv_file(cfg_path, cfg); apply_cli_kv(argc, argv, cfg); // Валидация std::string verr; if (!validate(cfg, verr)) { std::cerr << "❌ Ошибка параметров: " << verr << "\n"; return 1; } // Пролог std::cout << "📁 Локальный сайт: " << cfg.webmain_site_dir << "\n"; std::cout << "🛢️ База: " << cfg.db_name << " (user: " << cfg.db_user << ")\n"; std::cout << "🌐 Резерв: " << cfg.mirror_user << "@" << cfg.mirror_host << ":" << cfg.mirror_site_dir << " (" << cfg.target_server_name << ", mode: " << cfg.mirror_server_mode << ")\n"; std::cout << "🔄 Локальные порты сайта: " << cfg.mirror_http_port << (cfg.mirror_https_port>0?("/"+std::to_string(cfg.mirror_https_port)):"") << " | switch_to_mirror=" << (cfg.switch_to_mirror ? "yes":"no") << "\n"; if (!fs::exists(cfg.webmain_site_dir)) { std::cerr << "❌ Нет каталога: " << cfg.webmain_site_dir << "\n"; return 1; } // SSH-сессия ssh_session session = ssh_new(); if (!session) { std::cerr << "❌ ssh_new\n"; return 1; } int timeout = 30; ssh_options_set(session, SSH_OPTIONS_TIMEOUT, &timeout); ssh_options_set(session, SSH_OPTIONS_HOST, cfg.mirror_host.c_str()); ssh_options_set(session, SSH_OPTIONS_USER, cfg.mirror_user.c_str()); ssh_options_set(session, SSH_OPTIONS_PORT, &cfg.mirror_ssh_port); std::cout << "🔌 Подключаемся к " << cfg.mirror_host << ":" << cfg.mirror_ssh_port << "...\n"; if (ssh_connect(session) != SSH_OK) { std::cerr << "❌ ssh_connect: " << ssh_get_error(session) << "\n"; ssh_free(session); return 1; } std::cout << "✅ Соединение установлено (" << peer_ip(session) << ")\n"; if (ssh_userauth_password(session, nullptr, cfg.mirror_user_pass.c_str()) != SSH_AUTH_SUCCESS) { std::cerr << "❌ Аутентификация: " << ssh_get_error(session) << "\n"; ssh_disconnect(session); ssh_free(session); return 1; } // SFTP sftp_session sftp = sftp_new(session); if (!sftp) { std::cerr << "❌ sftp_new\n"; ssh_disconnect(session); ssh_free(session); return 1; } if (sftp_init(sftp) != SSH_OK) { std::cerr << "❌ sftp_init: " << ssh_get_error(session) << "\n"; sftp_free(sftp); ssh_disconnect(session); ssh_free(session); return 1; } // Backup and deploy do_backup_and_deploy(cfg, session, sftp); // Завершение sftp_free(sftp); ssh_disconnect(session); ssh_free(session); std::cout << "\n🎉 Бэкап и передача завершены.\n"; return 0; }