Automatic download of ARRL LotW users file at startup if needed

This commit is contained in:
Bill Somerville 2018-10-01 21:19:21 +01:00
parent 62a4569a4c
commit 0032a00ffe
5 changed files with 285 additions and 62 deletions

View File

@ -1428,7 +1428,6 @@ install (FILES
install (FILES
contrib/Ephemeris/JPLEPH
contrib/lotw-user-activity.csv
DESTINATION ${CMAKE_INSTALL_DATADIR}/${CMAKE_PROJECT_NAME}
#COMPONENT runtime
)

View File

@ -8,6 +8,12 @@
#include <QFile>
#include <QTextStream>
#include <QDir>
#include <QFileInfo>
#include <QPointer>
#include <QSaveFile>
#include <QUrl>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QDebug>
#include "Configuration.hpp"
@ -20,16 +26,176 @@ namespace
{
// Dictionary mapping call sign to date of last upload to LotW
using dictionary = QHash<QString, QDate>;
}
class LotWUsers::impl final
: public QObject
{
Q_OBJECT
public:
impl (LotWUsers * self, QNetworkAccessManager * network_manager, QString const& lotw_csv_file)
: self_ {self}
, network_manager_ {network_manager}
, csv_file_ {lotw_csv_file}
, url_valid_ {false}
, redirect_count_ {0}
{
}
void load (bool forced_fetch)
{
auto csv_file_name = csv_file_.fileName ();
abort (); // abort any active download
if (!QFileInfo::exists (csv_file_name) || forced_fetch)
{
current_url_.setUrl ("https://lotw.arrl.org/lotw-user-activity.csv");
redirect_count_ = 0;
download (current_url_);
}
else
{
// load the database asynchronously
future_load_ = std::async (std::launch::async, &LotWUsers::impl::load_dictionary, this, csv_file_name);
}
}
void download (QUrl url)
{
if (QNetworkAccessManager::Accessible != network_manager_->networkAccessible ()) {
// try and recover network access for QNAM
network_manager_->setNetworkAccessible (QNetworkAccessManager::Accessible);
}
if (url.isValid () && !QSslSocket::supportsSsl ())
{
url.setScheme ("http");
}
QNetworkRequest request {url};
request.setRawHeader ("User-Agent", "WSJT LotW User Downloader");
request.setOriginatingObject (this);
// this blocks for a second or two the first time it is used on
// Windows - annoying
if (!url_valid_)
{
reply_ = network_manager_->head (request);
}
else
{
reply_ = network_manager_->get (request);
}
connect (reply_.data (), &QNetworkReply::finished, this, &LotWUsers::impl::reply_finished);
connect (reply_.data (), &QNetworkReply::readyRead, this, &LotWUsers::impl::store);
}
void reply_finished ()
{
if (!reply_) return; // we probably deleted it in an
// earlier call
QUrl redirect_url {reply_->attribute (QNetworkRequest::RedirectionTargetAttribute).toUrl ()};
if (reply_->error () == QNetworkReply::NoError && !redirect_url.isEmpty ())
{
if (++redirect_count_ < 10) // maintain sanity
{
// follow redirect
download (reply_->url ().resolved (redirect_url));
}
else
{
Q_EMIT self_->LotW_users_error (tr ("Network Error - Too many redirects:\n\'%1\'")
.arg (redirect_url.toDisplayString ()));
url_valid_ = false; // reset
}
}
else if (reply_->error () != QNetworkReply::NoError)
{
csv_file_.cancelWriting ();
csv_file_.commit ();
url_valid_ = false; // reset
// report errors that are not due to abort
if (QNetworkReply::OperationCanceledError != reply_->error ())
{
Q_EMIT self_->LotW_users_error (tr ("Network Error:\n%1")
.arg (reply_->errorString ()));
}
}
else
{
if (url_valid_ && !csv_file_.commit ())
{
Q_EMIT self_->LotW_users_error (tr ("File System Error - Cannot commit changes to:\n\"%1\"")
.arg (csv_file_.fileName ()));
url_valid_ = false; // reset
}
else
{
if (!url_valid_)
{
// now get the body content
url_valid_ = true;
download (reply_->url ().resolved (redirect_url));
}
else
{
url_valid_ = false; // reset
qDebug () << "LotW Users Data downloaded from" << reply_->url ().toDisplayString ();
// load the database asynchronously
future_load_ = std::async (std::launch::async, &LotWUsers::impl::load_dictionary, this, csv_file_.fileName ());
}
}
}
if (reply_ && reply_->isFinished ())
{
reply_->deleteLater ();
}
}
void store ()
{
if (url_valid_)
{
if (!csv_file_.isOpen ())
{
// create temporary file in the final location
if (!csv_file_.open (QSaveFile::WriteOnly))
{
abort ();
Q_EMIT self_->LotW_users_error (tr ("File System Error - Cannot open file:\n\"%1\"\nError(%2): %3")
.arg (csv_file_.fileName ())
.arg (csv_file_.error ())
.arg (csv_file_.errorString ()));
}
}
if (csv_file_.write (reply_->read (reply_->bytesAvailable ())) < 0)
{
abort ();
Q_EMIT self_->LotW_users_error (tr ("File System Error - Cannot write to file:\n\"%1\"\nError(%2): %3")
.arg (csv_file_.fileName ())
.arg (csv_file_.error ())
.arg (csv_file_.errorString ()));
}
}
}
void abort ()
{
if (reply_ && reply_->isRunning ())
{
reply_->abort ();
}
}
// Load the database from the given file name
//
// Expects the file to be in CSV format with no header with one
// record per line. Record fields are call sign followed by upload
// date in yyyy-MM-dd format followed by upload time (ignored)
dictionary load (QString const& lotw_users_file)
dictionary load_dictionary (QString const& lotw_csv_file)
{
dictionary result;
QFile f {lotw_users_file};
QFile f {lotw_csv_file};
if (f.open (QFile::ReadOnly | QFile::Text))
{
QTextStream s {&f};
@ -46,26 +212,39 @@ namespace
}
return result;
}
}
class LotWUsers::impl final
{
public:
LotWUsers * self_;
QNetworkAccessManager * network_manager_;
QSaveFile csv_file_;
bool url_valid_;
QUrl current_url_; // may be a redirect
int redirect_count_;
QPointer<QNetworkReply> reply_;
std::future<dictionary> future_load_;
dictionary last_uploaded_;
};
LotWUsers::LotWUsers (Configuration const * configuration, QObject * parent)
#include "LotWUsers.moc"
LotWUsers::LotWUsers (Configuration const * configuration, QNetworkAccessManager * network_manager
, QObject * parent)
: QObject {parent}
, m_ {this
, network_manager
, configuration->writeable_data_dir ().absoluteFilePath ("lotw-user-activity.csv")}
{
// load the database asynchronously
m_->future_load_ = std::async (std::launch::async, load, configuration->writeable_data_dir ().absoluteFilePath ("lotw-user-activity.csv"));
m_->load (false);
}
LotWUsers::~LotWUsers ()
{
}
void LotWUsers::download_new_file ()
{
m_->load (true);
}
bool LotWUsers::user (QString const& call, qint64 uploaded_since_days) const
{
if (m_->future_load_.valid ())

View File

@ -8,6 +8,7 @@
class QString;
class QDate;
class Configuration;
class QNetworkAccessManager;
//
// LotWUsers - Lookup Logbook of the World users
@ -18,9 +19,11 @@ class LotWUsers final
Q_OBJECT
public:
LotWUsers (Configuration const * configuration, QObject * parent = 0);
LotWUsers (Configuration const * configuration, QNetworkAccessManager *, QObject * parent = 0);
~LotWUsers ();
void download_new_file ();
// returns true if the specified call sign 'call' has uploaded their
// log to LotW in the last 'uploaded_since_days' days
Q_SLOT bool user (QString const& call, qint64 uploaded_since_days) const;

View File

@ -34,12 +34,15 @@ void RemoteFile::local_file_path (QString const& name)
QFile file {local_file_.absoluteFilePath ()};
if (!file.rename (new_file.absoluteFilePath ()))
{
listener_->error (tr ("File System Error")
, tr ("Cannot rename file:\n\"%1\"\nto: \"%2\"\nError(%3): %4")
.arg (file.fileName ())
.arg (new_file.absoluteFilePath ())
.arg (file.error ())
.arg (file.errorString ()));
if (listener_)
{
listener_->error (tr ("File System Error")
, tr ("Cannot rename file:\n\"%1\"\nto: \"%2\"\nError(%3): %4")
.arg (file.fileName ())
.arg (new_file.absoluteFilePath ())
.arg (file.error ())
.arg (file.errorString ()));
}
}
}
std::swap (local_file_, new_file);
@ -49,15 +52,18 @@ void RemoteFile::local_file_path (QString const& name)
bool RemoteFile::local () const
{
auto is_local = (reply_ && !reply_->isFinished ()) || local_file_.exists ();
if (is_local)
if (listener_)
{
auto size = local_file_.size ();
listener_->download_progress (size, size);
listener_->download_finished (true);
}
else
{
listener_->download_progress (-1, 0);
if (is_local)
{
auto size = local_file_.size ();
listener_->download_progress (size, size);
listener_->download_finished (true);
}
else
{
listener_->download_progress (-1, 0);
}
}
return is_local;
}
@ -92,13 +98,19 @@ bool RemoteFile::sync (QUrl const& url, bool local, bool force)
auto path = local_file_.absoluteDir ();
if (path.remove (local_file_.fileName ()))
{
listener_->download_progress (-1, 0);
if (listener_)
{
listener_->download_progress (-1, 0);
}
}
else
{
listener_->error (tr ("File System Error")
, tr ("Cannot delete file:\n\"%1\"")
.arg (local_file_.absoluteFilePath ()));
if (listener_)
{
listener_->error (tr ("File System Error")
, tr ("Cannot delete file:\n\"%1\"")
.arg (local_file_.absoluteFilePath ()));
}
return false;
}
path.rmpath (".");
@ -137,10 +149,13 @@ void RemoteFile::download (QUrl url)
connect (reply_.data (), &QNetworkReply::readyRead, this, &RemoteFile::store);
connect (reply_.data (), &QNetworkReply::downloadProgress
, [this] (qint64 bytes_received, qint64 total_bytes) {
// report progress of wanted file
if (is_valid_)
if (listener_)
{
listener_->download_progress (bytes_received, total_bytes);
// report progress of wanted file
if (is_valid_)
{
listener_->download_progress (bytes_received, total_bytes);
}
}
});
}
@ -160,7 +175,7 @@ void RemoteFile::reply_finished ()
QUrl redirect_url {reply_->attribute (QNetworkRequest::RedirectionTargetAttribute).toUrl ()};
if (reply_->error () == QNetworkReply::NoError && !redirect_url.isEmpty ())
{
if (listener_->redirect_request (redirect_url))
if (!listener_ || listener_->redirect_request (redirect_url))
{
if (++redirect_count_ < 10) // maintain sanity
{
@ -169,19 +184,25 @@ void RemoteFile::reply_finished ()
}
else
{
listener_->download_finished (false);
listener_->error (tr ("Network Error")
, tr ("Too many redirects: %1")
.arg (redirect_url.toDisplayString ()));
if (listener_)
{
listener_->download_finished (false);
listener_->error (tr ("Network Error")
, tr ("Too many redirects: %1")
.arg (redirect_url.toDisplayString ()));
}
is_valid_ = false; // reset
}
}
else
{
listener_->download_finished (false);
listener_->error (tr ("Network Error")
, tr ("Redirect not followed: %1")
.arg (redirect_url.toDisplayString ()));
if (listener_)
{
listener_->download_finished (false);
listener_->error (tr ("Network Error")
, tr ("Redirect not followed: %1")
.arg (redirect_url.toDisplayString ()));
}
is_valid_ = false; // reset
}
}
@ -189,10 +210,13 @@ void RemoteFile::reply_finished ()
{
file_.cancelWriting ();
file_.commit ();
listener_->download_finished (false);
if (listener_)
{
listener_->download_finished (false);
}
is_valid_ = false; // reset
// report errors that are not due to abort
if (QNetworkReply::OperationCanceledError != reply_->error ())
if (listener_ && QNetworkReply::OperationCanceledError != reply_->error ())
{
listener_->error (tr ("Network Error"), reply_->errorString ());
}
@ -202,11 +226,17 @@ void RemoteFile::reply_finished ()
auto path = QFileInfo {file_.fileName ()}.absoluteDir ();
if (is_valid_ && !file_.commit ())
{
listener_->error (tr ("File System Error")
, tr ("Cannot commit changes to:\n\"%1\"")
.arg (file_.fileName ()));
if (listener_)
{
listener_->error (tr ("File System Error")
, tr ("Cannot commit changes to:\n\"%1\"")
.arg (file_.fileName ()));
}
path.rmpath ("."); // tidy empty directories
listener_->download_finished (false);
if (listener_)
{
listener_->download_finished (false);
}
is_valid_ = false; // reset
}
else
@ -219,7 +249,10 @@ void RemoteFile::reply_finished ()
}
else
{
listener_->download_finished (true);
if (listener_)
{
listener_->download_finished (true);
}
is_valid_ = false; // reset
}
}
@ -243,29 +276,38 @@ void RemoteFile::store ()
if (!file_.open (QSaveFile::WriteOnly))
{
abort ();
listener_->error (tr ("File System Error")
, tr ("Cannot open file:\n\"%1\"\nError(%2): %3")
.arg (path.path ())
.arg (file_.error ())
.arg (file_.errorString ()));
if (listener_)
{
listener_->error (tr ("File System Error")
, tr ("Cannot open file:\n\"%1\"\nError(%2): %3")
.arg (path.path ())
.arg (file_.error ())
.arg (file_.errorString ()));
}
}
}
else
{
abort ();
listener_->error (tr ("File System Error")
, tr ("Cannot make path:\n\"%1\"")
.arg (path.path ()));
if (listener_)
{
listener_->error (tr ("File System Error")
, tr ("Cannot make path:\n\"%1\"")
.arg (path.path ()));
}
}
}
if (file_.write (reply_->read (reply_->bytesAvailable ())) < 0)
{
abort ();
listener_->error (tr ("File System Error")
, tr ("Cannot write to file:\n\"%1\"\nError(%2): %3")
.arg (file_.fileName ())
.arg (file_.error ())
.arg (file_.errorString ()));
if (listener_)
{
listener_->error (tr ("File System Error")
, tr ("Cannot write to file:\n\"%1\"\nError(%2): %3")
.arg (file_.fileName ())
.arg (file_.error ())
.arg (file_.errorString ()));
}
}
}
}

View File

@ -203,7 +203,7 @@ MainWindow::MainWindow(QDir const& temp_directory, bool multiple,
m_settings {multi_settings->settings ()},
ui(new Ui::MainWindow),
m_config {temp_directory, m_settings, this},
m_lotw_users {&m_config},
m_lotw_users {&m_config, &m_network_manager},
m_WSPR_band_hopping {m_settings, &m_config, this},
m_WSPR_tx_next {false},
m_rigErrorMessageBox {MessageBox::Critical, tr ("Rig Control Error")