Kstars

cloud.cpp
1 /*
2  SPDX-FileCopyrightText: 2018 Jasem Mutlaq <[email protected]>
3 
4  Cloud Channel
5 
6  SPDX-License-Identifier: GPL-2.0-or-later
7 */
8 
9 #include "cloud.h"
10 #include "commands.h"
11 #include "profileinfo.h"
12 
13 #include "fitsviewer/fitsview.h"
14 #include "fitsviewer/fitsdata.h"
15 #include "fitsviewer/fpack.h"
16 
17 #include "ekos_debug.h"
18 
19 #include <QtConcurrent>
20 #include <QFutureWatcher>
21 #include <KFormat>
22 
23 namespace EkosLive
24 {
25 
26 Cloud::Cloud(Ekos::Manager * manager): m_Manager(manager)
27 {
28  connect(&m_WebSocket, &QWebSocket::connected, this, &Cloud::onConnected);
29  connect(&m_WebSocket, &QWebSocket::disconnected, this, &Cloud::onDisconnected);
30  connect(&m_WebSocket, static_cast<void(QWebSocket::*)(QAbstractSocket::SocketError)>(&QWebSocket::error), this,
31  &Cloud::onError);
32 
33  connect(&watcher, &QFutureWatcher<bool>::finished, this, &Cloud::sendImage, Qt::UniqueConnection);
34 
35  connect(this, &Cloud::newMetadata, this, &Cloud::uploadMetadata);
36  connect(this, &Cloud::newImage, this, &Cloud::uploadImage);
37 }
38 
39 void Cloud::connectServer()
40 {
41  QUrl requestURL(m_URL);
42 
43  QUrlQuery query;
44  query.addQueryItem("username", m_AuthResponse["username"].toString());
45  query.addQueryItem("token", m_AuthResponse["token"].toString());
46  if (m_AuthResponse.contains("remoteToken"))
47  query.addQueryItem("remoteToken", m_AuthResponse["remoteToken"].toString());
48  if (m_Options[OPTION_SET_CLOUD_STORAGE])
49  query.addQueryItem("cloudEnabled", "true");
50  query.addQueryItem("email", m_AuthResponse["email"].toString());
51  query.addQueryItem("from_date", m_AuthResponse["from_date"].toString());
52  query.addQueryItem("to_date", m_AuthResponse["to_date"].toString());
53  query.addQueryItem("plan_id", m_AuthResponse["plan_id"].toString());
54  query.addQueryItem("type", m_AuthResponse["type"].toString());
55 
56  requestURL.setPath("/cloud/ekos");
57  requestURL.setQuery(query);
58 
59  m_WebSocket.open(requestURL);
60 
61  qCInfo(KSTARS_EKOS) << "Connecting to cloud websocket server at" << requestURL.toDisplayString();
62 }
63 
64 void Cloud::disconnectServer()
65 {
66  m_WebSocket.close();
67 }
68 
69 void Cloud::onConnected()
70 {
71  qCInfo(KSTARS_EKOS) << "Connected to Cloud Websocket server at" << m_URL.toDisplayString();
72 
73  connect(&m_WebSocket, &QWebSocket::textMessageReceived, this, &Cloud::onTextReceived);
74 
75  m_isConnected = true;
76  m_ReconnectTries = 0;
77 
78  emit connected();
79 }
80 
81 void Cloud::onDisconnected()
82 {
83  qCInfo(KSTARS_EKOS) << "Disconnected from Cloud Websocket server.";
84  m_isConnected = false;
85 
86  disconnect(&m_WebSocket, &QWebSocket::textMessageReceived, this, &Cloud::onTextReceived);
87 
88  m_sendBlobs = true;
89 
90  for (const QString &oneFile : temporaryFiles)
91  QFile::remove(oneFile);
92  temporaryFiles.clear();
93 
94  emit disconnected();
95 }
96 
97 void Cloud::onError(QAbstractSocket::SocketError error)
98 {
99  qCritical(KSTARS_EKOS) << "Cloud Websocket connection error" << m_WebSocket.errorString();
102  {
103  if (m_ReconnectTries++ < RECONNECT_MAX_TRIES)
104  QTimer::singleShot(RECONNECT_INTERVAL, this, SLOT(connectServer()));
105  }
106 }
107 
108 void Cloud::onTextReceived(const QString &message)
109 {
110  qCInfo(KSTARS_EKOS) << "Cloud Text Websocket Message" << message;
111  QJsonParseError error;
112  auto serverMessage = QJsonDocument::fromJson(message.toLatin1(), &error);
113  if (error.error != QJsonParseError::NoError)
114  {
115  qCWarning(KSTARS_EKOS) << "Ekos Live Parsing Error" << error.errorString();
116  return;
117  }
118 
119  const QJsonObject msgObj = serverMessage.object();
120  const QString command = msgObj["type"].toString();
121  // const QJsonObject payload = msgObj["payload"].toObject();
122 
123  // if (command == commands[ALIGN_SET_FILE_EXTENSION])
124  // extension = payload["ext"].toString();
125  if (command == commands[SET_BLOBS])
126  m_sendBlobs = msgObj["payload"].toBool();
127  else if (command == commands[LOGOUT])
128  disconnectServer();
129 }
130 
131 void Cloud::upload(const QSharedPointer<FITSData> &data, const QString &uuid)
132 {
133  if (m_isConnected == false || m_Options[OPTION_SET_CLOUD_STORAGE] == false || m_sendBlobs == false)
134  return;
135 
136  m_UUID = uuid;
137  m_ImageData = data;
138  sendImage();
139 }
140 
141 void Cloud::upload(const QString &filename, const QString &uuid)
142 {
143  if (m_isConnected == false || m_Options[OPTION_SET_CLOUD_STORAGE] == false || m_sendBlobs == false)
144  return;
145 
146  watcher.waitForFinished();
147  m_UUID = uuid;
148  m_ImageData.reset(new FITSData(), &QObject::deleteLater);
149  QFuture<bool> result = m_ImageData->loadFromFile(filename);
150  watcher.setFuture(result);
151 }
152 
153 void Cloud::sendImage()
154 {
155  QtConcurrent::run(this, &Cloud::asyncUpload);
156 }
157 
158 void Cloud::asyncUpload()
159 {
160  // Send complete metadata
161  // Add file name and size
162  QJsonObject metadata;
163  // Skip empty or useless metadata
164  for (const auto &oneRecord : m_ImageData->getRecords())
165  {
166  if (oneRecord.key.isEmpty() || oneRecord.value.toString().isEmpty())
167  continue;
168  metadata.insert(oneRecord.key.toLower(), QJsonValue::fromVariant(oneRecord.value));
169  }
170 
171  // Filename only without path
172  QString filepath = m_ImageData->isCompressed() ? m_ImageData->compressedFilename() : m_ImageData->filename();
173  QString filenameOnly = QFileInfo(filepath).fileName();
174 
175  // Add filename and size as wells
176  metadata.insert("uuid", m_UUID);
177  metadata.insert("filename", filenameOnly);
178  metadata.insert("filesize", static_cast<int>(m_ImageData->size()));
179  // Must set Content-Disposition so
180  if (m_ImageData->isCompressed())
181  metadata.insert("Content-Disposition", QString("attachment;filename=%1").arg(filenameOnly));
182  else
183  metadata.insert("Content-Disposition", QString("attachment;filename=%1.fz").arg(filenameOnly));
184 
185  emit newMetadata(QJsonDocument(metadata).toJson(QJsonDocument::Compact));
186  //m_WebSocket.sendTextMessage(QJsonDocument(metadata).toJson(QJsonDocument::Compact));
187 
188  qCInfo(KSTARS_EKOS) << "Uploading file to the cloud with metadata" << metadata;
189 
190  QString compressedFile = filepath;
191  // Use cfitsio pack to compress the file first
192  if (m_ImageData->isCompressed() == false)
193  {
194  compressedFile = QDir::tempPath() + QString("/ekoslivecloud%1").arg(m_UUID);
195 
196  int isLossLess = 0;
197  fpstate fpvar;
198  fp_init (&fpvar);
199  if (fp_pack(filepath.toLatin1().data(), compressedFile.toLatin1().data(), fpvar, &isLossLess) < 0)
200  {
201  if (filepath.startsWith(QDir::tempPath()))
202  QFile::remove(filepath);
203  qCCritical(KSTARS_EKOS) << "Cloud upload failed. Failed to compress" << filepath;
204  return;
205  }
206  }
207 
208  // Upload the compressed image
209  QFile image(compressedFile);
210  if (image.open(QIODevice::ReadOnly))
211  {
212  //m_WebSocket.sendBinaryMessage(image.readAll());
213  emit newImage(image.readAll());
214  qCInfo(KSTARS_EKOS) << "Uploaded" << compressedFile << " to the cloud";
215  }
216  image.close();
217 
218  // Remove from disk if temporary
219  if (compressedFile != filepath && compressedFile.startsWith(QDir::tempPath()))
220  QFile::remove(compressedFile);
221 
222  m_ImageData.reset();
223 }
224 
225 void Cloud::uploadMetadata(const QByteArray &metadata)
226 {
227  m_WebSocket.sendTextMessage(metadata);
228 }
229 
230 void Cloud::uploadImage(const QByteArray &image)
231 {
232  m_WebSocket.sendBinaryMessage(image);
233 }
234 
235 void Cloud::setOptions(QMap<int, bool> options)
236 {
237  bool cloudEnabled = m_Options[OPTION_SET_CLOUD_STORAGE];
238  m_Options = options;
239 
240  // In case cloud storage is toggled, inform cloud
241  // websocket channel of this change.
242  if (cloudEnabled != m_Options[OPTION_SET_CLOUD_STORAGE])
243  {
244  bool enabled = m_Options[OPTION_SET_CLOUD_STORAGE];
245  QJsonObject payload = {{"value", enabled}};
247  {
248  {"type", commands[OPTION_SET_CLOUD_STORAGE]},
249  {"payload", payload}
250  };
251 
252  m_WebSocket.sendTextMessage(QJsonDocument(message).toJson(QJsonDocument::Compact));
253  }
254 }
255 
256 }
QFuture< T > run(Function function,...)
QJsonDocument fromJson(const QByteArray &json, QJsonParseError *error)
bool remove()
virtual bool open(QIODevice::OpenMode mode) override
QAbstractSocket::SocketError error() const const
QByteArray toLatin1() const const
QString tempPath()
void deleteLater()
QJsonObject::iterator insert(const QString &key, const QJsonValue &value)
QString toDisplayString(QUrl::FormattingOptions options) const const
UniqueConnection
virtual void close() override
void disconnected()
QString fileName() const const
void connected()
void setQuery(const QString &query, QUrl::ParsingMode mode)
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
Generic record interfaces and implementations.
Definition: cloud.cpp:23
void setPath(const QString &path, QUrl::ParsingMode mode)
QByteArray readAll()
void textMessageReceived(const QString &message)
QJsonValue fromVariant(const QVariant &variant)
QString message
char * data()
This file is part of the KDE documentation.
Documentation copyright © 1996-2022 The KDE developers.
Generated on Fri Aug 12 2022 04:00:53 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.