Files
qt5-legatus-legacy/Lts/Plugins/TakeHomePackageManager/THP2/Thp2UsbManagement/tpm_usb_dialog.cpp
T
2026-08-13 18:33:10 +02:00

639 lines
18 KiB
C++
Executable File

/**
* @RDE_HEADER@
* Show the exported scripts and allow them to upload them to the server
* $Id: tpm_exported_scripts_dialog.cpp 53111 2021-03-22 08:21:29Z wille_m $
* $HeadURL: svn://slssvn/TDS/branches/product/iterations/21_03_salted_caramel/Lts/Plugins/TakeHomePackageManager/tpm_exported_scripts_dialog.cpp $
* $Author: wille_m $
*
* Created by Mike Wille, SLTE, 2024-01-11
*/
#include <ui_tpm_usb2_dialog.h>
#include "tpm_usb_dialog.h"
#include <Common2/tpm_file_copy.h>
#include <Common2/tpm_process_dialog.h>
#include <Common2/tpm_exported_scripts_model.h>
#include <Common2/tpm_topics_model.h>
#include <Common2Core/tpm_js_parser.h>
#include <nglcompression/nglcomp_compress.h>
#include <nglsettings/nglsettings_connections.h>
#include <nglsettings/nglsettings_settings.h>
#include <LtsWidgets/lts_message_dialog.h>
#include <LtsWidgets/lts_global_wait_dialog.h>
#include <LtsSession/lts_session.h>
#include <LtsSession/lts_session_data.h>
#include <ngltools/ngltools_filesystem.h>
#include <QDir>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QProcess>
#include <QTemporaryFile>
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QUrl>
#include <QLocale>
#include <QTimer>
#include <QFileSystemWatcher>
#include <QTreeView>
#include <QFuture>
#include <QFutureWatcher>
#include <QString>
#include <QFile>
#include <QFileInfo>
#include <QVector>
//#include <QThread>
#include <QtConcurrent>
#include <QDirIterator>
#include <QFileDialog>
#ifdef Q_OS_UNIX
#include <unistd.h>
#endif
#include <QDebug>
#ifdef Q_OS_UNIX
#define _compiler_warning(x) _Pragma(#x)
#define compiler_warning(x) _compiler_warning(GCC warning x)
#define TMP_WARNING_LOG(x) compiler_warning("Temporary warning log active!") \
WARNING_LOG(x)
#else
#define compiler_warning(x)
#define TMP_WARNING_LOG(x) WARNING_LOG(x)
#endif
#ifndef ELOG
#define ELOG ERROR_LOG(m_logger) // polyfill
#endif
typedef std::runtime_error error;
namespace TakeHomePackageManager
{
/// USB2Dialog
USB2Dialog::USB2Dialog(LtsSession::LtsSession *session, QWidget *parent)
: LtsWidgets::LtsDialog(parent)
, m_localThpModel(new ExportedScriptsModel(this))
, m_localTopicsModel(new TopicsModel(this))
, m_usbThpModel(new ExportedScriptsModel(this))
, m_ui(new Ui::USB2Dialog)
, m_session(session)
//, m_fsWatcher(new QFileSystemWatcher(this))
// , m_exerciseId(m_session->exerciseId())
// , m_paths(0)
, m_logger(getLogger("USB2Dialog"))
{
m_ui->setupUi(this);
WARNING_LOG(m_logger)<<" USB2Dialog::USB2Dialog() START ";
connect(m_ui->pbCHange, &QPushButton::clicked,
this, &USB2Dialog::slotChangePath);
connect(m_ui->pbCopy, &QPushButton::clicked,
this, &USB2Dialog::slotCopy);
connect(m_ui->pbRemove, &QPushButton::clicked,
this, &USB2Dialog::slotRemove);
connect(m_ui->pbUnmount, &QPushButton::clicked, this, &USB2Dialog::slotUnmount);
reset();
#ifndef Q_OS_UNIX
m_ui->pbUnmount->hide();
#endif
m_ui->tvUSB->setModel(m_usbThpModel);
#ifndef LTS_DEBUG
m_ui->tvUSB->hideColumn(ExportedScriptsModel::ColId);
#endif
m_ui->tvUSB->hideColumn(ExportedScriptsModel::ColLocation);
m_ui->tvUSB->hideColumn(ExportedScriptsModel::ColCreatedAtServer);
m_ui->tvUSB->hideColumn(ExportedScriptsModel::ColUploadStatus);
m_ui->tvLocal->setModel(m_localThpModel); // important to set model before setting hidden columns
#ifndef LTS_DEBUG
m_ui->tvLocal->hideColumn(ExportedScriptsModel::ColId);
#endif
m_ui->tvLocal->hideColumn(ExportedScriptsModel::ColLocation);
m_ui->tvLocal->hideColumn(ExportedScriptsModel::ColCreatedAtServer);
m_ui->tvLocal->hideColumn(ExportedScriptsModel::ColUploadStatus);
m_ui->tvLocalTopics->setModel(m_localTopicsModel);
connect(m_ui->tvLocal->selectionModel(), &QItemSelectionModel::selectionChanged,
this, &USB2Dialog::slotSelectionChangedLocal);
connect(m_ui->tvUSB->selectionModel(), &QItemSelectionModel::selectionChanged,
this, &USB2Dialog::slotSelectionChangedUSB);
try{
initLocalModel(); // throws!
}
catch(...)
{
WARNING_LOG(m_logger)<<" data corrupted path: "<< localThpDir();
new LtsWidgets::LtsMessageDialogNew(this, tr("Couldn't Load THP(s)"));
}
setOnlineHelpFilename(QStringLiteral("Script_Editor-Export_Script_Administration"));
WARNING_LOG(m_logger)<<" USB2Dialog::USB2Dialog() END ";
}
USB2Dialog::~USB2Dialog()
{
delete m_ui;
}
void USB2Dialog::initUsbModel()
{
try
{
m_usbThpModel->init(m_usbRoot);
}
catch (const error &e)
{
new LtsWidgets::LtsMessageDialogNew(this, tr("The directory '%1' doesn't contain valid THP data.").arg(m_usbRoot));
ELOG << "cannot initialize usb thp model from usb root: " << m_usbRoot << " - " << e.what();
reset();
return;
}
m_ui->lblPath->setText(m_usbRoot);
m_ui->pbCopy->setEnabled(m_ui->tvLocal->selectionModel()->selectedRows().size() == 1);
}
/*!
* \brief USB2Dialog::unmountUSB
* \param device
* \details Use lsblk to get path (/dev/sdb1) from name(Niklas-Stick), then start normal umounting command with the part
* \author Niklas Schmidt
*/
void USB2Dialog::unmountUSB(const QString &device)
{
QProcess process;
/*!
* The sed is for substitute the '\x20' (HEX decimal for space) with real space
!*/
//
process.start(QStringLiteral("bash"), QStringList() << QStringLiteral("-c") << QStringLiteral("lsblk -o NAME,MOUNTPOINT -r"));
process.waitForFinished();
process.setReadChannel(QProcess::StandardOutput);
while (process.canReadLine())
{
QString outputLine = QString::fromUtf8(process.readLine());
DEBUG_LOG(m_logger) << "OutputLine: " << outputLine;
auto outputDevicePath = outputLine.split(QStringLiteral(" ")).last();
DEBUG_LOG(m_logger) << "OutputDevicePath: " << outputDevicePath;
auto outputPathStripped = outputDevicePath.replace(QLatin1String("\\x20"), QLatin1String(" "));
DEBUG_LOG(m_logger) << "OutputPathStripped: " << outputPathStripped;
auto outputDeviceName = outputPathStripped.split(QStringLiteral("/")).last();
DEBUG_LOG(m_logger) << "OutputDeviceName: " << outputDeviceName;
if (outputDeviceName.trimmed().compare(device.trimmed()) == 0)
{
auto serialPort = outputLine.split(QStringLiteral(" ")).first();
QString absPath = QStringLiteral("/dev/") + serialPort;
DEBUG_LOG(m_logger) << "Serial Port: " << absPath;
process.start(QStringLiteral("udisksctl"), QStringList() << QStringLiteral("unmount") << QStringLiteral("-b") << absPath);
process.waitForFinished();
reset();
return;
}
}
new LtsWidgets::LtsMessageDialogNew(this, tr("Cant unmount USB device"));
}
template <typename Func1>
void await(const typename QtPrivate::FunctionPointer<Func1>::Object *sender, Func1 signal, int timeoutMs = 0)
{
QEventLoop x;
QObject::connect(sender, signal, &x, &QEventLoop::quit);
std::unique_ptr<QTimer> t(nullptr);
bool timeout = false;
if (timeoutMs > 0)
{
t = std::make_unique<QTimer>(new QTimer);
t->setInterval(timeoutMs);
t->setSingleShot(true);
QObject::connect(t.get(), &QTimer::timeout, &x, [&timeout](){ timeout = true; });
QObject::connect(t.get(), &QTimer::timeout, &x, &QEventLoop::quit);
}
x.exec();
if (timeoutMs && t)
{
throw std::runtime_error("Timeout.");
}
}
void USB2Dialog::reset()
{
m_ui->pbCopy->setEnabled(false);
m_ui->pbRemove->setEnabled(false);
m_usbRoot.clear();
m_usbThpModel->clear();
retranslate(); // lblPath !
}
void USB2Dialog::slotChangePath()
{
QFileDialog *dlg = new QFileDialog(this);
dlg->setFileMode(QFileDialog::Directory);
dlg->setOption(QFileDialog::ShowDirsOnly);
dlg->setAttribute(Qt::WA_DeleteOnClose);
dlg->open(); // use exec?
bool accepted = false;
connect(dlg, &QFileDialog::finished, this, [&accepted](const int value){accepted = (value == 1);});
await(dlg, &QFileDialog::finished);
if (!accepted)
return;
const QStringList selected(dlg->selectedFiles());
if (selected.count() != 1)
return;
reset();
m_usbRoot = selected.first();
const QString scriptsJsonPath(scriptsJsLocation(m_usbRoot));
qWarning() << "m_usbRoot" << m_usbRoot << "scriptsJsonPath" << scriptsJsonPath;
if (QFile::exists(scriptsJsonPath))
{
qWarning() << "scripts.js exists!" << scriptsJsonPath;
initUsbModel();
return;
}
qWarning() << "scripts.js doesn't exists";
auto res = LtsWidgets::LtsMessageDialog::showMessage(this, tr("The selected directory '%1' doesn't contain THPs yet. <br />Do you want to initialize the directory now?").arg(m_usbRoot),
{}, LtsWidgets::Behaviour::Modal, LtsWidgets::Button::Yes | LtsWidgets::Button::Cancel);
if (res != LtsWidgets::Button::Yes)
{
return;
}
try
{
if (!QDir().mkpath(scriptsLocation(m_usbRoot)))
{
throw error("mkpath for m_usbRoot/scripts failed");
}
ScriptsJsParser(m_usbRoot, false).save(); // write empty scripts.js file
copyFiles(getFilesFromFolder(thpOverviewLocation(), m_usbRoot + QDir::separator()), this);
}
catch(const std::runtime_error &err)
{
ELOG << err.what();
new LtsWidgets::LtsMessageDialogNew(this, tr("The selected directory '%1' can't be written to correctly and the initialization was aborted.").arg(m_usbRoot));
return;
}
initUsbModel();
}
/*!
* \brief USB2Dialog::slotUnmount
* \details Check if name of usb device is part of the current path. Extract name of the usb device and start unmounting
* \author Niklas Schmidt
*/
void USB2Dialog::slotUnmount()
{
QString usbPath = QStringLiteral("/run/media/ctc_ws/");
//#ifdef LTS_DEBUG
// usbPath= QStringLiteral("/run/media/ctc/");
//#endif
QDir usbDir(usbPath);
usbDir.setFilter(QDir::Dirs | QDir::NoDotAndDotDot);
QStringList folderNames = usbDir.entryList();
for(const auto &folder : qAsConst(folderNames))
{
DEBUG_LOG(m_logger) << "Folder: " << folder << " USB Root: " << m_usbRoot;
if(m_usbRoot.contains(folder))
{
unmountUSB(folder);
return;
}
}
new LtsWidgets::LtsMessageDialogNew(this, tr("Path is not inside of a USB device"));
}
void USB2Dialog::slotCopy()
{
if (m_usbRoot.isEmpty())
return;
const auto selectedTopicIndices(m_ui->tvLocalTopics->selectionModel()->selectedRows());
if (selectedTopicIndices.isEmpty())
{
new LtsWidgets::LtsMessageDialogNew(this, tr("To export a THP, please select at least one topic in the 'Topics' table."));
return;
}
const QModelIndexList mil(m_ui->tvLocal->selectionModel()->selectedRows());
if (mil.size() != 1)
return;
const auto esi = m_localThpModel->getRowData(mil.first().row());
const auto &scriptId = esi.id();
try
{
// if script already in json, remove - also try to remove script folder (if exists - otherwise ignore)
removeScript(scriptId);
const QString srcPath(scriptLocation(localThpDir(), scriptId));
const QString dstPath(scriptLocation(m_usbRoot, scriptId));
CopyInfoList fileList;
fileList += getFilesFromFolder(srcPath, dstPath, { L("topics") }); // excludes topics
QSet<int> topicsToCopy;
for (const auto &idx : selectedTopicIndices)
{
bool k = false;
const int topicId = m_localTopicsModel->data(idx, Qt::UserRole).toInt(&k);
if (!k)
{
continue;
}
topicsToCopy += topicId;
}
qWarning() << "topicsToCopy" << topicsToCopy;
// only copy selected topics ...
const QString topicsDestDirPath(topicsLocation(m_usbRoot, scriptId)); // DEST!
const QJsonArray topicsArray(JsArrayParser::read(L("topicsJs"), topicsJsLocation(localThpDir(), scriptId) /*src*/, true));
const auto topics = Topic::topicsFromJson(topicsArray);
for (const auto &topic : topics)
{
qWarning() << "topic" << topic.id;
if (!topicsToCopy.contains(topic.id) || topic.filename.isEmpty() /* e.g. markers don't have files, no need to copy here */)
{
qWarning() << "SKIP FILE!";
continue;
}
const QFileInfo topicFile(topicLocation(localThpDir(), scriptId, topic.filename)); // SRC
fileList += { topicFile.absolutePath(), topicsDestDirPath, topicFile };
qWarning() << "!!!!! copy topic" << topicFile.absoluteFilePath() << "to" << topicsDestDirPath;
}
copyFiles(fileList, this);
// then finally write new, adjusted topics.js file (take original src file, write to destination)
QJsonArray finalTopicsArray;
for (const auto &topicVal : topicsArray)
{
auto topic = Topic::topicFromJson(topicVal.toObject());
if (!topicsToCopy.contains(topic.id))
{
continue;
}
finalTopicsArray += topicVal;
}
if (!QDir().mkpath(topicsLocation(m_usbRoot, scriptId))) // if no topics were copied (only marker topics), at least create the directory first for the topics.js file ...
{
throw error("mkpath for directory 'topics' failed");
}
JsArrayParser::write(L("topicsJs"), finalTopicsArray, topicsJsLocation(m_usbRoot, scriptId)); // NOTE: new, dst file!
// done! add copied script to model
m_usbThpModel->addScript(esi.exercise.id, esi.exercise.name, esi.script.id, esi.script.name, esi.createdAt);
}
catch(const error &e)
{
ELOG << e.what();
new LtsWidgets::LtsMessageDialogNew(this, tr("The files could not be copied successfully!"));
return;
}
new LtsWidgets::LtsMessageDialogNew(this, tr("The files were copied successfully"));
}
void USB2Dialog::removeScript(const QString &scriptId) noexcept(false)
{
if (scriptId.length() < 3)
{
ELOG << "scriptId is not in format 'n.n' - skipping for safety reasons - scriptId was: " << scriptId;
throw error("scriptId.length() < 3");
}
QDir dir(scriptLocation(m_usbRoot, scriptId));
if (!dir.exists())
{
ELOG << "script dir doesn't exist - ignoring - path was: " << dir.path();
}
else if (!dir.removeRecursively())
{
ELOG << "failed to remove script dir - path was: " << dir.path();
throw error("!dir.removeRecursively()");
}
m_usbThpModel->removeLocalScript(scriptId);
}
void USB2Dialog::slotRemove()
{
// this operation is fast enough to NOT do it async (meh, maybe not)
if (m_usbRoot.isEmpty())
return;
const QModelIndexList mil = m_ui->tvUSB->selectionModel()->selectedRows();
if (mil.size() < 1)
return;
DEBUG_LOG(m_logger) << "mil.size()" << mil.size();
QStringList scriptIdList;
for (const QModelIndex &idx : mil)
{
const QString scriptId(idx.data(Qt::UserRole).toString());
scriptIdList += scriptId;
}
auto res = LtsWidgets::LtsMessageDialog::showMessage(this, tr("Do you really want to remove the following script(s)?<br>%1").arg(scriptIdList.join(L(", "))),
{}, LtsWidgets::Behaviour::Modal, LtsWidgets::Button::Yes | LtsWidgets::Button::Cancel);
if (res != LtsWidgets::Button::Yes)
{
return;
}
try
{
for (const auto &scriptId : scriptIdList)
{
removeScript(scriptId);
}
}
catch(const std::runtime_error &e)
{
ELOG << e.what();
new LtsWidgets::LtsMessageDialogNew(this, tr("Failed to remove THP from from external medium."));
}
}
void USB2Dialog::slotSelectionChangedUSB()
{
const QModelIndexList mil(m_ui->tvUSB->selectionModel()->selectedRows());
bool enable = (mil.size() >= 1 && !m_usbRoot.isEmpty());
m_ui->pbRemove->setEnabled(enable);
}
QByteArray readFile(const QString &filename) noexcept(false)
{
QFile f(filename);
if (!f.open(QIODevice::ReadOnly))
{
throw error("Unable to read file at " + filename.toStdString() + ": " + f.errorString().toStdString());
}
return f.readAll();
}
QJsonDocument jsonDocument(const QByteArray &data) noexcept(false)
{
QJsonParseError err;
QJsonDocument doc(QJsonDocument::fromJson(data, &err));
if (err.error != QJsonParseError::NoError)
{
throw error(L("json parse error: %1 at offset %2").arg(err.errorString()).arg(err.offset).toStdString());
}
return doc;
}
void USB2Dialog::slotSelectionChangedLocal()
{
m_ui->pbCopy->setEnabled(false);
m_localTopicsModel->clear();
const QModelIndexList mil(m_ui->tvLocal->selectionModel()->selectedRows());
if (mil.size() != 1)
{
return;
}
const QString scriptId(mil.first().data(Qt::UserRole).toString());
try
{
qWarning() << "############### scriptId" << scriptId;
const QJsonArray topicsArray(JsArrayParser::read(L("topicsJs"), topicsJsLocation(localThpDir(), scriptId), true));
const auto topics = Topic::topicsFromJson(topicsArray);
for (const auto &topic : topics)
{
// if (topic.filterSet)
// continue;
m_localTopicsModel->addTopic(topic);
}
}
catch(const error &e)
{
ELOG << e.what();
new LtsWidgets::LtsMessageDialogNew(this, tr("Unable to read topics for this THP. Please select another THP."));
return;
}
m_ui->tvLocalTopics->selectAll();
m_ui->tvLocalTopics->resizeColumnToContents(TopicsModel::ColId);
m_ui->tvLocalTopics->resizeColumnToContents(TopicsModel::ColChannel);
m_ui->tvLocalTopics->resizeColumnToContents(TopicsModel::ColFilter);
m_ui->tvLocalTopics->resizeColumnToContents(TopicsModel::ColDescription);
m_ui->tvLocalTopics->resizeColumnToContents(TopicsModel::ColType);
m_ui->tvLocalTopics->resizeColumnToContents(TopicsModel::ColStart);
m_ui->tvLocalTopics->resizeColumnToContents(TopicsModel::ColDuration);
if (!m_usbRoot.isEmpty())
{
m_ui->pbCopy->setEnabled(true);
}
}
void USB2Dialog::retranslate()
{
translate(m_ui, tr("Export THP USB"));
m_ui->lblPath->setText(m_usbRoot.isEmpty() ? tr("(none)") : m_usbRoot);
}
void USB2Dialog::initLocalModel() noexcept(false)
{
DEBUG_LOG(m_logger) << "(re)initModel !!";
// TODO: move to model
// compiler_warning("TODO")
// m_fsWatcher->addPath(getRootFolder());
// m_fsWatcher->addPath(getContentFolder(m_session->exerciseId()));
m_localThpModel->init(localThpDir(), true /* watch script.json */);
slotSelectionChangedLocal(); // aka. enaDisa
}
} // namespace TakeHomePackageManager