Initial Commit

This commit is contained in:
Jordan Sherer
2018-02-08 21:28:33 -05:00
commit 678c1d3966
14352 changed files with 3176737 additions and 0 deletions
+319
View File
@@ -0,0 +1,319 @@
#include "Directory.hpp"
#include <QVariant>
#include <QString>
#include <QHeaderView>
#include <QStringList>
#include <QFileInfo>
#include <QDir>
#include <QNetworkAccessManager>
#include <QAuthenticator>
#include <QNetworkReply>
#include <QTreeWidgetItem>
#include <QTreeWidgetItemIterator>
#include <QJsonDocument>
#include <QJsonParseError>
#include <QJsonArray>
#include <QJsonObject>
#include <QRegularExpression>
#include "Configuration.hpp"
#include "DirectoryNode.hpp"
#include "FileNode.hpp"
#include "revision_utils.hpp"
#include "MessageBox.hpp"
#include "moc_Directory.cpp"
namespace
{
char const * samples_dir_name = "samples";
QString const contents_file_name = "contents_" + version (false) + ".json";
}
Directory::Directory (Configuration const * configuration
, QNetworkAccessManager * network_manager
, QWidget * parent)
: QTreeWidget {parent}
, configuration_ {configuration}
, network_manager_ {network_manager}
, http_only_ {false}
, root_dir_ {configuration_->save_directory ()}
, contents_ {this
, network_manager_
, QDir {root_dir_.absoluteFilePath (samples_dir_name)}.absoluteFilePath (contents_file_name)}
{
dir_icon_.addPixmap (style ()->standardPixmap (QStyle::SP_DirClosedIcon), QIcon::Normal, QIcon::Off);
dir_icon_.addPixmap (style ()->standardPixmap (QStyle::SP_DirOpenIcon), QIcon::Normal, QIcon::On);
file_icon_.addPixmap (style ()->standardPixmap (QStyle::SP_FileIcon));
setColumnCount (2);
setHeaderLabels ({"File", "Progress"});
header ()->setSectionResizeMode (QHeaderView::ResizeToContents);
setItemDelegate (&item_delegate_);
connect (network_manager_, &QNetworkAccessManager::authenticationRequired
, this, &Directory::authentication);
connect (this, &Directory::itemChanged, [this] (QTreeWidgetItem * item) {
switch (item->type ())
{
case FileNode::Type:
{
FileNode * node = static_cast<FileNode *> (item);
if (!node->sync (node->checkState (0) == Qt::Checked))
{
FileNode::sync_blocker b {node};
node->setCheckState (0, node->checkState (0) == Qt::Checked ? Qt::Unchecked : Qt::Checked);
}
}
break;
default:
break;
}
});
}
bool Directory::url_root (QUrl root)
{
if (!root.path ().endsWith ('/'))
{
root.setPath (root.path () + '/');
}
bool valid = root.isValid ();
if (valid)
{
url_root_ = root;
}
return valid;
}
void Directory::error (QString const& title, QString const& message)
{
MessageBox::warning_message (this, title, message);
}
bool Directory::refresh (bool http_only)
{
abort ();
clear ();
// update locations
root_dir_ = configuration_->save_directory ();
QDir contents_dir {root_dir_.absoluteFilePath (samples_dir_name)};
contents_.local_file_path (contents_dir.absoluteFilePath (contents_file_name));
contents_.http_only (http_only_ = http_only);
QUrl url {url_root_.resolved (QDir {root_dir_.relativeFilePath (samples_dir_name)}.filePath (contents_file_name))};
if (url.isValid ())
{
return contents_.sync (url, true, true); // attempt to fetch contents
}
else
{
MessageBox::warning_message (this
, tr ("URL Error")
, tr ("Invalid URL:\n\"%1\"")
.arg (url.toDisplayString ()));
}
return false;
}
void Directory::download_finished (bool success)
{
if (success)
{
QFile contents {contents_.local_file_path ()};
if (contents.open (QFile::ReadOnly | QFile::Text))
{
QJsonParseError json_status;
auto content = QJsonDocument::fromJson (contents.readAll (), &json_status);
if (json_status.error)
{
MessageBox::warning_message (this
, tr ("JSON Error")
, tr ("Contents file syntax error %1 at character offset %2")
.arg (json_status.errorString ()).arg (json_status.offset));
return;
}
if (!content.isArray ())
{
MessageBox::warning_message (this, tr ("JSON Error")
, tr ("Contents file top level must be a JSON array"));
return;
}
QTreeWidgetItem * parent {invisibleRootItem ()};
parent = new DirectoryNode {parent, samples_dir_name};
parent->setIcon (0, dir_icon_);
parent->setExpanded (true);
parse_entries (content.array (), root_dir_.relativeFilePath (samples_dir_name), parent);
}
else
{
MessageBox::warning_message (this, tr ("File System Error")
, tr ("Failed to open \"%1\"\nError: %2 - %3")
.arg (contents.fileName ())
.arg (contents.error ())
.arg (contents.errorString ()));
}
}
}
void Directory::parse_entries (QJsonArray const& entries, QDir const& dir, QTreeWidgetItem * parent)
{
if (dir.isRelative () && !dir.path ().startsWith ('.'))
{
for (auto const& value: entries)
{
if (value.isObject ())
{
auto const& entry = value.toObject ();
auto const& name = entry["name"].toString ();
if (name.size () && !name.contains (QRegularExpression {R"([/:;])"}))
{
auto const& type = entry["type"].toString ();
if ("file" == type)
{
QUrl url {url_root_.resolved (dir.filePath (name))};
if (url.isValid ())
{
auto node = new FileNode {parent, network_manager_
, QDir {root_dir_.filePath (dir.path ())}.absoluteFilePath (name)
, url, http_only_};
FileNode::sync_blocker b {node};
node->setIcon (0, file_icon_);
node->setCheckState (0, node->local () ? Qt::Checked : Qt::Unchecked);
update (parent);
}
else
{
MessageBox::warning_message (this
, tr ("URL Error")
, tr ("Invalid URL:\n\"%1\"")
.arg (url.toDisplayString ()));
}
}
else if ("directory" == type)
{
auto node = new DirectoryNode {parent, name};
node->setIcon (0, dir_icon_);
auto const& entries = entry["entries"];
if (entries.isArray ())
{
parse_entries (entries.toArray ()
, QDir {root_dir_.relativeFilePath (dir.path ())}.filePath (name)
, node);
}
else
{
MessageBox::warning_message (this, tr ("JSON Error")
, tr ("Contents entries must be a JSON array"));
}
}
else
{
MessageBox::warning_message (this, tr ("JSON Error")
, tr ("Contents entries must have a valid type"));
}
}
else
{
MessageBox::warning_message (this, tr ("JSON Error")
, tr ("Contents entries must have a valid name"));
}
}
else
{
MessageBox::warning_message (this, tr ("JSON Error")
, tr ("Contents entries must be JSON objects"));
}
}
}
else
{
MessageBox::warning_message (this, tr ("JSON Error")
, tr ("Contents directories must be relative and within \"%1\"")
.arg (samples_dir_name));
}
}
void Directory::abort ()
{
QTreeWidgetItemIterator iter {this};
while (*iter)
{
if ((*iter)->type () == FileNode::Type)
{
auto * node = static_cast<FileNode *> (*iter);
node->abort ();
}
++iter;
}
}
namespace
{
//
// traverse the passed subtree accumulating the number of items, the
// number we have size data for, the bytes downloaded so far and the
// maximum bytes to expect
//
int recurse_children (QTreeWidgetItem const * item, int * counted
, qint64 * bytes, qint64 * max)
{
int items {0};
for (int index {0}; index < item->childCount (); ++index)
{
auto const * child = item->child (index);
if (child->type () == FileNode::Type) // only count files
{
++items;
if (auto size = child->data (1, Qt::UserRole).toLongLong ())
{
*max += size;
++*counted;
}
*bytes += child->data (1, Qt::DisplayRole).toLongLong ();
}
else
{
// recurse into sub-directory subtrees
items += recurse_children (child, counted, bytes, max);
}
}
return items;
}
}
void Directory::update (QTreeWidgetItem * item)
{
// iterate the tree under item and accumulate the progress
if (item)
{
Q_ASSERT (item->type () == DirectoryNode::Type);
qint64 max {0};
qint64 bytes {0};
int counted {0};
// get the count, progress and size of children
int items {recurse_children (item, &counted, &bytes, &max)};
// estimate size of items not yet downloaded as average of
// those actually present
if (counted)
{
max += (items - counted) * max / counted;
}
// save as our progress
item->setData (1, Qt::UserRole, max);
item->setData (1, Qt::DisplayRole, bytes);
// recurse up to top
update (item->parent ());
}
}
void Directory::authentication (QNetworkReply * /* reply */
, QAuthenticator * /* authenticator */)
{
MessageBox::warning_message (this, tr ("Network Error"), tr ("Authentication required"));
}
+60
View File
@@ -0,0 +1,60 @@
#ifndef SAMPLE_DOWNLOADER_DIRECTORY_HPP__
#define SAMPLE_DOWNLOADER_DIRECTORY_HPP__
#include <QObject>
#include <QString>
#include <QTreeWidget>
#include <QIcon>
#include <QSize>
#include <QDir>
#include <QUrl>
#include "DirectoryDelegate.hpp"
#include "RemoteFile.hpp"
class Configuration;
class QNetworkAccessManager;
class QTreeWidgetItem;
class QNetworkReply;
class QAuthenticator;
class QJsonArray;
class Directory final
: public QTreeWidget
, protected RemoteFile::ListenerInterface
{
Q_OBJECT
public:
explicit Directory (Configuration const * configuration
, QNetworkAccessManager * network_manager
, QWidget * parent = nullptr);
QSize sizeHint () const override {return {400, 500};}
bool url_root (QUrl);
bool refresh (bool http_only);
void abort ();
void update (QTreeWidgetItem * item);
protected:
void error (QString const& title, QString const& message) override;
bool redirect_request (QUrl const&) override {return true;} // allow
void download_finished (bool success) override;
private:
Q_SLOT void authentication (QNetworkReply *, QAuthenticator *);
void parse_entries (QJsonArray const& entries, QDir const& dir, QTreeWidgetItem * parent);
Configuration const * configuration_;
QNetworkAccessManager * network_manager_;
bool http_only_;
QDir root_dir_;
QUrl url_root_;
RemoteFile contents_;
DirectoryDelegate item_delegate_;
QIcon dir_icon_;
QIcon file_icon_;
};
#endif
+44
View File
@@ -0,0 +1,44 @@
#include "DirectoryDelegate.hpp"
#include <QApplication>
#include <QVariant>
#include <QString>
#include <QStyle>
#include <QModelIndex>
#include <QPainter>
#include <QStyleOptionViewItem>
#include <QStyleOptionProgressBar>
void DirectoryDelegate::paint (QPainter * painter, QStyleOptionViewItem const& option
, QModelIndex const& index) const
{
if (1 == index.column ())
{
QStyleOptionProgressBar progress_bar_option;
progress_bar_option.rect = option.rect;
progress_bar_option.state = QStyle::State_Enabled;
progress_bar_option.direction = QApplication::layoutDirection ();
progress_bar_option.fontMetrics = QApplication::fontMetrics ();
progress_bar_option.minimum = 0;
progress_bar_option.maximum = 100;
auto progress = index.data ().toLongLong ();
if (progress > 0)
{
auto percent = int (progress * 100 / index.data (Qt::UserRole).toLongLong ());
progress_bar_option.progress = percent;
progress_bar_option.text = QString::number (percent) + '%';
progress_bar_option.textVisible = true;
progress_bar_option.textAlignment = Qt::AlignCenter;
}
else
{
// not started
progress_bar_option.progress = -1;
}
QApplication::style ()->drawControl (QStyle::CE_ProgressBar, &progress_bar_option, painter);
}
else
{
QStyledItemDelegate::paint (painter, option, index);
}
}
+30
View File
@@ -0,0 +1,30 @@
#ifndef DIRECTORY_DELEGATE_HPP__
#define DIRECTORY_DELEGATE_HPP__
#include <QStyledItemDelegate>
class QObject;
class QStyleOptionVoew;
class QModelIndex;
class QPainter;
//
// Styled item delegate that renders a progress bar in column #1
//
// model column #1 DisplayRole is the progress in bytes
// model column #1 UserRole is the expected number of bytes
//
class DirectoryDelegate final
: public QStyledItemDelegate
{
public:
explicit DirectoryDelegate (QObject * parent = nullptr)
: QStyledItemDelegate {parent}
{
}
void paint (QPainter * painter, QStyleOptionViewItem const& option
, QModelIndex const& index) const override;
};
#endif
+51
View File
@@ -0,0 +1,51 @@
#ifndef DIRECTORY_NODE_HPP__
#define DIRECTORY_NODE_HPP__
#include <QTreeWidgetItem>
#include <QString>
//
// Tree widget item representing a file system directory.
//
// It renders the directory name in the first column and progress
// information in the 2nd column. The progress information consists of
// two 64 bit integer values, the 1st in the DisplayRole is the number
// of bytes received and the 2nd in the UserRole the total bytes
// expected. The progress information is not automatically
// maintained, see the Directory class for an example of how to
// dynamically maintain the DirectoryNode progress values. The 1st
// column also renders a tristate check box that controls the first
// column check boxes of child items.
//
class DirectoryNode final
: public QTreeWidgetItem
{
public:
explicit DirectoryNode (QTreeWidgetItem * parent, QString const& name)
: QTreeWidgetItem {parent, Type}
{
setFlags (flags () | Qt::ItemIsUserCheckable | Qt::ItemIsTristate);
setText (0, name);
setCheckState (0, Qt::Unchecked);
// initialize as empty, the owning QTreeWidget must maintain these
// progress values
setData (1, Qt::DisplayRole, 0ll); // progress in bytes
setData (1, Qt::UserRole, 0ll); // expected bytes
}
bool operator == (QString const& name) const
{
return name == text (0);
}
static int constexpr Type {UserType};
};
inline
bool operator == (QString const& lhs, DirectoryNode const& rhs)
{
return rhs == lhs;
}
#endif
+74
View File
@@ -0,0 +1,74 @@
#include "FileNode.hpp"
#include <QVariant>
#include <QUrl>
#include <QDir>
#include <QFileInfo>
#include "Directory.hpp"
#include "MessageBox.hpp"
FileNode::FileNode (QTreeWidgetItem * parent
, QNetworkAccessManager * network_manager
, QString const& local_file_path
, QUrl const& url
, bool http_only)
: QTreeWidgetItem {parent, Type}
, remote_file_ {this, network_manager, local_file_path, http_only}
, block_sync_ {false}
{
sync_blocker b {this};
setFlags (flags () | Qt::ItemIsUserCheckable);
setText (0, QFileInfo {local_file_path}.fileName ()); // display
setData (0, Qt::UserRole, url);
setData (0, Qt::UserRole + 1, local_file_path); // local absolute path
setCheckState (0, Qt::Unchecked);
}
void FileNode::error (QString const& title, QString const& message)
{
MessageBox::warning_message (treeWidget (), title, message);
}
bool FileNode::sync (bool local)
{
if (block_sync_)
{
return true;
}
return remote_file_.sync (data (0, Qt::UserRole).toUrl (), local);
}
void FileNode::download_progress (qint64 bytes_received, qint64 total_bytes)
{
sync_blocker b {this};
setData (1, Qt::UserRole, total_bytes);
if (bytes_received < 0)
{
setData (1, Qt::DisplayRole, 0ll);
setCheckState (0, Qt::Unchecked);
}
else
{
setData (1, Qt::DisplayRole, bytes_received);
}
static_cast<Directory *> (treeWidget ())->update (parent ());
}
void FileNode::download_finished (bool success)
{
sync_blocker b {this};
if (!success)
{
setData (1, Qt::UserRole, 0ll);
setData (1, Qt::DisplayRole, 0ll);
}
setCheckState (0, success ? Qt::Checked : Qt::Unchecked);
static_cast<Directory *> (treeWidget ())->update (parent ());
}
void FileNode::abort ()
{
sync_blocker b {this};
remote_file_.abort ();
}
+67
View File
@@ -0,0 +1,67 @@
#ifndef FILE_NODE_HPP__
#define FILE_NODE_HPP__
#include <QTreeWidgetItem>
#include "RemoteFile.hpp"
class QNetworkAccessManager;
class QString;
class QUrl;
//
// A holder for a RemoteFile object linked to a QTreeWidget row.
//
// It renders the file name in first column and holds download
// progress data in the second column. The progress information is a
// 64 bit integer number of bytes in the DisplayRole and a total bytes
// expected in the UserRole. The first column also renders a check box
// that downloads the file when checked and removes the downloaded
// file when unchecked. The URL and local absolute file path are
// stored in the UserData and UserData+1 roles of the first column.
//
class FileNode final
: public QTreeWidgetItem
, protected RemoteFile::ListenerInterface
{
public:
explicit FileNode (QTreeWidgetItem * parent
, QNetworkAccessManager * network_manager
, QString const& local_path
, QUrl const& url
, bool http_only);
bool local () const {return remote_file_.local ();}
bool sync (bool local);
void abort ();
static int constexpr Type {UserType + 1};
//
// Clients may use this RAII class to block nested calls to sync
// which may be troublesome, e.g. when UI updates cause recursion.
//
struct sync_blocker
{
sync_blocker (FileNode * node) : node_ {node} {node_->block_sync_ = true;}
sync_blocker (sync_blocker const&) = delete;
sync_blocker& operator = (sync_blocker const&) = delete;
~sync_blocker () {node_->block_sync_ = false;}
private:
FileNode * node_;
};
protected:
void error (QString const& title, QString const& message) override;
bool redirect_request (QUrl const&) override {return true;} // allow
void download_progress (qint64 bytes_received, qint64 total_bytes) override;
void download_finished (bool success) override;
private:
RemoteFile remote_file_; // active download
bool block_sync_;
friend struct sync_blocker;
};
#endif
+5
View File
@@ -0,0 +1,5 @@
A UI for downloading sample files from a web server.
Works in concert with samples/CMakeLists.txt which generates the JSON
contents description file and has a build target upload-samples that
uploads the samples and content file to the project files server.
+271
View File
@@ -0,0 +1,271 @@
#include "RemoteFile.hpp"
#include <utility>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QDir>
#include <QByteArray>
#include "moc_RemoteFile.cpp"
RemoteFile::RemoteFile (ListenerInterface * listener, QNetworkAccessManager * network_manager
, QString const& local_file_path, bool http_only, QObject * parent)
: QObject {parent}
, listener_ {listener}
, network_manager_ {network_manager}
, local_file_ {local_file_path}
, http_only_ {http_only}
, is_valid_ {false}
, redirect_count_ {0}
, file_ {local_file_path}
{
local_file_.setCaching (false);
}
void RemoteFile::local_file_path (QString const& name)
{
QFileInfo new_file {name};
new_file.setCaching (false);
if (new_file != local_file_)
{
if (local_file_.exists ())
{
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 ()));
}
}
std::swap (local_file_, new_file);
}
}
bool RemoteFile::local () const
{
auto is_local = (reply_ && !reply_->isFinished ()) || local_file_.exists ();
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;
}
bool RemoteFile::sync (QUrl const& url, bool local, bool force)
{
if (local)
{
if (!reply_ || reply_->isFinished ()) // not active download
{
if (force || !local_file_.exists () || url != url_)
{
url_ = url;
redirect_count_ = 0;
Q_ASSERT (!is_valid_);
download (url_);
}
}
else
{
return false;
}
}
else
{
if (reply_ && reply_->isRunning ())
{
reply_->abort ();
}
if (local_file_.exists ())
{
auto path = local_file_.absoluteDir ();
if (path.remove (local_file_.fileName ()))
{
listener_->download_progress (-1, 0);
}
else
{
listener_->error (tr ("File System Error")
, tr ("Cannot delete file:\n\"%1\"")
.arg (local_file_.absoluteFilePath ()));
return false;
}
path.rmpath (".");
}
}
return true;
}
void RemoteFile::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 () || http_only_))
{
url.setScheme ("http");
}
QNetworkRequest request {url};
request.setRawHeader ("User-Agent", "WSJT Sample Downloader");
request.setOriginatingObject (this);
// this blocks for a second or two the first time it is used on
// Windows - annoying
if (!is_valid_)
{
reply_ = network_manager_->head (request);
}
else
{
reply_ = network_manager_->get (request);
}
connect (reply_.data (), &QNetworkReply::finished, this, &RemoteFile::reply_finished);
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_)
{
listener_->download_progress (bytes_received, total_bytes);
}
});
}
void RemoteFile::abort ()
{
if (reply_ && reply_->isRunning ())
{
reply_->abort ();
}
}
void RemoteFile::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 (listener_->redirect_request (redirect_url))
{
if (++redirect_count_ < 10) // maintain sanity
{
// follow redirect
download (reply_->url ().resolved (redirect_url));
}
else
{
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 ()));
is_valid_ = false; // reset
}
}
else if (reply_->error () != QNetworkReply::NoError)
{
file_.cancelWriting ();
file_.commit ();
listener_->download_finished (false);
is_valid_ = false; // reset
// report errors that are not due to abort
if (QNetworkReply::OperationCanceledError != reply_->error ())
{
listener_->error (tr ("Network Error"), reply_->errorString ());
}
}
else
{
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 ()));
path.rmpath ("."); // tidy empty directories
listener_->download_finished (false);
is_valid_ = false; // reset
}
else
{
if (!is_valid_)
{
// now get the body content
is_valid_ = true;
download (reply_->url ().resolved (redirect_url));
}
else
{
listener_->download_finished (true);
is_valid_ = false; // reset
}
}
}
if (reply_ && reply_->isFinished ())
{
reply_->deleteLater ();
}
}
void RemoteFile::store ()
{
if (is_valid_)
{
if (!file_.isOpen ())
{
// create temporary file in the final location
auto path = QFileInfo {file_.fileName ()}.absoluteDir ();
if (path.mkpath ("."))
{
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 ()));
}
}
else
{
abort ();
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 ()));
}
}
}
+84
View File
@@ -0,0 +1,84 @@
#ifndef REMOTE_FILE_HPP__
#define REMOTE_FILE_HPP__
#include <QObject>
#include <QString>
#include <QUrl>
#include <QFileInfo>
#include <QSaveFile>
#include <QPointer>
class QNetworkAccessManager;
class QNetworkReply;
//
// Synchronize an individual file specified by a URL to the local file
// system
//
class RemoteFile final
: public QObject
{
Q_OBJECT
public:
//
// Clients of RemoteFile must provide an instance of this
// interface. It may be used to receive information and requests
// from the RemoteFile instance as it does its work.
//
class ListenerInterface
{
protected:
ListenerInterface () {}
public:
virtual void error (QString const& title, QString const& message) = 0;
virtual bool redirect_request (QUrl const&) {return false;} // disallow
virtual void download_progress (qint64 /* bytes_received */, qint64 /* total_bytes */) {}
virtual void download_finished (bool /* success */) {}
};
explicit RemoteFile (ListenerInterface * listener, QNetworkAccessManager * network_manager
, QString const& local_file_path, bool http_only = false
, QObject * parent = nullptr);
// true if local file exists or will do very soon
bool local () const;
// download/remove the local file
bool sync (QUrl const& url, bool local = true, bool force = false);
// abort an active download
void abort ();
// change the local location, this will rename if the file exists locally
void local_file_path (QString const&);
QString local_file_path () const {return local_file_.absoluteFilePath ();}
QUrl url () const {return url_;}
// always use an http scheme for remote URLs
void http_only (bool flag = true) {http_only_ = flag;}
private:
void download (QUrl url);
void reply_finished ();
Q_SLOT void store ();
Q_SIGNAL void redirect (QUrl const&, unsigned redirect_count);
Q_SIGNAL void downloadProgress (qint64 bytes_received, qint64 total_bytes);
Q_SIGNAL void finished ();
ListenerInterface * listener_;
QNetworkAccessManager * network_manager_;
QFileInfo local_file_;
bool http_only_;
QUrl url_;
QPointer<QNetworkReply> reply_;
bool is_valid_;
unsigned redirect_count_;
QSaveFile file_;
};
#endif