Add a sample download dialog and upload sub-system

Samples are  downloaded from a  web server, currently the  SF download
server.   The samples  are  stored in  the  source controlled  samples
directory and the CMake script  there builds a suitable directory tree
for upload to the web  server under samples/web containing the samples
hierarchy and the  generated JSON contents database  file. The samples
CMake script also  defines an 'upload-samples' target  that uses rsync
to efficiently upload  the samples and the  accompanying contents JSON
database file.

Any directory structure under the samples directory may be created, to
add a new sample file simply add  the file to source control and amend
the list of sample files (SAMPLE_FILES) in samples/CMakeLists.txt.

git-svn-id: svn+ssh://svn.code.sf.net/p/wsjt/wsjt/branches/wsjtx@6308 ab8295b8-cf94-4d9e-aec4-7959e3be5d79
This commit is contained in:
Bill Somerville 2015-12-24 11:41:05 +00:00
parent 0ab5a28067
commit 924e20efa8
26 changed files with 1391 additions and 43 deletions

View File

@ -51,9 +51,11 @@ set (PROJECT_NAME "WSJT-X")
set (PROJECT_VENDOR "Joe Taylor, K1JT") set (PROJECT_VENDOR "Joe Taylor, K1JT")
set (PROJECT_CONTACT "Joe Taylor <k1jt@arrl.net>") set (PROJECT_CONTACT "Joe Taylor <k1jt@arrl.net>")
set (PROJECT_COPYRIGHT "Copyright (C) 2001-2015 by Joe Taylor, K1JT") set (PROJECT_COPYRIGHT "Copyright (C) 2001-2015 by Joe Taylor, K1JT")
set (PROJECT_HOMEPAGE "http://www.physics.princeton.edu/pulsar/K1JT/wsjtx.html") set (PROJECT_HOMEPAGE http://www.physics.princeton.edu/pulsar/K1JT/wsjtx.html)
set (PROJECT_MANUAL wsjtx-main-${wsjtx_VERSION}.html) set (PROJECT_MANUAL wsjtx-main-${wsjtx_VERSION}.html)
set (PROJECT_MANUAL_DIRECTORY_URL http://www.physics.princeton.edu/pulsar/K1JT/wsjtx-doc) set (PROJECT_MANUAL_DIRECTORY_URL http://www.physics.princeton.edu/pulsar/K1JT/wsjtx-doc)
set (PROJECT_SAMPLES_URL http://downloads.sourceforge.net/project/wsjt/)
set (PROJECT_SAMPLES_UPLOAD_DEST frs.sourceforge.net:/home/frs/project/wsjt/)
set (PROJECT_SUMMARY_DESCRIPTION "${PROJECT_NAME} - JT9 and JT65 Modes for LF, MF and HF Amateur Radio.") set (PROJECT_SUMMARY_DESCRIPTION "${PROJECT_NAME} - JT9 and JT65 Modes for LF, MF and HF Amateur Radio.")
set (PROJECT_DESCRIPTION "${PROJECT_SUMMARY_DESCRIPTION} set (PROJECT_DESCRIPTION "${PROJECT_SUMMARY_DESCRIPTION}
${PROJECT_NAME} implements JT9, a new mode designed especially for the LF, MF, ${PROJECT_NAME} implements JT9, a new mode designed especially for the LF, MF,
@ -111,7 +113,6 @@ option (WSJT_TRACE_CAT_POLLS "Debugging option that turns on CAT diagnostics dur
option (WSJT_HAMLIB_TRACE "Debugging option that turns on minimal Hamlib internal diagnostics.") option (WSJT_HAMLIB_TRACE "Debugging option that turns on minimal Hamlib internal diagnostics.")
option (WSJT_SOFT_KEYING "Apply a ramp to CW keying envelope to reduce transients." ON) option (WSJT_SOFT_KEYING "Apply a ramp to CW keying envelope to reduce transients." ON)
option (WSJT_SKIP_MANPAGES "Skip *nix manpage generation.") option (WSJT_SKIP_MANPAGES "Skip *nix manpage generation.")
option (WSJT_EMBED_SAMPLES "Embed sample files into WSJT-X resources." ON)
option (WSJT_GENERATE_DOCS "Generate documentation files." ON) option (WSJT_GENERATE_DOCS "Generate documentation files." ON)
CMAKE_DEPENDENT_OPTION (WSJT_HAMLIB_VERBOSE_TRACE "Debugging option that turns on full Hamlib internal diagnostics." OFF WSJT_HAMLIB_TRACE OFF) CMAKE_DEPENDENT_OPTION (WSJT_HAMLIB_VERBOSE_TRACE "Debugging option that turns on full Hamlib internal diagnostics." OFF WSJT_HAMLIB_TRACE OFF)
@ -209,6 +210,11 @@ set (wsjt_qt_CXXSRCS
MessageClient.cpp MessageClient.cpp
LettersSpinBox.cpp LettersSpinBox.cpp
HelpTextWindow.cpp HelpTextWindow.cpp
SampleDownloader.cpp
SampleDownloader/DirectoryDelegate.cpp
SampleDownloader/Directory.cpp
SampleDownloader/FileNode.cpp
SampleDownloader/RemoteFile.cpp
) )
set (jt9_CXXSRCS set (jt9_CXXSRCS
@ -533,11 +539,6 @@ set (PALETTE_FILES
Palettes/ZL1FZ.pal Palettes/ZL1FZ.pal
) )
set (SAMPLE_FILES
samples/130418_1742.wav
samples/130610_2343.wav
)
if (APPLE) if (APPLE)
set (WSJTX_ICON_FILE ${CMAKE_PROJECT_NAME}.icns) set (WSJTX_ICON_FILE ${CMAKE_PROJECT_NAME}.icns)
set (ICONSRCS set (ICONSRCS
@ -618,6 +619,11 @@ endif (APPLE)
# #
find_program(CTAGS ctags) find_program(CTAGS ctags)
find_program(ETAGS etags) find_program(ETAGS etags)
#
# sub-directories
#
add_subdirectory (samples)
if (WSJT_GENERATE_DOCS) if (WSJT_GENERATE_DOCS)
add_subdirectory (doc) add_subdirectory (doc)
endif (WSJT_GENERATE_DOCS) endif (WSJT_GENERATE_DOCS)
@ -855,9 +861,6 @@ endfunction (add_resources resources path)
add_resources (wsjtx_RESOURCES "" ${TOP_LEVEL_RESOURCES}) add_resources (wsjtx_RESOURCES "" ${TOP_LEVEL_RESOURCES})
add_resources (wsjtx_RESOURCES /Palettes ${PALETTE_FILES}) add_resources (wsjtx_RESOURCES /Palettes ${PALETTE_FILES})
if (WSJT_EMBED_SAMPLES)
add_resources (wsjtx_RESOURCES /samples ${SAMPLE_FILES})
endif (WSJT_EMBED_SAMPLES)
configure_file (wsjtx.qrc.in wsjtx.qrc @ONLY) configure_file (wsjtx.qrc.in wsjtx.qrc @ONLY)

151
SampleDownloader.cpp Normal file
View File

@ -0,0 +1,151 @@
#include "SampleDownloader.hpp"
#include <QString>
#include <QSettings>
#include <QtWidgets>
#include "SettingsGroup.hpp"
#include "SampleDownloader/Directory.hpp"
#include "pimpl_impl.hpp"
#include "moc_SampleDownloader.cpp"
namespace
{
char const * const title = "Download Samples";
}
class SampleDownloader::impl final
: public QDialog
{
Q_OBJECT
public:
explicit impl (QSettings * settings, Configuration const *, QNetworkAccessManager *, QWidget * parent);
~impl () {save_window_state ();}
void refresh ()
{
show ();
raise ();
activateWindow ();
directory_.refresh ();
}
protected:
void closeEvent (QCloseEvent * e) override
{
save_window_state ();
QDialog::closeEvent (e);
}
private:
void save_window_state ()
{
SettingsGroup g (settings_, title);
settings_->setValue ("geometry", saveGeometry ());
settings_->setValue ("SamplesURL", url_line_edit_.text ());
}
Q_SLOT void button_clicked (QAbstractButton *);
QSettings * settings_;
Directory directory_;
QGridLayout main_layout_;
QVBoxLayout left_layout_;
QDialogButtonBox button_box_;
QWidget details_widget_;
QFormLayout details_layout_;
QLineEdit url_line_edit_;
};
#include "SampleDownloader.moc"
SampleDownloader::SampleDownloader (QSettings * settings, Configuration const * configuration
, QNetworkAccessManager * network_manager, QWidget * parent)
: m_ {settings, configuration, network_manager, parent}
{
}
SampleDownloader::~SampleDownloader ()
{
}
void SampleDownloader::show ()
{
m_->refresh ();
}
SampleDownloader::impl::impl (QSettings * settings
, Configuration const * configuration
, QNetworkAccessManager * network_manager
, QWidget * parent)
: QDialog {parent, Qt::Window | Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowMinimizeButtonHint}
, settings_ {settings}
, directory_ {configuration, network_manager}
, button_box_ {QDialogButtonBox::Close, Qt::Vertical}
{
setWindowTitle (windowTitle () + ' ' + tr (title));
resize (500, 600);
{
SettingsGroup g {settings_, title};
restoreGeometry (settings_->value ("geometry", saveGeometry ()).toByteArray ());
url_line_edit_.setText (settings_->value ("SamplesURL", PROJECT_SAMPLES_URL).toString ());
directory_.url_root (url_line_edit_.text ());
}
setWindowTitle (QApplication::applicationName () + " - " + tr ("Download Samples"));
button_box_.button (QDialogButtonBox::Close)->setDefault (true);
button_box_.addButton ("&Abort", QDialogButtonBox::DestructiveRole);
button_box_.addButton ("&Refresh", QDialogButtonBox::ResetRole);
left_layout_.addWidget (&directory_);
auto details_button = button_box_.addButton ("&Details", QDialogButtonBox::HelpRole);
details_button->setCheckable (true);
details_widget_.hide ();
details_layout_.setMargin (0);
details_layout_.addRow ("Base URL for samples:", &url_line_edit_);
details_widget_.setLayout (&details_layout_);
main_layout_.addLayout (&left_layout_, 0, 0);
main_layout_.addWidget (&button_box_, 0, 1);
main_layout_.addWidget (&details_widget_, 1, 0, 1, 2);
main_layout_.setRowStretch (1, 2);
setLayout (&main_layout_);
connect (&button_box_, &QDialogButtonBox::clicked, this, &SampleDownloader::impl::button_clicked);
connect (details_button, &QAbstractButton::clicked, &details_widget_, &QWidget::setVisible);
connect (&url_line_edit_, &QLineEdit::editingFinished, [this] () {
if (directory_.url_root (url_line_edit_.text ()))
{
directory_.refresh ();
}
else
{
QMessageBox::warning (this, "Input Error", "Invalid URL format");
}
});
}
void SampleDownloader::impl::button_clicked (QAbstractButton * button)
{
switch (button_box_.buttonRole (button))
{
case QDialogButtonBox::RejectRole:
hide ();
break;
case QDialogButtonBox::DestructiveRole:
directory_.abort ();
break;
case QDialogButtonBox::ResetRole:
directory_.refresh ();
break;
default:
break;
}
}

50
SampleDownloader.hpp Normal file
View File

@ -0,0 +1,50 @@
#ifndef SAMPLE_DOWNLOADER_HPP__
#define SAMPLE_DOWNLOADER_HPP__
#include <QObject>
#include "pimpl_h.hpp"
class QSettings;
class QWidget;
class QNetworkAccessManager;
class Configuration;
//
// SampleDownloader - A Dialog to maintain sample files
//
// This uses a Qt Dialog window that contains a tree view of the
// available sample files on a web or ftp server. The files can be
// installed locally by ticking a check box or removed from the local
// machine by un-ticking the check boxes.
//
// The class requires a pointer to an open QSettings instance where it
// will save its persistent state, a pointer to a WSJT-X Configuration
// instance that is used to obtain configuration information like the
// current file save location and, a pointer to a
// QNetworkAccessManager instance which is used for network requests.
//
// An instance of SampleDownloader need not be destroyed after use,
// just call SampleDownloader::show() to make the dialog visible
// again.
//
class SampleDownloader final
: public QObject
{
Q_OBJECT;
public:
SampleDownloader (QSettings * settings
, Configuration const *
, QNetworkAccessManager *
, QWidget * parent = nullptr);
~SampleDownloader ();
Q_SLOT void show ();
private:
class impl;
pimpl<impl> m_;
};
#endif

View File

@ -0,0 +1,299 @@
#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 <QMessageBox>
#include <QJsonDocument>
#include <QJsonParseError>
#include <QJsonArray>
#include <QJsonObject>
#include <QRegularExpression>
#include "Configuration.hpp"
#include "DirectoryNode.hpp"
#include "FileNode.hpp"
#include "revision_utils.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}
, 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 () + '/');
}
if (root.isValid ())
{
url_root_ = root;
refresh ();
}
return root.isValid ();
}
void Directory::error (QString const& title, QString const& message)
{
QMessageBox::warning (this, title, message);
}
bool Directory::refresh ()
{
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));
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
{
QMessageBox::warning (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)
{
QMessageBox::warning (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 ())
{
QMessageBox::warning (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
{
QMessageBox::warning (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};
FileNode::sync_blocker b {node};
node->setIcon (0, file_icon_);
node->setCheckState (0, node->local () ? Qt::Checked : Qt::Unchecked);
update (parent);
}
else
{
QMessageBox::warning (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
{
QMessageBox::warning (this, tr ("JSON Error")
, tr ("Contents entries must be a JSON array"));
}
}
else
{
QMessageBox::warning (this, tr ("JSON Error")
, tr ("Contents entries must have a valid type"));
}
}
else
{
QMessageBox::warning (this, tr ("JSON Error")
, tr ("Contents entries must have a valid name"));
}
}
else
{
QMessageBox::warning (this, tr ("JSON Error")
, tr ("Contents entries must be JSON objects"));
}
}
}
else
{
QMessageBox::warning (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;
}
}
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};
// reset progress
item->setData (1, Qt::UserRole, max);
item->setData (1, Qt::DisplayRole, bytes);
int items {0};
int counted {0};
QTreeWidgetItemIterator iter {item};
// iterate sub tree only
while (*iter && (*iter == item || (*iter)->parent () != item->parent ()))
{
if ((*iter)->type () == FileNode::Type) // only count files
{
++items;
if (auto size = (*iter)->data (1, Qt::UserRole).toLongLong ())
{
max += size;
++counted;
}
bytes += (*iter)->data (1, Qt::DisplayRole).toLongLong ();
}
++iter;
}
// 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 */)
{
QMessageBox::warning (this, "Network Error", "Authentication required");
}

View File

@ -0,0 +1,59 @@
#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 ();
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_;
QDir root_dir_;
QUrl url_root_;
RemoteFile contents_;
DirectoryDelegate item_delegate_;
QIcon dir_icon_;
QIcon file_icon_;
};
#endif

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);
}
}

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

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 const Type {UserType};
};
inline
bool operator == (QString const& lhs, DirectoryNode const& rhs)
{
return rhs == lhs;
}
#endif

