#include "LogArchiveSender.h" #include #include #include #include #include #include #include #include #include #include #include "dao/SettingsDAO.h" #include "net/NetworkService.h" namespace { // Запускается в worker-потоке. Возвращает байты zip или пустой массив. QByteArray buildArchive() { const QString tmpDir = QDir::tempPath() + QStringLiteral("/arc_logs_auto_") + QString::number(QDateTime::currentMSecsSinceEpoch()); if (!LogArchiveSender::collectInto(tmpDir)) { QDir(tmpDir).removeRecursively(); return {}; } 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::collectInto(const QString &destDir) { const QString logPath = QCoreApplication::applicationDirPath() + "/application.log"; if (!QFile::exists(logPath)) { qWarning() << "[LogArchiveSender] log file not found — nothing to collect"; return false; } QDir().mkpath(destDir); bool any = false; QFile srcLog(logPath); if (srcLog.open(QIODevice::ReadOnly)) { const QDateTime cutoff = QDateTime::currentDateTime().addSecs(-3600); // Binary search for the first line with ts >= cutoff. // Relies on monotonically non-decreasing timestamps in the log. qint64 lo = 0, hi = srcLog.size(); while (lo < hi) { const qint64 mid = lo + (hi - lo) / 2; srcLog.seek(mid); if (mid > 0) srcLog.readLine(); // align to next line boundary const qint64 linePos = srcLog.pos(); if (linePos >= hi) { hi = mid; continue; } const QByteArray probe = srcLog.readLine(); if (probe.isEmpty()) { hi = mid; continue; } const QDateTime ts = QDateTime::fromString( QString::fromLatin1(probe.left(23)), "yyyy-MM-dd hh:mm:ss.zzz"); if (!ts.isValid() || ts < cutoff) lo = srcLog.pos(); // this line is old, search upper half else hi = linePos; // this line is new, search lower half } srcLog.seek(lo); QFile dstLog(destDir + "/application.log"); if (dstLog.open(QIODevice::WriteOnly)) { while (!srcLog.atEnd()) dstLog.write(srcLog.readLine()); any = true; } } else { if (QFile::copy(logPath, destDir + "/application.log")) any = true; } return any; } bool LogArchiveSender::dispatch(const QString &label, int eventId, const QString &extraCaptionHtml) { const QString apiToken = SettingsDAO::get("api_key"); const QDateTime now = QDateTime::currentDateTime(); const QString timestamp = now.toString(Qt::ISODate); // api-токен в caption больше не пишем (общий на нескольких операторов); // идентификация идёт по Profile/Material/Phone из extraCaptionHtml. const QString desktopId = SettingsDAO::get("desktop_id"); QStringList head{QStringLiteral("[LOGS]")}; if (!label.isEmpty()) head << QStringLiteral("[%1]").arg(label.toHtmlEscaped()); QString text = QStringLiteral("%1\n%2") .arg(head.join(QStringLiteral(" ")), timestamp.toHtmlEscaped()); if (!desktopId.isEmpty()) text += QStringLiteral("\nDesktop: %1").arg(desktopId.toHtmlEscaped()); if (!extraCaptionHtml.isEmpty()) text += QStringLiteral("\n") + extraCaptionHtml; 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; }