File indexing completed on 2024-04-21 14:45:01

0001 /*
0002     SPDX-FileCopyrightText: 2018 Jasem Mutlaq <mutlaqja@ikarustech.com>
0003 
0004     Cloud Channel
0005 
0006     SPDX-License-Identifier: GPL-2.0-or-later
0007 */
0008 
0009 #include "cloud.h"
0010 #include "commands.h"
0011 #include "fitsviewer/fitsdata.h"
0012 
0013 #include "ekos_debug.h"
0014 #include "version.h"
0015 #include "../fitsviewer/fpack.h"
0016 #include "Options.h"
0017 
0018 #include <QtConcurrent>
0019 #include <QFutureWatcher>
0020 #include <KFormat>
0021 
0022 namespace EkosLive
0023 {
0024 
0025 Cloud::Cloud(Ekos::Manager * manager, QVector<QSharedPointer<NodeManager>> &nodeManagers):
0026     m_Manager(manager), m_NodeManagers(nodeManagers)
0027 {
0028     for (auto &nodeManager : m_NodeManagers)
0029     {
0030         if (nodeManager->cloud() == nullptr)
0031             continue;
0032 
0033         connect(nodeManager->cloud(), &Node::connected, this, &Cloud::onConnected);
0034         connect(nodeManager->cloud(), &Node::disconnected, this, &Cloud::onDisconnected);
0035         connect(nodeManager->cloud(), &Node::onTextReceived, this, &Cloud::onTextReceived);
0036     }
0037 
0038     connect(&watcher, &QFutureWatcher<bool>::finished, this, &Cloud::sendImage, Qt::UniqueConnection);
0039     connect(this, &Cloud::newImage, this, &Cloud::uploadImage);
0040     connect(Options::self(), &Options::EkosLiveCloudChanged, this, &Cloud::updateOptions);
0041 }
0042 
0043 ///////////////////////////////////////////////////////////////////////////////////////////
0044 ///
0045 ///////////////////////////////////////////////////////////////////////////////////////////
0046 bool Cloud::isConnected() const
0047 {
0048     return std::any_of(m_NodeManagers.begin(), m_NodeManagers.end(), [](auto & nodeManager)
0049     {
0050         return nodeManager->cloud() && nodeManager->cloud()->isConnected();
0051     });
0052 }
0053 
0054 void Cloud::onConnected()
0055 {
0056     auto node = qobject_cast<Node*>(sender());
0057     if (!node)
0058         return;
0059 
0060     qCInfo(KSTARS_EKOS) << "Connected to Cloud Websocket server at" << node->url().toDisplayString();
0061 
0062     emit connected();
0063 }
0064 
0065 void Cloud::onDisconnected()
0066 {
0067     qCInfo(KSTARS_EKOS) << "Disconnected from Cloud Websocket server.";
0068     m_sendBlobs = true;
0069 
0070     for (auto &oneFile : temporaryFiles)
0071         QFile::remove(oneFile);
0072     temporaryFiles.clear();
0073 
0074     emit disconnected();
0075 }
0076 
0077 void Cloud::onTextReceived(const QString &message)
0078 {
0079     qCInfo(KSTARS_EKOS) << "Cloud Text Websocket Message" << message;
0080     QJsonParseError error;
0081     auto serverMessage = QJsonDocument::fromJson(message.toLatin1(), &error);
0082     if (error.error != QJsonParseError::NoError)
0083     {
0084         qCWarning(KSTARS_EKOS) << "Ekos Live Parsing Error" << error.errorString();
0085         return;
0086     }
0087 
0088     const QJsonObject msgObj = serverMessage.object();
0089     const QString command = msgObj["type"].toString();
0090     if (command == commands[SET_BLOBS])
0091         m_sendBlobs = msgObj["payload"].toBool();
0092     else if (command == commands[LOGOUT])
0093     {
0094         for (auto &nodeManager : m_NodeManagers)
0095         {
0096             if (nodeManager->cloud() == nullptr)
0097                 continue;
0098 
0099             nodeManager->cloud()->disconnectServer();
0100         }
0101     }
0102 }
0103 
0104 void Cloud::upload(const QSharedPointer<FITSData> &data, const QString &uuid)
0105 {
0106     if (Options::ekosLiveCloud() == false  || m_sendBlobs == false)
0107         return;
0108 
0109     m_UUID = uuid;
0110     m_ImageData = data;
0111     sendImage();
0112 }
0113 
0114 void Cloud::upload(const QString &filename, const QString &uuid)
0115 {
0116     if (Options::ekosLiveCloud() == false  || m_sendBlobs == false)
0117         return;
0118 
0119     watcher.waitForFinished();
0120     m_UUID = uuid;
0121     m_ImageData.reset(new FITSData(), &QObject::deleteLater);
0122     QFuture<bool> result = m_ImageData->loadFromFile(filename);
0123     watcher.setFuture(result);
0124 }
0125 
0126 void Cloud::sendImage()
0127 {
0128     QtConcurrent::run(this, &Cloud::asyncUpload);
0129 }
0130 
0131 void Cloud::asyncUpload()
0132 {
0133     // Send complete metadata
0134     // Add file name and size
0135     QJsonObject metadata;
0136     // Skip empty or useless metadata
0137     for (const auto &oneRecord : m_ImageData->getRecords())
0138     {
0139         if (oneRecord.key.isEmpty() || oneRecord.value.toString().isEmpty())
0140             continue;
0141         metadata.insert(oneRecord.key.toLower(), QJsonValue::fromVariant(oneRecord.value));
0142     }
0143 
0144     // Filename only without path
0145     QString filepath = m_ImageData->filename();
0146     QString filenameOnly = QFileInfo(filepath).fileName();
0147 
0148     // Add filename and size as wells
0149     metadata.insert("uuid", m_UUID);
0150     metadata.insert("filename", filenameOnly);
0151     metadata.insert("filesize", static_cast<int>(m_ImageData->size()));
0152     // Must set Content-Disposition so
0153     metadata.insert("Content-Disposition", QString("attachment;filename=%1.fz").arg(filenameOnly));
0154 
0155     QByteArray image;
0156     QByteArray meta = QJsonDocument(metadata).toJson(QJsonDocument::Compact);
0157     meta = meta.leftJustified(METADATA_PACKET, 0);
0158     image += meta;
0159 
0160     QString compressedFile = QDir::tempPath() + QString("/ekoslivecloud%1").arg(m_UUID);
0161     m_ImageData->saveImage(compressedFile + QStringLiteral("[compress R]"));
0162     // Upload the compressed image
0163     QFile compressedImage(compressedFile);
0164     if (compressedImage.open(QIODevice::ReadOnly))
0165     {
0166         image += compressedImage.readAll();
0167         emit newImage(image);
0168         qCInfo(KSTARS_EKOS) << "Uploaded" << compressedFile << " to the cloud";
0169     }
0170 
0171     // Remove from disk if temporary
0172     if (compressedFile != filepath && compressedFile.startsWith(QDir::tempPath()))
0173         QFile::remove(compressedFile);
0174 
0175     m_ImageData.reset();
0176 }
0177 
0178 void Cloud::uploadImage(const QByteArray &image)
0179 {
0180     for (auto &nodeManager : m_NodeManagers)
0181     {
0182         if (nodeManager->cloud() == nullptr)
0183             continue;
0184 
0185         nodeManager->cloud()->sendBinaryMessage(image);
0186     }
0187 }
0188 
0189 void Cloud::updateOptions()
0190 {
0191     // In case cloud storage is toggled, inform cloud
0192     // websocket channel of this change.
0193     QJsonObject payload = {{"name", "ekosLiveCloud"}, {"value", Options::ekosLiveCloud()}};
0194     QJsonObject message =
0195     {
0196         {"type",  commands[OPTION_SET]},
0197         {"payload", payload}
0198     };
0199 
0200     for (auto &nodeManager : m_NodeManagers)
0201     {
0202         if (nodeManager->cloud() == nullptr)
0203             continue;
0204 
0205         nodeManager->cloud()->sendTextMessage(QJsonDocument(message).toJson(QJsonDocument::Compact));
0206     }
0207 }
0208 
0209 }