View File

@ -0,0 +1,73 @@
#include "FileNode.hpp"
#include <QVariant>
#include <QUrl>
#include <QDir>
#include <QFileInfo>
#include <QMessageBox>
#include "Directory.hpp"
FileNode::FileNode (QTreeWidgetItem * parent
, QNetworkAccessManager * network_manager
, QString const& local_file_path
, QUrl const& url)
: QTreeWidgetItem {parent, Type}
, remote_file_ {this, network_manager, local_file_path}
, 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)
{
QMessageBox::warning (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 ();
}

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 local () const {return remote_file_.local ();}
bool sync (bool local);
void abort ();
static int const 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:
QNetworkAccessManager * network_manager_;
RemoteFile remote_file_; // active download
bool block_sync_;
friend struct sync_blocker;
};
#endif

5
SampleDownloader/README Normal file
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.

View File

@ -0,0 +1,269 @@
#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, QObject * parent)
: QObject {parent}
, listener_ {listener}
, network_manager_ {network_manager}
, local_file_ {local_file_path}
, reply_ {nullptr}
, 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 const& url)
{
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_, &QNetworkReply::finished, this, &RemoteFile::reply_finished);
connect (reply_, &QNetworkReply::readyRead, this, &RemoteFile::store);
connect (reply_, &QNetworkReply::downloadProgress
, [this] (qint64 bytes_received, qint64 total_bytes) {
// report progress of wanted file
if (is_valid_)
{
listener_->download_progress (bytes_received, total_bytes);
}
});
connect (reply_, &QNetworkReply::sslErrors, [this] (QList<QSslError> const& errors) {
QString message;
for (auto const& error: errors)
{
message += '\n' + reply_->request ().url ().toDisplayString () + ": "
+ error.errorString ();
}
listener_->error ("Network SSL Errors", message);
});
}
void RemoteFile::abort ()
{
if (reply_ && reply_->isRunning ())
{
reply_->abort ();
}
}
void RemoteFile::reply_finished ()
{
auto saved_reply = reply_;
auto redirect_url = reply_->attribute (QNetworkRequest::RedirectionTargetAttribute).toUrl ();
if (!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_->isFinished ()) reply_ = nullptr;
disconnect (saved_reply);
saved_reply->deleteLater (); // finished with QNetworkReply
}
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 ()));
}
}
}

