/** * @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 #include "tpm_usb_dialog.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include //#include #include #include #include #ifdef Q_OS_UNIX #include #endif #include #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 void await(const typename QtPrivate::FunctionPointer::Object *sender, Func1 signal, int timeoutMs = 0) { QEventLoop x; QObject::connect(sender, signal, &x, &QEventLoop::quit); std::unique_ptr t(nullptr); bool timeout = false; if (timeoutMs > 0) { t = std::make_unique(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.
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 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)?
%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