subsurface/core/uploadDiveLogsDE.cpp
Michael Keller 5fae7ce7a0 Equipment: Include Unused Tanks in Merge if Preference is Enabled.
Include unused tanks in merges of multiple logs into a single dive if
the 'Show unused cylinders' preference is enabled.
Also rename the preference (in code) to `include_unused_tanks` to
reflect the fact that it is already used in more places than just the
display (exporting, cloning dives).
Simplified the cylinder model to make forced inclusion of unused tanks
dependent on use of the model in planner.
Leaving the persisted name of the preference as `display_unused_tanks`
to avoid resetting this for all users - is there a good way to migrate
preference names?

Signed-off-by: Michael Keller <github@ike.ch>
2023-07-25 11:19:03 +12:00

360 lines
9.6 KiB
C++

// SPDX-License-Identifier: GPL-2.0
#include "uploadDiveLogsDE.h"
#include <QDir>
#include <QDebug>
#include <QTemporaryFile>
#include <zip.h>
#include <errno.h>
#include "core/errorhelper.h"
#include "core/qthelper.h"
#include "core/dive.h"
#include "core/membuffer.h"
#include "core/divesite.h"
#include "core/cloudstorage.h"
#include "core/xmlparams.h"
#ifndef SUBSURFACE_MOBILE
#include "core/selection.h"
#endif // SUBSURFACE_MOBILE
#include "core/settings/qPrefCloudStorage.h"
uploadDiveLogsDE *uploadDiveLogsDE::instance()
{
static uploadDiveLogsDE *self = new uploadDiveLogsDE;
return self;
}
uploadDiveLogsDE::uploadDiveLogsDE():
reply(NULL),
multipart(NULL)
{
timeout.setSingleShot(true);
}
static QString makeTempFileName()
{
QTemporaryFile tmpfile;
tmpfile.setFileTemplate(QDir::tempPath() + "/divelogsde-upload.XXXXXXXX.dld");
tmpfile.open();
return tmpfile.fileName();
}
void uploadDiveLogsDE::doUpload(bool selected, const QString &userid, const QString &password)
{
QString err;
QString filename = makeTempFileName();
// Make zip file, with all dives, in divelogs.de format
if (!prepareDives(filename, selected)) {
emit uploadFinish(false, tr("Cannot prepare dives, none selected?"));
timeout.stop();
return;
}
// And upload it
uploadDives(filename, userid, password);
}
bool uploadDiveLogsDE::prepareDives(const QString &tempfile, bool selected)
{
static const char errPrefix[] = "divelog.de-upload:";
xsltStylesheetPtr xslt = NULL;
struct zip *zip;
emit uploadStatus(tr("building zip file to upload"));
xslt = get_stylesheet("divelogs-export.xslt");
if (!xslt) {
qDebug() << errPrefix << "missing stylesheet";
report_error(tr("Stylesheet to export to divelogs.de is not found").toUtf8());
return false;
}
// Prepare zip file
int error_code;
zip = zip_open(QFile::encodeName(QDir::toNativeSeparators(tempfile)), ZIP_CREATE, &error_code);
if (!zip) {
char buffer[1024];
zip_error_to_str(buffer, sizeof buffer, error_code, errno);
report_error(tr("Failed to create zip file for upload: %s").toUtf8(), buffer);
return false;
}
/* walk the dive list in chronological order */
int i;
struct dive *dive;
for_each_dive (i, dive) {
char filename[PATH_MAX];
int streamsize;
char *membuf;
xmlDoc *transformed;
struct zip_source *s;
struct membufferpp mb;
struct xml_params *params = alloc_xml_params();
/*
* Get the i'th dive in XML format so we can process it.
* We need to save to a file before we can reload it back into memory...
*/
if (selected && !dive->selected) {
free_xml_params(params);
continue;
}
/* add the dive */
struct dive_site *ds = dive->dive_site;
if (ds) {
put_format(&mb, "<divelog><divesites><site uuid='%8x' name='", ds->uuid);
put_quoted(&mb, ds->name, 1, 0);
put_format(&mb, "'");
put_location(&mb, &ds->location, " gps='", "'");
put_format(&mb, ">\n");
if (ds->taxonomy.nr) {
for (int j = 0; j < ds->taxonomy.nr; j++) {
struct taxonomy *t = &ds->taxonomy.category[j];
if (t->category != TC_NONE && t->category == prefs.geocoding.category[j] && t->value) {
put_format(&mb, " <geo cat='%d'", t->category);
put_format(&mb, " origin='%d' value='", t->origin);
put_quoted(&mb, t->value, 1, 0);
put_format(&mb, "'/>\n");
}
}
}
put_format(&mb, "</site>\n</divesites>\n");
}
save_one_dive_to_mb(&mb, dive, false);
if (ds) {
put_format(&mb, "</divelog>\n");
}
/*
* Parse the memory buffer into XML document and
* transform it to divelogs.de format, finally dumping
* the XML into a character buffer.
*/
xmlDoc *doc = xmlReadMemory(mb.buffer, mb.len, "divelog", NULL, XML_PARSE_HUGE | XML_PARSE_RECOVER);
if (!doc) {
qWarning() << errPrefix << "could not parse back into memory the XML file we've just created!";
report_error(tr("internal error").toUtf8());
zip_close(zip);
QFile::remove(tempfile);
xsltFreeStylesheet(xslt);
free_xml_params(params);
return false;
}
xml_params_add_int(params, "allcylinders", prefs.include_unused_tanks);
transformed = xsltApplyStylesheet(xslt, doc, xml_params_get(params));
free_xml_params(params);
if (!transformed) {
qWarning() << errPrefix << "XSLT transform failed for dive: " << i;
report_error(tr("Conversion of dive %1 to divelogs.de format failed").arg(i).toUtf8());
continue;
}
xmlDocDumpMemory(transformed, (xmlChar **)&membuf, &streamsize);
xmlFreeDoc(doc);
xmlFreeDoc(transformed);
/*
* Save the XML document into a zip file.
*/
snprintf(filename, PATH_MAX, "%d.xml", i + 1);
s = zip_source_buffer(zip, membuf, streamsize, 1); // frees membuffer!
if (s) {
int64_t ret = zip_add(zip, filename, s);
if (ret == -1)
qDebug() << errPrefix << "failed to include dive:" << i;
}
}
xsltFreeStylesheet(xslt);
if (zip_close(zip)) {
int ze, se;
#if LIBZIP_VERSION_MAJOR >= 1
zip_error_t *error = zip_get_error(zip);
ze = zip_error_code_zip(error);
se = zip_error_code_system(error);
#else
zip_error_get(zip, &ze, &se);
#endif
report_error(qPrintable(tr("error writing zip file: %s zip error %d system error %d - %s")),
qPrintable(QDir::toNativeSeparators(tempfile)), ze, se, zip_strerror(zip));
return false;
}
return true;
}
void uploadDiveLogsDE::cleanupTempFile()
{
if (tempFile.isOpen())
tempFile.remove();
}
void uploadDiveLogsDE::uploadDives(const QString &filename, const QString &userid, const QString &password)
{
QHttpPart part1, part2, part3;
static QNetworkRequest request;
QString args;
// Check if there is an earlier request open
if (reply != NULL && reply->isOpen()) {
reply->abort();
delete reply;
reply = NULL;
}
if (multipart != NULL) {
delete multipart;
multipart = NULL;
}
multipart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
emit uploadStatus(tr("Uploading dives"));
// prepare header with filename (of all dives) and pointer to file
args = "form-data; name=\"userfile\"; filename=\"" + filename + "\"";
part1.setRawHeader("Content-Disposition", args.toLatin1());
// open new file for reading
cleanupTempFile();
tempFile.setFileName(filename);
if (!tempFile.open(QIODevice::ReadOnly)) {
qDebug() << "ERROR opening zip file: " << filename;
return;
}
part1.setBodyDevice(&tempFile);
multipart->append(part1);
// Add userid
args = "form-data; name=\"user\"";
part2.setRawHeader("Content-Disposition", args.toLatin1());
part2.setBody(qPrefCloudStorage::divelogde_user().toUtf8());
multipart->append(part2);
// Add password
args = "form-data; name=\"pass\"";
part3.setRawHeader("Content-Disposition", args.toLatin1());
part3.setBody(qPrefCloudStorage::divelogde_pass().toUtf8());
multipart->append(part3);
// Prepare network request
request.setUrl(QUrl("https://divelogs.de/DivelogsDirectImport.php"));
request.setRawHeader("Accept", "text/xml, application/xml");
request.setRawHeader("User-Agent", getUserAgent().toUtf8());
// Execute async.
reply = manager()->post(request, multipart);
// connect signals from upload process
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
connect(reply, &QNetworkReply::finished, this, &uploadDiveLogsDE::uploadFinishedSlot);
connect(reply, &QNetworkReply::errorOccurred, this, &uploadDiveLogsDE::uploadErrorSlot);
connect(reply, &QNetworkReply::uploadProgress, this, &uploadDiveLogsDE::updateProgressSlot);
connect(&timeout, &QTimer::timeout, this, &uploadDiveLogsDE::uploadTimeoutSlot);
#else
connect(reply, SIGNAL(finished()), this, SLOT(uploadFinishedSlot()));
connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this,
SLOT(uploadErrorSlot(QNetworkReply::NetworkError)));
connect(reply, SIGNAL(uploadProgress(qint64, qint64)), this,
SLOT(updateProgressSlot(qint64, qint64)));
connect(&timeout, SIGNAL(timeout()), this, SLOT(uploadTimeoutSlot()));
#endif
timeout.start(30000); // 30s
}
void uploadDiveLogsDE::updateProgressSlot(qint64 current, qint64 total)
{
if (!reply)
return;
if (total <= 0 || current <= 0)
return;
// Calculate percentage as 0.x (values between 0.0 and 1.0)
// And signal whoever wants to know
qreal percentage = (float)current / (float)total;
emit uploadProgress(percentage, 1.0);
// reset the timer: 30 seconds after we last got any data
timeout.start();
}
void uploadDiveLogsDE::uploadFinishedSlot()
{
QString err;
if (!reply)
return;
// check what the server sent us: it might contain
// an error condition, such as a failed login
QByteArray xmlData = reply->readAll();
reply->deleteLater();
reply = NULL;
cleanupTempFile();
char *resp = xmlData.data();
if (resp) {
char *parsed = strstr(resp, "<Login>");
if (parsed) {
if (strstr(resp, "<Login>succeeded</Login>")) {
if (strstr(resp, "<FileCopy>failed</FileCopy>")) {
report_error(tr("Upload failed").toUtf8());
return;
}
timeout.stop();
err = tr("Upload successful");
emit uploadFinish(true, err);
return;
}
timeout.stop();
err = tr("Login failed");
report_error(err.toUtf8());
emit uploadFinish(false, err);
return;
}
timeout.stop();
err = tr("Cannot parse response");
report_error(tr("Cannot parse response").toUtf8());
emit uploadFinish(false, err);
}
}
void uploadDiveLogsDE::uploadTimeoutSlot()
{
timeout.stop();
if (reply) {
reply->deleteLater();
reply = NULL;
}
cleanupTempFile();
QString err(tr("divelogs.de not responding"));
report_error(err.toUtf8());
emit uploadFinish(false, err);
}
void uploadDiveLogsDE::uploadErrorSlot(QNetworkReply::NetworkError error)
{
timeout.stop();
if (reply) {
reply->deleteLater();
reply = NULL;
}
cleanupTempFile();
QString err(tr("network error %1").arg(error));
report_error(err.toUtf8());
emit uploadFinish(false, err);
}