View File

@ -0,0 +1,78 @@
#ifndef REMOTE_FILE_HPP__
#define REMOTE_FILE_HPP__
#include <QObject>
#include <QString>
#include <QUrl>
#include <QFileInfo>
#include <QSaveFile>
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, 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_;}
private:
void download (QUrl const& 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_;
QUrl url_;
QNetworkReply * reply_;
bool is_valid_;
unsigned redirect_count_;
QSaveFile file_;
};
#endif

View File

@ -7,6 +7,7 @@
#include <QDateTime> #include <QDateTime>
#include <QApplication> #include <QApplication>
#include <QNetworkAccessManager>
#include <QRegularExpression> #include <QRegularExpression>
#include <QObject> #include <QObject>
#include <QSettings> #include <QSettings>
@ -206,7 +207,7 @@ int main(int argc, char *argv[])
).toBool () ? 1u : 4u; ).toBool () ? 1u : 4u;
} }
MainWindow w(multiple, &settings, &mem_jt9, downSampleFactor); MainWindow w(multiple, &settings, &mem_jt9, downSampleFactor, new QNetworkAccessManager {&a});
w.show(); w.show();
QObject::connect (&a, SIGNAL (lastWindowClosed()), &a, SLOT (quit())); QObject::connect (&a, SIGNAL (lastWindowClosed()), &a, SLOT (quit()));

