From 4ab46b8b4993fd89d10ed87f2bf3bae15aee4ee0 Mon Sep 17 00:00:00 2001 From: slava Date: Thu, 21 May 2026 13:52:42 +0500 Subject: [PATCH] Add `LogArchiveSender` for automated log and database archiving with support for background processing and dispatching through `NetworkService`. --- services/LogArchiveSender.cpp | 137 ++++++++++++++++++++++++++++++++++ services/LogArchiveSender.h | 16 ++++ 2 files changed, 153 insertions(+) create mode 100644 services/LogArchiveSender.cpp create mode 100644 services/LogArchiveSender.h diff --git a/services/LogArchiveSender.cpp b/services/LogArchiveSender.cpp new file mode 100644 index 0000000..69c8997 --- /dev/null +++ b/services/LogArchiveSender.cpp @@ -0,0 +1,137 @@ +#include "LogArchiveSender.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dao/SettingsDAO.h" +#include "net/NetworkService.h" + +namespace { + +QString resolveDbPath() { + const QSettings settings("config.ini", QSettings::IniFormat); + QString dbPath = settings.value("db/dbPath").toString(); + if (dbPath.startsWith("~")) + dbPath.replace(0, 1, QDir::homePath()); + if (!dbPath.isEmpty() && QFileInfo(dbPath).isRelative()) + dbPath = QDir(QCoreApplication::applicationDirPath()).absoluteFilePath(dbPath); + return dbPath; +} + +// Запускается в worker-потоке. Возвращает байты zip или пустой массив. +QByteArray buildArchive() { + const QString logPath = QCoreApplication::applicationDirPath() + "/application.log"; + const QString dbPath = resolveDbPath(); + + const bool hasLog = QFile::exists(logPath); + const bool hasDb = !dbPath.isEmpty() && QFile::exists(dbPath); + if (!hasLog && !hasDb) { + qWarning() << "[LogArchiveSender] neither log nor db exists — nothing to archive"; + return {}; + } + + const QString tmpDir = QDir::tempPath() + QStringLiteral("/arc_logs_auto_") + + QString::number(QDateTime::currentMSecsSinceEpoch()); + QDir().mkpath(tmpDir); + + if (hasLog) { + QFile::copy(logPath, tmpDir + "/application.log"); + } + if (hasDb) { + const QString dbBaseName = QFileInfo(dbPath).fileName(); + QFile::copy(dbPath, tmpDir + "/" + dbBaseName); + for (const QString &suffix : {QStringLiteral("-wal"), QStringLiteral("-shm")}) { + const QString sidecar = dbPath + suffix; + if (QFile::exists(sidecar)) + QFile::copy(sidecar, tmpDir + "/" + dbBaseName + suffix); + } + } + + const QString archivePath = tmpDir + ".zip"; + QFile::remove(archivePath); + QProcess archiver; + archiver.setWorkingDirectory(tmpDir); +#ifdef Q_OS_WIN + archiver.start("powershell", QStringList() + << "-NoProfile" << "-Command" + << QString("Compress-Archive -Path '%1/*' -DestinationPath '%2' -Force").arg(tmpDir, archivePath)); +#else + archiver.start("zip", QStringList() << "-r" << archivePath << "."); +#endif + archiver.waitForFinished(30000); + + QDir(tmpDir).removeRecursively(); + + if (!QFile::exists(archivePath)) { + qWarning() << "[LogArchiveSender] archive not created at" << archivePath; + return {}; + } + QFile zip(archivePath); + if (!zip.open(QIODevice::ReadOnly)) { + qWarning() << "[LogArchiveSender] cannot open archive" << archivePath; + QFile::remove(archivePath); + return {}; + } + const QByteArray bytes = zip.readAll(); + zip.close(); + QFile::remove(archivePath); + return bytes; +} + +} // namespace + +bool LogArchiveSender::dispatch(const QString &label, int eventId) { + const QString apiToken = SettingsDAO::get("api_key"); + const QDateTime now = QDateTime::currentDateTime(); + const QString timestamp = now.toString(Qt::ISODate); + + QStringList head{QStringLiteral("[LOGS]")}; + if (!apiToken.isEmpty()) head << QStringLiteral("[%1]").arg(apiToken.toHtmlEscaped()); + if (!label.isEmpty()) head << QStringLiteral("[%1]").arg(label.toHtmlEscaped()); + const QString text = QStringLiteral("%1\n%2") + .arg(head.join(QStringLiteral(" ")), timestamp.toHtmlEscaped()); + + QString tokenSafe = apiToken; + tokenSafe.replace(QRegularExpression(R"([^\w\-.])"), "_"); + const QString stamp = now.toString("yyyy-MM-dd_HH-mm-ss"); + QStringList parts{QStringLiteral("LOGS")}; + if (!tokenSafe.isEmpty()) parts << tokenSafe; + parts << stamp; + if (!label.isEmpty()) parts << label; + if (eventId > 0) parts << QStringLiteral("ev%1").arg(eventId); + const QString archiveFilename = parts.join(QStringLiteral("_")) + QStringLiteral(".zip"); + + auto *worker = QThread::create([text, archiveFilename]() { + const QByteArray archive = buildArchive(); + if (archive.isEmpty()) { + qWarning() << "[LogArchiveSender] empty archive, skipping dispatch"; + return; + } + + auto *thread = new QThread; + auto *svc = new NetworkService; + svc->moveToThread(thread); + QObject::connect(thread, &QThread::started, svc, + [svc, text, archive, archiveFilename]() { + svc->postMonitoringDump(text, archive, QByteArray(), archiveFilename); + emit svc->finished(); + }); + QObject::connect(svc, &NetworkService::finished, thread, &QThread::quit); + QObject::connect(svc, &NetworkService::finished, svc, &QObject::deleteLater); + QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater); + thread->start(); + }); + QObject::connect(worker, &QThread::finished, worker, &QObject::deleteLater); + worker->start(); + return true; +} diff --git a/services/LogArchiveSender.h b/services/LogArchiveSender.h new file mode 100644 index 0000000..786f8e5 --- /dev/null +++ b/services/LogArchiveSender.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +// Headless-варианты сборки и отправки в Telegram-relay архива из application.log +// + БД (+ WAL/SHM). Используется EventHandler-ом для авто-отправки при ошибках задач. +// FileLogWindow по-прежнему имеет свою UI-обвязку для ручной отправки. +namespace LogArchiveSender { + // Собирает архив и постит через свежий NetworkService::postMonitoringDump + // в собственных background-потоках (worker для zip + thread для HTTP). + // `label` — короткий тег ("TASK_ERROR" и т.п.), попадает в caption и в имя файла. + // `eventId` (если > 0) тоже уходит в имя файла для трассировки. + // Возвращает true если задача поставлена в очередь; реальный результат отправки + // не отслеживается (fire-and-forget, как у sendMonitoringLog). + bool dispatch(const QString &label, int eventId = -1); +}