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 }