View File

@ -47,6 +47,7 @@
#include "wsprnet.h" #include "wsprnet.h"
#include "signalmeter.h" #include "signalmeter.h"
#include "HelpTextWindow.hpp" #include "HelpTextWindow.hpp"
#include "SampleDownloader.hpp"
#include "ui_mainwindow.h" #include "ui_mainwindow.h"
#include "moc_mainwindow.cpp" #include "moc_mainwindow.cpp"
@ -130,7 +131,8 @@ namespace
//--------------------------------------------------- MainWindow constructor //--------------------------------------------------- MainWindow constructor
MainWindow::MainWindow(bool multiple, QSettings * settings, QSharedMemory *shdmem, MainWindow::MainWindow(bool multiple, QSettings * settings, QSharedMemory *shdmem,
unsigned downSampleFactor, QWidget *parent) : unsigned downSampleFactor, QNetworkAccessManager * network_manager,
QWidget *parent) :
QMainWindow(parent), QMainWindow(parent),
m_dataDir {QStandardPaths::writableLocation (QStandardPaths::DataLocation)}, m_dataDir {QStandardPaths::writableLocation (QStandardPaths::DataLocation)},
m_revision {revision ()}, m_revision {revision ()},
@ -327,6 +329,14 @@ MainWindow::MainWindow(bool multiple, QSettings * settings, QSharedMemory *shdme
ui->actionInclude_averaging->setActionGroup(DepthGroup); ui->actionInclude_averaging->setActionGroup(DepthGroup);
ui->actionInclude_correlation->setActionGroup(DepthGroup); ui->actionInclude_correlation->setActionGroup(DepthGroup);
connect (ui->download_samples_action, &QAction::triggered, [this, network_manager] () {
if (!m_sampleDownloader)
{
m_sampleDownloader.reset (new SampleDownloader {m_settings, &m_config, network_manager, this});
}
m_sampleDownloader->show ();
});
QButtonGroup* txMsgButtonGroup = new QButtonGroup; QButtonGroup* txMsgButtonGroup = new QButtonGroup;
txMsgButtonGroup->addButton(ui->txrb1,1); txMsgButtonGroup->addButton(ui->txrb1,1);
txMsgButtonGroup->addButton(ui->txrb2,2); txMsgButtonGroup->addButton(ui->txrb2,2);
@ -669,7 +679,7 @@ MainWindow::MainWindow(bool multiple, QSettings * settings, QSharedMemory *shdme
progressBar->setMaximum(m_TRperiod); progressBar->setMaximum(m_TRperiod);
m_modulator->setPeriod(m_TRperiod); // TODO - not thread safe m_modulator->setPeriod(m_TRperiod); // TODO - not thread safe
m_dialFreqRxWSPR=0; m_dialFreqRxWSPR=0;
wsprNet = new WSPRNet(this); wsprNet = new WSPRNet(network_manager, this);
connect( wsprNet, SIGNAL(uploadStatus(QString)), this, SLOT(uploadResponse(QString))); connect( wsprNet, SIGNAL(uploadStatus(QString)), this, SLOT(uploadResponse(QString)));
if(m_bFastMode) { if(m_bFastMode) {
int ntr[]={5,10,15,30}; int ntr[]={5,10,15,30};

View File

@ -49,6 +49,7 @@ namespace Ui {
} }
class QSettings; class QSettings;
class QNetworkAccessManager;
class QLineEdit; class QLineEdit;
class QFont; class QFont;
class QHostInfo; class QHostInfo;
@ -68,6 +69,7 @@ class SoundOutput;
class Modulator; class Modulator;
class SoundInput; class SoundInput;
class Detector; class Detector;
class SampleDownloader;
class MainWindow : public QMainWindow class MainWindow : public QMainWindow
{ {
@ -79,7 +81,8 @@ public:
// Multiple instances: call MainWindow() with *thekey // Multiple instances: call MainWindow() with *thekey
explicit MainWindow(bool multiple, QSettings *, QSharedMemory *shdmem, explicit MainWindow(bool multiple, QSettings *, QSharedMemory *shdmem,
unsigned downSampleFactor, QWidget *parent = 0); unsigned downSampleFactor, QNetworkAccessManager * network_manager,
QWidget *parent = 0);
~MainWindow(); ~MainWindow();
public slots: public slots:
@ -283,6 +286,7 @@ private:
WSPRBandHopping m_WSPR_band_hopping; WSPRBandHopping m_WSPR_band_hopping;
bool m_WSPR_tx_next; bool m_WSPR_tx_next;
QMessageBox m_rigErrorMessageBox; QMessageBox m_rigErrorMessageBox;
QScopedPointer<SampleDownloader> m_sampleDownloader;
QScopedPointer<WideGraph> m_wideGraph; QScopedPointer<WideGraph> m_wideGraph;
QScopedPointer<EchoGraph> m_echoGraph; QScopedPointer<EchoGraph> m_echoGraph;

View File

@ -2351,6 +2351,7 @@ QPushButton[state=&quot;ok&quot;] {
</property> </property>
<addaction name="actionOnline_User_Guide"/> <addaction name="actionOnline_User_Guide"/>
<addaction name="actionLocal_User_Guide"/> <addaction name="actionLocal_User_Guide"/>
<addaction name="download_samples_action"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="actionKeyboard_shortcuts"/> <addaction name="actionKeyboard_shortcuts"/>
<addaction name="actionSpecial_mouse_commands"/> <addaction name="actionSpecial_mouse_commands"/>
@ -2816,6 +2817,14 @@ QPushButton[state=&quot;ok&quot;] {
<string>JTMSK</string> <string>JTMSK</string>
</property> </property>
</action> </action>
<action name="download_samples_action">
<property name="text">
<string>&amp;Download Samples ...</string>
</property>
<property name="whatsThis">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Download sample audio files demonstrating the various modes.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</action>
</widget> </widget>
<layoutdefault spacing="6" margin="11"/> <layoutdefault spacing="6" margin="11"/>
<customwidgets> <customwidgets>

View File

@ -76,6 +76,21 @@ QString font_as_stylesheet (QFont const&);
// conditional style sheet updates // conditional style sheet updates
void update_dynamic_property (QWidget *, char const * property, QVariant const& value); void update_dynamic_property (QWidget *, char const * property, QVariant const& value);
template <class T>
class VPtr
{
public:
static T * asPtr (QVariant v)
{
return reinterpret_cast<T *> (v.value<void *> ());
}
static QVariant asQVariant(T * ptr)
{
return qVariantFromValue (reinterpret_cast<void *> (ptr));
}
};
// Register some useful Qt types with QMetaType // Register some useful Qt types with QMetaType
Q_DECLARE_METATYPE (QHostAddress); Q_DECLARE_METATYPE (QHostAddress);

View File

@ -63,13 +63,18 @@ QString revision (QString const& svn_rev_string)
return result.trimmed (); return result.trimmed ();
} }
QString version () QString version (bool include_patch)
{ {
#if defined (CMAKE_BUILD) #if defined (CMAKE_BUILD)
QString v {WSJTX_STRINGIZE (WSJTX_VERSION_MAJOR) "." WSJTX_STRINGIZE (WSJTX_VERSION_MINOR) "." WSJTX_STRINGIZE (WSJTX_VERSION_PATCH)}; QString v {WSJTX_STRINGIZE (WSJTX_VERSION_MAJOR) "." WSJTX_STRINGIZE (WSJTX_VERSION_MINOR)};
if (include_patch)
{
v += "." WSJTX_STRINGIZE (WSJTX_VERSION_PATCH)
# if defined (WSJTX_RC) # if defined (WSJTX_RC)
v += "-rc" WSJTX_STRINGIZE (WSJTX_RC); + "-rc" WSJTX_STRINGIZE (WSJTX_RC)
# endif # endif
;
}
#else #else
QString v {"Not for Release"}; QString v {"Not for Release"};
#endif #endif

View File

@ -4,7 +4,7 @@
#include <QString> #include <QString>
QString revision (QString const& svn_rev_string = QString {}); QString revision (QString const& svn_rev_string = QString {});
QString version (); QString version (bool include_patch = true);
QString program_title (QString const& revision = QString {}); QString program_title (QString const& revision = QString {});
#endif #endif

122
samples/CMakeLists.txt Normal file
View File

@ -0,0 +1,122 @@
set (SAMPLE_FILES
JT9+JT65/130610_2343.wav
JT9/130418_1742.wav
)
#set_directory_properties (PROPERTIES EXCLUDE_FROM_ALL ON)
set (contents_file_ ${CMAKE_CURRENT_BINARY_DIR}/contents_${WSJTX_VERSION_MAJOR}.${WSJTX_VERSION_MINOR}.json)
function (indent_)
foreach (temp_ RANGE ${level_})
file (APPEND ${contents_file_} " ")
endforeach ()
endfunction ()
function (end_entry_)
file (APPEND ${contents_file_} "\n")
set(first_ 0 PARENT_SCOPE)
math (EXPR level_ "${level_} - 1")
indent_ ()
file (APPEND ${contents_file_} "]\n")
math (EXPR level_ "${level_} - 2")
indent_ ()
file (APPEND ${contents_file_} "}")
string (FIND "${dirs_}" "${cwd_}" pos_)
set (level_ ${level_} PARENT_SCOPE)
endfunction ()
file (WRITE ${contents_file_} "[")
set (cwd_)
set (level_ 0)
set (first_ 1)
list (SORT SAMPLE_FILES)
foreach (sample_ IN LISTS SAMPLE_FILES)
string (REGEX MATCHALL "[^/]*/" dirs_ "${sample_}")
string (REPLACE "/" "" dirs_ "${dirs_}")
string (REGEX MATCH "[^/]*$" name_ "${sample_}")
string (FIND "${dirs_}" "${cwd_}" pos_)
list (LENGTH cwd_ cwd_count_)
if (${pos_} EQUAL 0)
# same root
while (${cwd_count_} GREATER 0)
list (REMOVE_AT dirs_ 0)
math (EXPR cwd_count_ "${cwd_count_} - 1")
endwhile ()
else ()
# reduce cwd_ until matched
while ((NOT ${pos_} EQUAL 0) AND ${cwd_count_} GREATER 0)
math (EXPR cwd_count_ "${cwd_count_} - 1")
list (REMOVE_AT cwd_ ${cwd_count_})
end_entry_ ()
endwhile ()
# back to same root
while (${cwd_count_} GREATER 0)
list (REMOVE_AT dirs_ 0)
math (EXPR cwd_count_ "${cwd_count_} - 1")
endwhile ()
endif ()
list (LENGTH cwd_ cwd_count_)
list (LENGTH dirs_ path_count_)
while (${path_count_} GREATER 0)
list (GET dirs_ 0 dir_)
list (APPEND cwd_ "${dir_}")
list (REMOVE_AT dirs_ 0)
if (${first_})
file (APPEND ${contents_file_} "\n")
set (first 0)
else ()
file (APPEND ${contents_file_} ",\n")
endif ()
indent_ ()
file (APPEND ${contents_file_} "{\n")
math (EXPR level_ "${level_} + 1")
indent_ ()
file (APPEND ${contents_file_} "\"type\": \"directory\",\n")
indent_ ()
file (APPEND ${contents_file_} "\"name\": \"${dir_}\",\n")
indent_ ()
file (APPEND ${contents_file_} "\"entries\": [")
set (first_ 1)
math (EXPR level_ "${level_} + 2")
math (EXPR path_count_ "${path_count_} - 1")
endwhile ()
file (COPY ${sample_} DESTINATION web/samples/${cwd_})
if (${first_})
file (APPEND ${contents_file_} "\n")
set (first 0)
else ()
file (APPEND ${contents_file_} ",\n")
endif ()
indent_ ()
file (APPEND ${contents_file_} "{\n")
math (EXPR level_ "${level_} + 1")
indent_ ()
file (APPEND ${contents_file_} "\"type\": \"file\",\n")
indent_ ()
file (APPEND ${contents_file_} "\"name\": \"${name_}\"\n")
math (EXPR level_ "${level_} - 1")
indent_ ()
file (APPEND ${contents_file_} "}")
set (first_ 0)
endforeach ()
if (${level_} GREATER 1)
end_entry_ ()
endif ()
file (APPEND ${contents_file_} "\n]\n")
file (COPY ${contents_file_} DESTINATION web/samples)
find_program (RSYNC_EXECUTABLE rsync)
if (RSYNC_EXECUTABLE)
add_custom_command (
OUTPUT upload.timestamp
COMMAND ${RSYNC_EXECUTABLE} ARGS -avz ${CMAKE_CURRENT_BINARY_DIR}/web/samples ${PROJECT_SAMPLES_UPLOAD_DEST}
COMMAND ${CMAKE_COMMAND} ARGS touch upload.timestamp
DEPENDS ${contents_file_} ${SAMPLE_FILES}
COMMENT "Uploading WSJT-X samples to web server"
)
add_custom_target (upload-samples DEPENDS upload.timestamp)
endif ()

View File

@ -16,6 +16,7 @@
#cmakedefine WSJT_DATA_DESTINATION "@WSJT_DATA_DESTINATION@" #cmakedefine WSJT_DATA_DESTINATION "@WSJT_DATA_DESTINATION@"
#cmakedefine PROJECT_MANUAL "@PROJECT_MANUAL@" #cmakedefine PROJECT_MANUAL "@PROJECT_MANUAL@"
#cmakedefine PROJECT_MANUAL_DIRECTORY_URL "@PROJECT_MANUAL_DIRECTORY_URL@" #cmakedefine PROJECT_MANUAL_DIRECTORY_URL "@PROJECT_MANUAL_DIRECTORY_URL@"
#cmakedefine PROJECT_SAMPLES_URL "@PROJECT_SAMPLES_URL@"
#cmakedefine01 WSJT_SHARED_RUNTIME #cmakedefine01 WSJT_SHARED_RUNTIME
#cmakedefine01 WSJT_QDEBUG_TO_FILE #cmakedefine01 WSJT_QDEBUG_TO_FILE

View File

@ -22,9 +22,9 @@ namespace
// char const * const wsprNetUrl = "http://127.0.0.1/post?"; // char const * const wsprNetUrl = "http://127.0.0.1/post?";
}; };
WSPRNet::WSPRNet(QObject *parent) WSPRNet::WSPRNet(QNetworkAccessManager * manager, QObject *parent)
: QObject{parent} : QObject{parent}
, networkManager {new QNetworkAccessManager {this}} , networkManager {manager}
, uploadTimer {new QTimer {this}} , uploadTimer {new QTimer {this}}
, m_urlQueueSize {0} , m_urlQueueSize {0}
{ {
@ -74,33 +74,35 @@ void WSPRNet::upload(QString const& call, QString const& grid, QString const& rf
void WSPRNet::networkReply(QNetworkReply *reply) void WSPRNet::networkReply(QNetworkReply *reply)
{ {
if (QNetworkReply::NoError != reply->error ()) { // check if request was ours
Q_EMIT uploadStatus (QString {"Error: %1"}.arg (reply->error ())); if (m_outstandingRequests.removeOne (reply)) {
// not clearing queue or halting queuing as it may be a transient if (QNetworkReply::NoError != reply->error ()) {
// one off request error Q_EMIT uploadStatus (QString {"Error: %1"}.arg (reply->error ()));
} // not clearing queue or halting queuing as it may be a transient
else { // one off request error
QString serverResponse = reply->readAll(); }
if( m_uploadType == 2) { else {
if (!serverResponse.contains(QRegExp("spot\\(s\\) added"))) { QString serverResponse = reply->readAll();
emit uploadStatus("Upload Failed"); if( m_uploadType == 2) {
urlQueue.clear(); if (!serverResponse.contains(QRegExp("spot\\(s\\) added"))) {
emit uploadStatus("Upload Failed");
urlQueue.clear();
uploadTimer->stop();
}
}
if (urlQueue.isEmpty()) {
emit uploadStatus("done");
QFile::remove(m_file);
uploadTimer->stop(); uploadTimer->stop();
} }
} }
if (urlQueue.isEmpty()) { qDebug () << QString {"WSPRnet.org %1 outstanding requests"}.arg (m_outstandingRequests.size ());
emit uploadStatus("done");
QFile::remove(m_file); // delete request object instance on return to the event loop otherwise it is leaked
uploadTimer->stop(); reply->deleteLater ();
}
} }
m_outstandingRequests.removeOne (reply);
qDebug () << QString {"WSPRnet.org %1 outstanding requests"}.arg (m_outstandingRequests.size ());
// delete request object instance on return to the event loop otherwise it is leaked
reply->deleteLater ();
} }
bool WSPRNet::decodeLine(QString const& line, QHash<QString,QString> &query) bool WSPRNet::decodeLine(QString const& line, QHash<QString,QString> &query)

View File

@ -16,7 +16,7 @@ class WSPRNet : public QObject
Q_OBJECT; Q_OBJECT;
public: public:
explicit WSPRNet(QObject *parent = nullptr); explicit WSPRNet(QNetworkAccessManager *, QObject *parent = nullptr);
void upload(QString const& call, QString const& grid, QString const& rfreq, QString const& tfreq, void upload(QString const& call, QString const& grid, QString const& rfreq, QString const& tfreq,
QString const& mode, QString const& tpct, QString const& dbm, QString const& version, QString const& mode, QString const& tpct, QString const& dbm, QString const& version,
QString const& fileName); QString const& fileName);