mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-14 08:38:45 +00:00
489 lines
17 KiB
C++
489 lines
17 KiB
C++
/*
|
|
* Copyright (c) 2017 Mark Liversedge <liversedge@gmail.com>
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License as published by the Free
|
|
* Software Foundation; either version 2 of the License, or (at your option)
|
|
* any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
* more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along
|
|
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
|
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
*/
|
|
|
|
#include "SixCycle.h"
|
|
#include "Athlete.h"
|
|
#include "Settings.h"
|
|
#include <QByteArray>
|
|
#include <QHttpMultiPart>
|
|
#include <QJsonDocument>
|
|
#include <QJsonArray>
|
|
#include <QJsonObject>
|
|
#include <QJsonValue>
|
|
|
|
|
|
#ifndef SIXCYCLE_DEBUG
|
|
#define SIXCYCLE_DEBUG false
|
|
#endif
|
|
#ifdef Q_CC_MSVC
|
|
#define printd(fmt, ...) do { \
|
|
if (SIXCYCLE_DEBUG) { \
|
|
printf("[%s:%d %s] " fmt , __FILE__, __LINE__, \
|
|
__FUNCTION__, __VA_ARGS__); \
|
|
fflush(stdout); \
|
|
} \
|
|
} while(0)
|
|
#else
|
|
#define printd(fmt, args...) \
|
|
do { \
|
|
if (SIXCYCLE_DEBUG) { \
|
|
printf("[%s:%d %s] " fmt , __FILE__, __LINE__, \
|
|
__FUNCTION__, ##args); \
|
|
fflush(stdout); \
|
|
} \
|
|
} while(0)
|
|
#endif
|
|
|
|
SixCycle::SixCycle(Context *context) : CloudService(context), context(context), root_(NULL)
|
|
{
|
|
if (context) {
|
|
nam = new QNetworkAccessManager(this);
|
|
connect(nam, SIGNAL(sslErrors(QNetworkReply*, const QList<QSslError> & )), this, SLOT(onSslErrors(QNetworkReply*, const QList<QSslError> & )));
|
|
}
|
|
|
|
// how is data uploaded and downloaded?
|
|
uploadCompression = gzip;
|
|
downloadCompression = none;
|
|
filetype = CloudService::uploadType::TCX;
|
|
session_token = ""; // not authenticated yet
|
|
|
|
useMetric = true; // distance and duration metadata
|
|
useEndDate = false; // startDateTime added, so no longer need this
|
|
|
|
// config
|
|
settings.insert(Username, GC_SIXCYCLE_USER);
|
|
settings.insert(Password, GC_SIXCYCLE_PASS);
|
|
settings.insert(URL, GC_SIXCYCLE_URL);
|
|
settings.insert(DefaultURL, "https://live.sixcycle.com");
|
|
}
|
|
|
|
SixCycle::~SixCycle() {
|
|
if (context) delete nam;
|
|
}
|
|
|
|
void
|
|
SixCycle::onSslErrors(QNetworkReply *reply, const QList<QSslError>&)
|
|
{
|
|
reply->ignoreSslErrors();
|
|
}
|
|
|
|
// open by connecting and getting a basic list of folders available
|
|
bool
|
|
SixCycle::open(QStringList &errors)
|
|
{
|
|
printd("Sixcycle::open\n");
|
|
|
|
// do we have a token
|
|
QString user = getSetting(GC_SIXCYCLE_USER, "").toString();
|
|
QString pass = getSetting(GC_SIXCYCLE_PASS, "").toString();
|
|
|
|
if (user == "") {
|
|
errors << "You must setup Sixcycle login details first";
|
|
return false;
|
|
}
|
|
|
|
// use the configed URL
|
|
QString url = QString("%1/rest-auth/login/")
|
|
.arg(getSetting(GC_SIXCYCLE_URL, "https://live.sixcycle.com").toString());
|
|
|
|
// We need to POST a form with the user and password
|
|
QUrlQuery postData(url);
|
|
postData.addQueryItem("email", user);
|
|
postData.addQueryItem("password", pass);
|
|
|
|
QNetworkRequest request(url);
|
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
|
|
QNetworkReply *reply = nam->post(request, postData.toString(QUrl::FullyEncoded).toUtf8());
|
|
|
|
// blocking request - wait for response, timeout after 5 seconds
|
|
QEventLoop loop;
|
|
|
|
connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
|
|
connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
|
|
QTimer::singleShot(5000,&loop, SLOT(quit())); // timeout after 5 seconds
|
|
|
|
loop.exec();
|
|
|
|
if (reply->error() != QNetworkReply::NoError) {
|
|
qDebug() << "error" << reply->errorString();
|
|
errors << tr("Network Problem authenticating with the Sixcycle service");
|
|
return false;
|
|
}
|
|
|
|
// did we get a good response ?
|
|
QByteArray r = reply->readAll();
|
|
QJsonParseError parseError;
|
|
QJsonDocument document = QJsonDocument::fromJson(r, &parseError);
|
|
|
|
// if path was returned all is good, lets set root
|
|
if (parseError.error == QJsonParseError::NoError) {
|
|
printd("NoError\n");
|
|
|
|
QJsonObject content = document.object();
|
|
|
|
// extract the user root and the session_token
|
|
QJsonValue key = content["key"];
|
|
session_token = key.toString();
|
|
printd("session token: %s\n", session_token.toStdString().c_str());
|
|
|
|
QJsonValue user = content["user"];
|
|
session_user = user.toString();
|
|
printd("session user: %s\n", session_user.toStdString().c_str());
|
|
|
|
// set root (there is no directory concept for SixCycle)
|
|
root_ = newCloudServiceEntry();
|
|
|
|
// path name
|
|
root_->name = "/";
|
|
root_->isDir = true;
|
|
root_->size = 0;
|
|
|
|
// happy with what we got ?
|
|
if (root_->name != "/") errors << tr("invalid root path.");
|
|
if (root_->isDir != true) errors << tr("root is not a directory.");
|
|
|
|
} else {
|
|
errors << tr("Problem with Sixcycle authentication.");
|
|
printd("timed out, empty or invalid response.\n");
|
|
}
|
|
|
|
// ok so far ?
|
|
if (errors.count()) return false;
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
SixCycle::close()
|
|
{
|
|
printd("Sixcycle::close\n");
|
|
// nothing to do for now
|
|
return true;
|
|
}
|
|
|
|
// home dire
|
|
QString
|
|
SixCycle::home()
|
|
{
|
|
return "";
|
|
}
|
|
|
|
bool
|
|
SixCycle::createFolder(QString)
|
|
{
|
|
printd("Sixcycle::createFolder\n");
|
|
|
|
return false;
|
|
}
|
|
|
|
QList<CloudServiceEntry*>
|
|
SixCycle::readdir(QString path, QStringList &errors, QDateTime from, QDateTime to)
|
|
{
|
|
Q_UNUSED(from)
|
|
Q_UNUSED(to)
|
|
|
|
printd("Sixcycle::readdir(%s)\n", path.toStdString().c_str());
|
|
|
|
QList<CloudServiceEntry*> returning;
|
|
if (session_token == "") {
|
|
errors << tr("You must authenticate with Sixcycle first");
|
|
return returning;
|
|
}
|
|
|
|
// From the SixCycle docs:
|
|
//
|
|
// curl -X POST -H "Authorization: Token b635be6030e563fc74840bdac7811f4e994c7b61"
|
|
// https://live.sixcycle.com/api/v1/activitysummaryfile/
|
|
//
|
|
// example responses:
|
|
// [{"url":"https://s3.amazonaws.com/sixcycle/rawFiles/biking_bd372d69-ae61-4e08-accc-a08805cc67c7.fit",
|
|
// "dataCheckSum":"28352b42980ce8d09e18feeee027621c","activityEndDateTime":"2015-07-11T17:36:26Z","id":28684},
|
|
// {"url":"https://s3.amazonaws.com/sixcycle/rawFiles/com_20160229_105346_2016-02-29-06-37-38_06f16b19-7f1f-418a-8af4-2eb0869a9d57.fit",
|
|
// "dataCheckSum":"9052927da57b8fc44783e76322d30c2c","activityEndDateTime":"2016-02-29T12:31:10Z","id":28682},
|
|
// {"url":"https://s3.amazonaws.com/sixcycle/rawFiles/840_conordunne_2015-09-13-08-55-15Z_d44fb7c7-ed3a-4e66-9b6b-c6a782974c5b.pwx",
|
|
// "dataCheckSum":"6caf38aa1c0bb2101225680e05f5ece6","activityEndDateTime":"2015-09-10T15:44:54Z","id":35929}]
|
|
//
|
|
QString url = QString("%1/api/v1/activitysummaryfile/")
|
|
.arg(getSetting(GC_SIXCYCLE_URL, "https://live.sixcycle.com").toString());
|
|
|
|
printd("endpoint: %s\n", url.toStdString().c_str());
|
|
|
|
// request using the session token
|
|
QNetworkRequest request(url);
|
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
|
|
request.setRawHeader("Authorization", (QString("Token %1").arg(session_token)).toLatin1());
|
|
|
|
printd("session token: %s\n", session_token.toStdString().c_str());
|
|
|
|
// pass the user id
|
|
QUrlQuery postData(url);
|
|
postData.addQueryItem("user", session_user);
|
|
|
|
printd("user: %s\n", session_user.toStdString().c_str());
|
|
|
|
// post the request
|
|
reply = nam->post(request, postData.toString(QUrl::FullyEncoded).toUtf8());
|
|
|
|
// blocking request, with a 10 seconds timeout (might have a lot of data)
|
|
QEventLoop loop;
|
|
connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
|
|
QTimer::singleShot(10000,&loop, SLOT(quit())); // timeout after 10000 seconds
|
|
|
|
loop.exec();
|
|
|
|
// did we get a good response ?
|
|
QByteArray r = reply->readAll();
|
|
printd("response begins: %s ...\n", r.toStdString().substr(0,900).c_str());
|
|
|
|
QJsonParseError parseError;
|
|
QJsonDocument document = QJsonDocument::fromJson(r, &parseError);
|
|
|
|
// if path was returned all is good, lets set root
|
|
if (parseError.error == QJsonParseError::NoError) {
|
|
|
|
// results ?
|
|
QJsonArray results = document.array();
|
|
|
|
printd("items found: %d\n", results.size());
|
|
|
|
// lets look at that then
|
|
for(int i=0; i<results.size(); i++) {
|
|
|
|
QJsonObject each = results.at(i).toObject();
|
|
CloudServiceEntry *add = newCloudServiceEntry();
|
|
|
|
//SixCycle has full path, we just want the file name
|
|
add->id = each["url"].toString();
|
|
add->label = QString("%1").arg(each["id"].toInt());
|
|
add->isDir = false;
|
|
add->distance = double(each["meters_in_distance"].toInt()) / 1000.0f;
|
|
add->duration = double(each["seconds_in_activity"].toInt());
|
|
//add->size
|
|
//add->modified
|
|
|
|
//QString name = QDateTime::fromMSecsSinceEpoch(each["ts"].toDouble()).toString("yyyy_MM_dd_HH_mm_ss")+=".json";
|
|
//add->name = name;
|
|
QString dateString = each["activityStartDateTime"].toString();
|
|
QString suffix = QFileInfo(add->id).suffix();
|
|
|
|
// data might be compressed remotely, need to adapt to their scheme of naming
|
|
// the rawfiles with the format at the front and compression at the back of the
|
|
// filename; e.g:
|
|
// https://s3.amazonaws.com/sixcycle/rawFiles/tcx_ba4319c2-a6dd-4503-9a50-0cb1f0f832ab.gz
|
|
//
|
|
if (suffix == "gz") {
|
|
QString prefix = add->id.split("/").last().split("_").first();
|
|
suffix = prefix + "." + suffix;
|
|
}
|
|
QDateTime startTime= QDateTime::fromString(dateString, Qt::ISODate).toLocalTime();
|
|
QChar zero = QLatin1Char ( '0' );
|
|
add->name = QString ( "%1_%2_%3_%4_%5_%6.%7" )
|
|
.arg ( startTime.date().year(), 4, 10, zero )
|
|
.arg ( startTime.date().month(), 2, 10, zero )
|
|
.arg ( startTime.date().day(), 2, 10, zero )
|
|
.arg ( startTime.time().hour(), 2, 10, zero )
|
|
.arg ( startTime.time().minute(), 2, 10, zero )
|
|
.arg ( startTime.time().second(), 2, 10, zero )
|
|
.arg ( suffix );
|
|
|
|
printd("entry: %s (%s) [%s]\n", add->name.toStdString().c_str(), add->label.toStdString().c_str(), dateString.toStdString().c_str());
|
|
|
|
returning << add;
|
|
}
|
|
}
|
|
|
|
// all good ?
|
|
return returning;
|
|
}
|
|
|
|
// read a file at location (relative to home) into passed array
|
|
bool
|
|
SixCycle::readFile(QByteArray *data, QString remotename, QString remoteid)
|
|
{
|
|
printd("Sixcycle::readFile(%s)\n", remotename.toStdString().c_str());
|
|
|
|
if (session_token == "") {
|
|
return false;
|
|
}
|
|
|
|
// this must be performed asyncronously and call made
|
|
// to notifyReadComplete(QByteArray &data, QString remotename, QString message) when done
|
|
|
|
// lets connect and get basic info on the root directory
|
|
QString url = remoteid;
|
|
|
|
printd("url:%s\n", url.toStdString().c_str());
|
|
|
|
// request using the bearer token
|
|
QNetworkRequest request(url);
|
|
//request.setRawHeader("Authorization", (QString("Token %1").arg(session_token)).toLatin1());
|
|
//request.setRawHeader("Accept-Encoding", "gzip, deflate");
|
|
|
|
// put the file
|
|
QNetworkReply *reply = nam->get(request);
|
|
|
|
// remember
|
|
mapReply(reply,remotename);
|
|
buffers.insert(reply,data);
|
|
|
|
// catch finished signal
|
|
connect(reply, SIGNAL(finished()), this, SLOT(readFileCompleted()));
|
|
connect(reply, SIGNAL(readyRead()), this, SLOT(readyRead()));
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
SixCycle::writeFile(QByteArray &data, QString remotename, RideFile *ride)
|
|
{
|
|
Q_UNUSED(ride);
|
|
|
|
printd("Sixcycle::writeFile(%s)\n", remotename.toStdString().c_str());
|
|
|
|
// if we are called to upload a single ride we will not have been
|
|
// authenticated yet, so lets try and do that now. When called via
|
|
// the SyncDialog this will have already been done.
|
|
if (session_token == "") {
|
|
|
|
// we need to authenticate !
|
|
QStringList errors;
|
|
open(errors);
|
|
|
|
// we didn't get a token?
|
|
if (session_token == "") return false;
|
|
}
|
|
|
|
// Writing to SixCycle:
|
|
//
|
|
// curl -X POST -H "Authorization: Token b635be6030e563fc74840bdac7811f4e994c7b61" -F "rawFile=@running.fit" -F user="http://stg.sixcycle.com/api/v1/user/1174/" -v https://stg.sixcycle.com/api/v1/activitysummarydata/
|
|
//
|
|
// The -v tag means verbose and will provide all the protocol info. It should look something like this:
|
|
//
|
|
//> POST /api/v1/activitysummarydata/ HTTP/1.1
|
|
//> Host: stg.sixcycle.com
|
|
//> User-Agent: curl/7.50.3
|
|
//> Accept: */*
|
|
//> Authorization: Token b635be6030e563fc74840bdac7811f4e994c7b61
|
|
//> Content-Length: 36969
|
|
//> Expect: 100-continue
|
|
//> Content-Type: multipart/form-data; boundary=------------------------24675cd48c8a2baf
|
|
|
|
//The header content-type should be multipart/form-data and we have 2 fields, user and rawFile.
|
|
//The user field is a url given in the login response on success.
|
|
|
|
// set the target url
|
|
QString url = QString("%1/api/v1/activitysummarydata/")
|
|
.arg(getSetting(GC_SIXCYCLE_URL, "https://live.sixcycle.com").toString());
|
|
//url = "http://requestb.in/1bakg1q1";
|
|
printd("endpoint: '%s'\n", url.toStdString().c_str());
|
|
|
|
// create a request passing the session token and user
|
|
QNetworkRequest request(url);
|
|
request.setRawHeader("Authorization", (QString("Token %1").arg(session_token)).toLatin1());
|
|
|
|
// MULTIPART *****************
|
|
QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
|
|
|
|
// boundary is a random number
|
|
QString boundary = QVariant(qrand()).toString()+QVariant(qrand()).toString()+QVariant(qrand()).toString();
|
|
multiPart->setBoundary(boundary.toLatin1());
|
|
printd("boundary: '%s'\n", boundary.toStdString().c_str());
|
|
|
|
// set the user this is for
|
|
QHttpPart userPart;
|
|
userPart.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
|
|
userPart.setHeader(QNetworkRequest::ContentDispositionHeader,
|
|
QString("form-data; name=\"user\";"));
|
|
userPart.setBody(QByteArray(session_user.toLatin1()));
|
|
|
|
// second part is the file (rawFile)
|
|
printd("user: %s\n", session_user.toStdString().c_str());
|
|
printd("remotename: %s\n", remotename.toStdString().c_str());
|
|
printd("data begins: %s ...\n", data.toStdString().substr(0,20).c_str());
|
|
|
|
QHttpPart filePart;
|
|
filePart.setHeader(QNetworkRequest::ContentTypeHeader, "application/octet-stream");
|
|
filePart.setHeader(QNetworkRequest::ContentDispositionHeader,
|
|
QString("form-data; name=\"rawFile\"; filename=\"%1\";").arg(remotename));
|
|
filePart.setBody(data);
|
|
|
|
multiPart->append(filePart);
|
|
multiPart->append(userPart);
|
|
|
|
// post the file
|
|
reply = nam->post(request, multiPart);
|
|
|
|
multiPart->setParent(reply);
|
|
|
|
// catch finished signal
|
|
connect(reply, SIGNAL(finished()), this, SLOT(writeFileCompleted()));
|
|
|
|
// remember
|
|
mapReply(reply,remotename);
|
|
|
|
return true;
|
|
}
|
|
|
|
void
|
|
SixCycle::writeFileCompleted()
|
|
{
|
|
printd("Sixcycle::writeFileCompleted()\n");
|
|
|
|
QString writestatus = reply->readAll();
|
|
|
|
printd("reply begins: %s ...\n", writestatus.toStdString().substr(0,80).c_str());
|
|
|
|
if (reply->error() == QNetworkReply::NoError) {
|
|
notifyWriteComplete(
|
|
replyName(static_cast<QNetworkReply*>(QObject::sender())),
|
|
tr("Completed."));
|
|
} else {
|
|
notifyWriteComplete(
|
|
replyName(static_cast<QNetworkReply*>(QObject::sender())),
|
|
tr("Network Error - Upload failed."));
|
|
}
|
|
}
|
|
|
|
void
|
|
SixCycle::readyRead()
|
|
{
|
|
QNetworkReply *reply = static_cast<QNetworkReply*>(QObject::sender());
|
|
buffers.value(reply)->append(reply->readAll());
|
|
}
|
|
|
|
void
|
|
SixCycle::readFileCompleted()
|
|
{
|
|
printd("Sixcycle::readFileCompleted\n");
|
|
|
|
QNetworkReply *reply = static_cast<QNetworkReply*>(QObject::sender());
|
|
|
|
printd("reply begins:%s", buffers.value(reply)->toStdString().substr(0,80).c_str());
|
|
|
|
// lets not override the date use an unformatted file name, keeping any .tcx.gz style suffix
|
|
QString name = QString("temp.%1").arg(QFileInfo(replyName(reply)).completeSuffix());
|
|
|
|
// process it then ........
|
|
notifyReadComplete(buffers.value(reply), name, tr("Completed."));
|
|
}
|
|
|
|
static bool addSixCycle() {
|
|
CloudServiceFactory::instance().addService(new SixCycle(NULL));
|
|
return true;
|
|
}
|
|
|
|
static bool add = addSixCycle();
|