Initial Commit
This commit is contained in:
@@ -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"));
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 ();
|
||||
}
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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 ()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user