videodownloader/network/download.cpp

1248 lines
47 KiB
C++

#include "./download.h"
#include "./permissionstatus.h"
// these includes are only needed to provide the Download::fromUrl method
#include "./bitsharedownload.h"
#include "./httpdownload.h"
#include "./socksharedownload.h"
#include "./youtubedownload.h"
#include <c++utilities/application/global.h>
#include <c++utilities/io/path.h>
#include <QFileInfo>
#include <QIODevice>
#include <QMessageBox>
#include <QTranslator>
#include <random>
using namespace std;
using namespace CppUtilities;
namespace Network {
/*!
* \class Download
* \brief The Download class is the base class for all download implementations used within the downloader application.
*
* The Download class does more then just downloading. It also writes the downloaded data to an output device and calculates
* the current progress percentage, the current speed and the remaining time.
*
* Some implementations might feature different download options (e. g. different video
* qualities) and are able to fetch additional meta data such as title and uploader. A suitable filename for the
* output file is possibly provided as well.
*
* <h3>Methods to be implemented when subclassing:</h3>
* - doInit(): Starts the initialisation; called before the actual download. After initiating the reportInitiated() method
* must be called.
* - doDownload(): Starts the actual download.
* - abortDownload(): Aborts the current download.
* - checkStatusAndClear(): Checks the status and frees resources after the download has been completed; called
* after reportDownloadComplete() has been called. Results should be returned using the reportFinalDownloadStatus() method.
* - followRedirection(): Starts the download again using the redirection URL; called when a redirection is available
* and the redirection is accepted.
* - isInitiatingInstantlyRecommendable(): Returns whether instantly initiating is recommendable.
* - supportsRange(): Returns whether a range can be set.
* - typeName(): Returns the type of the download as string (e. g. "Youtube Download").
*
* All of these methods must return immediately. Lasting operations should be performed asynchronously.
*
* <h3>Methods to be called when subclassing:</h3>
* - reportInitiated(): Reports that the download has been initiated.
* - reportNewDataToBeWritten(): Reports that there is new data to be written available.
* - reportRedirectionAvailable(): Reports that there is a redirection available.
* - reportAuthenticationRequired(): Reports that authentication credentials are required.
* - reportSslErrors(): Reports that one or more SSL errors occured.
* - reportDownloadComplete(): Reports that the download is complete.
* - reportFinalDownloadStatus(): Reports the final download status.
* - addDownloadUrl(): Makes a download URL under the specified option name available. Meant to be called during initialization.
* - changeDownloadUrl(): Updates the download URL with the specified option index.
*
* <h3>Signals to be handled when using the class:</h3>
* - overwriteingPermissionRequired(): Emitted when the permission to overwrite an existing file is required. Overwriting might be allowed or
* refused using the setOverwritePermission() method.
* - appendingPermissionRequired(): Emitted when the permission to append data to an existing file is required. Appending might be allowed or
* refused using the setAppendPermission() method.
* - redirectionPermissonRequired(): Emitted when the permission to overwrite a file is required. Overwriting might be allowed or
* refused using the setRedirectPermission() method.
* - outputDeviceRequired(): Emitted when an output device for writing the downloaded data is required. An output device can be provided or denialed
* using the provideOutputDevice() method.
* - authenticationRequired(): Emitted when authentication credentials are required. Authentication credentials can be provided or denialed
* using the provideAuthenticationCredentials() method.
*/
int Download::s_defaultUserAgent = -1;
/*!
* \brief Returns a random default user agent string.
*
* The default user agent is used and returned by userAgent() if no custom user agent has been assigned
* using the setUserAgent() method.
*
* The default user agent is picked from a set of pre-defined user agent strings. To pick another
* string use the scrambleDefaultUserAgent() method.
*/
const QString &Download::defaultUserAgent()
{
static const QString agents[] = { QStringLiteral("Mozilla/5.0 (X11; Linux x86_64; rv:32.0) Gecko/20100101 Firefox/32.0"),
QStringLiteral("Mozilla/5.0 (Windows NT 5.1; rv:31.0) Gecko/20100101 Firefox/31.0"),
QStringLiteral("Mozilla/5.0 (X11; OpenBSD amd64; rv:28.0) Gecko/20100101 Firefox/28.0"),
QStringLiteral("Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2049.0 Safari/537.36"),
QStringLiteral("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.120 Safari/537.36"),
QStringLiteral("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.34 (KHTML, like Gecko) konqueror/4.14.0 Safari/534.34"),
QStringLiteral("Opera/9.80 (Windows NT 6.0) Presto/2.12.388 Version/12.14") };
if (s_defaultUserAgent < 0 || s_defaultUserAgent > 6) {
scrambleDefaultUserAgent();
}
return agents[s_defaultUserAgent];
}
/*!
* \brief Scrambles the default user agent.
* \sa defaultUserAgent()
*/
void Download::scrambleDefaultUserAgent()
{
random_device rd;
default_random_engine engine(rd());
uniform_int_distribution<int> distribution(0, 6);
s_defaultUserAgent = distribution(engine);
}
/*!
* \brief Returns a suitable filename for the download.
*
* This name might be used when creating the output file.
*/
QString Download::suitableFilename() const
{
string title(this->title().toStdString());
removeInvalidChars(title);
return QString::fromStdString(title);
}
/*!
* \brief Sets the meta data for the download.
*
* This method might be useful to provide meta data from an external source.
*/
void Download::provideMetaData(const QString &title, const QString &uploader, TimeSpan duration, const QString &collectionName,
int positionInCollection, int views, const QString &rating)
{
if (!title.isEmpty()) {
this->m_title = title;
this->m_dontAcceptNewTitleFromFilenameAnymore = true;
}
if (!uploader.isEmpty()) {
this->m_uploader = uploader;
}
if (!duration.isNull()) {
this->m_duration = duration;
}
if (!collectionName.isEmpty()) {
this->m_collectionName = collectionName;
}
if (positionInCollection > 0) {
this->m_positionInCollection = positionInCollection;
}
if (views > 0) {
this->m_views = views;
}
if (!rating.isEmpty()) {
this->m_rating = rating;
}
}
/*!
* \brief Returns an indication whether a instant initiation of the download is recommendable.
*
* Some downloads (such as YouTube downloads) should be initiated instantly to fetch information like
* available qualities, title and uploader. Some downloads can not fetch these information during initialization
* and might be initiated just before starting the download.
*/
bool Download::isInitiatingInstantlyRecommendable() const
{
return false;
}
/*!
* \brief Constructs a new download for the specified \a url.
* \returns Returns the download or nullptr if no download could be
* constructed.
*/
Download *Download::fromUrl(const QUrl &url)
{
QString scheme = url.scheme();
QString host = url.host(QUrl::FullyDecoded);
if (scheme == QLatin1String("http") || scheme == QLatin1String("https") || scheme == QLatin1String("ftp")) {
if (host.contains(QStringLiteral("youtube"), Qt::CaseInsensitive) || host.contains(QStringLiteral("youtu.be"), Qt::CaseInsensitive)) {
return new YoutubeDownload(url);
} else if (host.contains(QStringLiteral("sockshare")) || host.contains(QStringLiteral("putlocker"))) {
return new SockshareDownload(url);
} else if (host.contains(QStringLiteral("bitshare"))) {
return new BitshareDownload(url);
} else {
return new HttpDownload(url);
}
}
return nullptr;
}
/*!
* \brief Returns an indication whether the download can be started.
* \sa start()
* \param reasonIfNot Specifies a string which will hold the reason if the download can not be started.
* \remarks This method is used internally be the start() method to determine whether the download
* can be started and to set the statusInfo() appropriately. Hence it is not required to perform
* this check before starting a download. You might just want to check isStarted() instead.
*/
bool Download::canStart(QString &reasonIfNot)
{
if (!isInitiated()) {
reasonIfNot = tr("Download is not initiated.");
} else if (isStarted()) {
reasonIfNot = tr("Download is already started.");
} else if (!isValidOptionChosen()) {
reasonIfNot = tr("No valid option chosen.");
} else {
return true;
}
return false;
}
/*!
* \brief Starts the download. Does nothing if the download is already started.
*
* If a target path is has been set (using the setTargetPath() method) it will be used.
*
* If no target file is set, an output device will be requested later (when a suitable filename
* might be available).
*
* \remarks The download needs to be initiated before and a valid option needs to be chosen.
* \sa init()
* \sa isValidOptionChosen()
* \sa availableOptionCount()
* \sa setChosenOption()
*/
void Download::start()
{
if (!isStarted()) {
QString reasonForFail;
if (canStart(reasonForFail)) {
for (auto &optionData : m_optionData) {
optionData.m_downloadComplete = false;
optionData.m_downloadAbortedInternally = false;
optionData.m_bytesWritten = 0;
}
OptionData &optionData = m_optionData.at(chosenOption());
if (!optionData.m_outputDevice && !m_targetPath.isEmpty()) {
unique_ptr<QFile> targetDevice(new QFile(m_targetPath));
if (prepareOutputDevice(chosenOption(), targetDevice.get(), true)) {
targetDevice.release();
} else {
return; // output device couldn't be prepared, error already handled in prepareOutputDevice()
}
}
setStatus(DownloadStatus::Downloading);
doDownload();
} else {
setStatusInfo(reasonForFail);
setStatus(DownloadStatus::Failed);
}
}
}
/*!
* \brief Starts the download. Does nothing if the download is already started.
*
* The specified file will be created/opened by the download and used to store the received data.
*
* The current target path is replaced by the specified one.
*
* \remarks The download needs to be initiated before and a valid option needs to be chosen.
* \sa init()
* \sa isValidOptionChosen()
* \sa availableOptionCount()
* \sa setChosenOption()
*/
void Download::start(const QString &targetPath)
{
if (!isStarted()) {
finalizeOutputDevice(chosenOption());
m_targetPath = targetPath;
start();
}
}
/*!
* \brief Starts the download. Does nothing if the download is already started.
*
* The specified \a targetDevice will be used by the download to store the received data.
* The download will ignore a previously set target path.
*
* The download take only ownership over the specified \a targetDevice if \a giveOwnership is true.
*
* \remarks The download needs to be initiated before and a valid option needs to be chosen.
*
* \sa init()
* \sa isValidOptionChosen()
* \sa availableOptionCount()
* \sa setChosenOption()
*/
void Download::start(QIODevice *targetDevice, bool giveOwnership)
{
if (!isStarted()) {
QString reasonForFail;
if (canStart(reasonForFail)) {
finalizeOutputDevice(chosenOption());
if (prepareOutputDevice(chosenOption(), targetDevice, giveOwnership)) {
start();
}
} else {
setStatusInfo(reasonForFail);
setStatus(DownloadStatus::Failed);
}
}
}
/*!
* \brief Stops the download. Does nothing if the download has not been started or has been ended yet.
*/
void Download::stop()
{
if (isStarted()) {
switch (status()) {
case DownloadStatus::Interrupting:
setStatus(DownloadStatus::Aborting);
[[fallthrough]];
case DownloadStatus::Aborting:
break;
default:
setStatus(DownloadStatus::Aborting);
abortDownload();
}
} else if (status() == DownloadStatus::Initiating) {
setStatus(DownloadStatus::Aborting);
abortDownload();
}
}
/*!
* \brief Interrupts the download. Does nothing if the download has not been started or has been ended yet.
*
* The only difference to the stop() method is that the download will enter the interrupting/interrupted status
* and not the failed status.
*/
void Download::interrupt()
{
if (isStarted()) {
switch (status()) {
case DownloadStatus::Aborting:
setStatus(DownloadStatus::Interrupting);
break;
case DownloadStatus::Interrupting:
break;
default:
setStatus(DownloadStatus::Interrupting);
abortDownload();
}
}
}
/*!
* \brief Constructs a new donwload with the specified \a url.
*/
Download::Download(const QUrl &url, QObject *parent)
: QObject(parent)
, m_initialUrl(url)
, m_dontAcceptNewTitleFromFilenameAnymore(false)
, m_views(0)
, m_positionInCollection(0)
, m_chosenOption(0)
, m_selectedOptionChanged(true)
, m_availableOptionsChanged(true)
, m_status(DownloadStatus::None)
, m_lastState(DownloadStatus::None)
, m_bytesReceived(-1)
, m_bytesToReceive(-1)
, m_newBytesReceived(0)
, m_newBytesToReceive(0)
, m_speed(0.0)
, m_shiftSpeed(0.0)
, m_networkError(QNetworkReply::NoError)
, m_initiated(false)
, m_progressUpdateInterval(300)
, m_useDefaultUserAgent(true)
, m_proxy(QNetworkProxy::NoProxy)
{
m_time.start();
}
/*!
* \brief Starts the download again using the redirection URL.
* \returns Returns an indication whether the operation succeeded.
*
* Needs to be implemented when subclassing.
*/
bool Download::followRedirection(std::size_t)
{
return false;
}
/*!
* \brief Destroys the download.
*/
Download::~Download()
{
}
/*!
* \brief Sets the chosen option. This option will be used when starting the download.
* \returns Returns an indication whether the chosen option could be set.
*/
bool Download::setChosenOption(size_t optionIndex)
{
if (optionIndex < availableOptionCount()) {
if (m_chosenOption != optionIndex) {
m_chosenOption = optionIndex;
m_selectedOptionChanged = true;
}
return true;
} else {
return false;
}
}
/*!
* \brief Starts initiating the download.
*
* The download will enter the initiating status. This method returns immediately.
*/
void Download::init()
{
if ((!m_initiated) && (status() != DownloadStatus::Initiating)) {
setStatus(DownloadStatus::Initiating);
doInit();
}
}
/*!
* \brief Reports the initialization status.
*
* Needs to be called when subclassing after the initialzation ended.
*
* \param success Specifies whether the initialization succeeded.
* \param reasonIfNot Specifies the reason if the initialization failed; ignored otherwise.
* \param networkError Specifies if and what kind of network error occured.
*/
void Download::reportInitiated(bool success, const QString &reasonIfNot, const QNetworkReply::NetworkError &networkError)
{
setNetworkError(networkError);
if (success) {
m_initiated = true;
setStatus(DownloadStatus::Ready);
} else {
m_initiated = false;
setStatusInfo(tr("The initial information for this download couldn't be retireved. Reason: ") + reasonIfNot);
setStatus(DownloadStatus::Failed);
}
}
/*!
* \brief Prepares the specified output \a device for the option with the specified \a optionIndex.
* \remarks If there is already an output device assigned it must be
* finalized before calling this method. This method is used internally to
* assign an output device instead of setting m_optionData.at(...).outputDevice directly.
* \returns Returns whether the \a device could be prepared.
*
* If the operation fails the error is handled by the method.
*/
bool Download::prepareOutputDevice(size_t optionIndex, QIODevice *device, bool takeOwnership)
{
bool ok = true;
bool ready = true;
OptionData &optionData = m_optionData.at(optionIndex);
if (!device->isOpen()) {
if (QFile *file = qobject_cast<QFile *>(device)) {
if (file->exists() && file->size() > 0) {
if (m_range.isUsedForWritingOutput() && m_range.currentOffset() > 0) {
// we need to append to an existing file
switch (optionData.m_appendPermission) {
case PermissionStatus::Unknown:
case PermissionStatus::Asking:
setStatus(DownloadStatus::Waiting);
optionData.m_outputDevice = device;
if (optionData.m_appendPermission != PermissionStatus::Asking) {
optionData.m_appendPermission = PermissionStatus::Asking;
emit appendingPermissionRequired(this, optionIndex, file->fileName(), m_range.currentOffset(), file->size());
}
ready = false;
break;
case PermissionStatus::Allowed:
case PermissionStatus::AlwaysAllowed:
usePermission(optionData.m_appendPermission);
break;
case PermissionStatus::Refused:
case PermissionStatus::AlwaysRefused:
usePermission(optionData.m_appendPermission);
setStatusInfo(tr("Appending to existing target file not permitted."));
ok = false;
}
} else { // we need to overwrite the existing file
switch (optionData.m_overwritePermission) {
case PermissionStatus::Unknown:
case PermissionStatus::Asking:
setStatus(DownloadStatus::Waiting);
optionData.m_outputDevice = device;
if (optionData.m_overwritePermission != PermissionStatus::Asking) {
optionData.m_overwritePermission = PermissionStatus::Asking;
emit overwriteingPermissionRequired(this, optionIndex, file->fileName());
}
ready = false;
break;
case PermissionStatus::Allowed:
case PermissionStatus::AlwaysAllowed:
usePermission(optionData.m_overwritePermission);
// overwriting the file is allowed -> clear the present file
if (!file->resize(0)) {
setStatusInfo(tr("The already existing output file couldn't be cleared."));
ok = false;
}
break;
case PermissionStatus::Refused:
case PermissionStatus::AlwaysRefused:
usePermission(optionData.m_overwritePermission);
setStatusInfo(tr("Overwriting the existing target file not permitted."));
ok = false;
}
}
}
}
if (ok && ready && !device->open(QIODevice::WriteOnly | QIODevice::Append)) {
setStatusInfo(tr("Unable to open the output file/stream."));
ok = false;
}
} else if (!device->isWritable()) {
setStatusInfo(tr("The output file/stream isn't writable."));
ok = false;
}
if (ok && ready && m_range.isUsedForWritingOutput() && m_range.currentOffset() >= 0) {
if (device->isSequential()) {
setStatusInfo(tr("Unable to seek to the range on sequential output streams."));
ok = false;
} else {
if (!device->seek(m_range.currentOffset())) {
setStatusInfo(tr("Unable to seek to the range in the output file/stream."));
ok = false;
}
}
}
optionData.m_outputDeviceReady = ok && ready;
if (ok) {
optionData.m_outputDevice = device;
optionData.m_hasOutputDeviceOwnership = takeOwnership;
} else { // handle error case
if (isStarted()) {
abortDownload(); // ensure download is aborted
}
if (takeOwnership && device) {
delete device;
}
optionData.m_outputDevice = nullptr;
optionData.m_hasOutputDeviceOwnership = false;
optionData.m_buffer.reset();
optionData.m_requestingNewOutputDevice = false;
m_range.increaseCurrentOffset(optionData.m_bytesWritten);
m_range.setUsedForRequest();
m_range.setUsedForWritingOutput();
setStatus(DownloadStatus::Failed);
}
return ok;
}
/*!
* \brief Ensures that the currently assigned output device is prepared.
*
* Does nothing if there has been no output device assigned using prepareOutputDevice or
* the output device is already prepared.
*/
void Download::ensureOutputDeviceIsPrepared(size_t optionIndex)
{
OptionData &optionData = m_optionData.at(optionIndex);
if (optionData.m_outputDevice && !optionData.m_outputDeviceReady) {
if (!prepareOutputDevice(optionIndex, optionData.m_outputDevice, optionData.m_hasOutputDeviceOwnership)) {
return; // output device can not be prepared
}
writeBufferToOutputDevice(optionIndex);
optionData.m_stillWriting = false; // not writing anymore
if (optionData.m_downloadComplete) { // download has ended, too
checkStatusAndClear(optionIndex);
}
}
}
/*!
* \brief Finalizes the output device.
*
* This method is meant to be called after the all data has been written to the output device.
* The output device will be closed and deleted if the downloader has the ownership.
* Does nothing if the download has not the ownership over the device or there is no output device assigned.
*/
void Download::finalizeOutputDevice(size_t optionIndex)
{
OptionData &optionData = m_optionData.at(optionIndex);
if (optionData.m_hasOutputDeviceOwnership && optionData.m_outputDevice) {
if (optionData.m_outputDevice->isOpen()) {
if (QFile *targetFile = qobject_cast<QFile *>(optionData.m_outputDevice)) {
targetFile->flush();
}
optionData.m_outputDevice->close();
}
optionData.chuckOutputDevice();
}
optionData.m_outputDevice = nullptr;
optionData.m_outputDeviceReady = false;
}
/*!
* \brief Reports the final download status.
*
* Needs to be called when subclassing after the download has ended.
*
* \param optionIndex Specifies the concerning option.
* \param success Specifies whether the download was successful.
* \param statusDescription Specifies a status description.
* \param networkError Specifies if or what kind of network error occured.
*/
void Download::reportFinalDownloadStatus(size_t optionIndex, bool success, const QString &statusDescription, QNetworkReply::NetworkError networkError)
{
finalizeOutputDevice(optionIndex);
const OptionData &optionData = m_optionData[optionIndex];
if (!optionData.m_downloadAbortedInternally) {
m_range.increaseCurrentOffset(optionData.m_bytesWritten);
m_range.setUsedForRequest();
m_range.setUsedForWritingOutput();
if (success) {
setStatusInfo(statusDescription);
setStatus(DownloadStatus::Finished);
} else {
// set current offset of the range to be able to resume downloading
setStatusInfo(statusDescription);
setNetworkError(networkError);
setStatus(DownloadStatus::Failed);
}
}
}
/*!
* \brief Reports that the download has been interrupted.
*
* Needs to be called when subclassing after the download has been interrupted.
*/
void Download::reportDownloadInterrupted(size_t optionIndex)
{
finalizeOutputDevice(optionIndex);
// set current offset of the range to be able to resume downloading
const OptionData &optionData = m_optionData[optionIndex];
m_range.increaseCurrentOffset(optionData.m_bytesWritten);
m_range.setUsedForRequest();
m_range.setUsedForWritingOutput();
setStatus(DownloadStatus::Interrupted);
}
/*!
* \brief Reports the download progress.
* \param bytesReceived Specifies the number of bytes received.
* \param bytesToReceive Specifies the number of bytes to be received.
*
* Needs to be called when subclassing while downloading.
* If the total number of bytes is unknown, -1 should be specified for \a bytesToRecieve.
*/
void Download::reportDownloadProgressUpdate(size_t optionIndex, qint64 bytesReceived, qint64 bytesToReceive)
{
const OptionData &optionData = m_optionData[optionIndex];
if (bytesReceived == bytesToReceive) {
setProgress(bytesReceived, bytesToReceive);
} else {
if (!optionData.m_downloadComplete && lastProgressUpdate() >= m_progressUpdateInterval) {
switch (status()) {
case DownloadStatus::Interrupting:
case DownloadStatus::Aborting:
break;
default:
setStatus(DownloadStatus::Downloading);
}
setProgress(bytesReceived, bytesToReceive);
}
}
}
/*!
* \brief Reports that new bytes are available.
* \param inputDevice Specifies the device the download will read the available data from.
*
* Needs to be called when subclassing while downloading.
*
* Does nothing when currently not downloading.
*/
void Download::reportNewDataToBeWritten(size_t optionIndex, QIODevice *inputDevice)
{
OptionData &optionData = m_optionData[optionIndex];
optionData.m_stillWriting = true;
char buffer[1024];
streamsize read;
qint64 written;
if (optionData.m_outputDevice && optionData.m_outputDeviceReady) { // there's a ready output device
if (!writeBufferToOutputDevice(optionIndex)) { // write data which has been buffered earlier first
return; // error is already handled within writeBufferToOutputDevice(), just return here
}
// read the new data from the input device and write it to the output device
if (inputDevice) {
while ((read = inputDevice->read(buffer, sizeof(buffer))) > 0) {
if ((written = optionData.m_outputDevice->write(buffer, read)) == read) {
optionData.m_bytesWritten += written;
} else {
abortDownload(); // ensure download is aborted
optionData.m_buffer.reset();
optionData.m_requestingNewOutputDevice = false;
optionData.m_stillWriting = false;
optionData.m_downloadAbortedInternally = true;
reportFinalDownloadStatus(optionIndex, false, tr("Unable to write to provided output device."));
return;
}
}
}
optionData.m_stillWriting = false; // not writing anymore
if (optionData.m_downloadComplete) {
// check status and clear if the download is done; shouldn't happen?
checkStatusAndClear(optionIndex);
}
} else {
// there's no ready output device -> use a buffer to store the data
if (!optionData.m_buffer) { // create a new buffer if none exists
optionData.m_buffer.reset(new stringstream(stringstream::in | stringstream::out));
}
// write the data to the buffer
if (inputDevice) {
while ((read = inputDevice->read(buffer, sizeof(buffer))) > 0) {
optionData.m_buffer->write(buffer, read);
}
}
if (!optionData.m_outputDevice && !optionData.m_requestingNewOutputDevice) {
// request a new output device if not requested yet
optionData.m_requestingNewOutputDevice = true;
if (optionData.m_downloadComplete) {
// set the status to waiting if the actual download is complete but an output device is still required
setStatus(DownloadStatus::Waiting);
}
emit outputDeviceRequired(this, optionIndex);
} // else: we're still waiting for the output device to be ready but there's nothing to be done about that here
}
}
/*!
* \brief Writes buffer data to the output device.
* \remarks
* - Does nothing if there is no buffered data or if there is no output device.
* - Wipes the buffer afterwards.
* \returns Returns an indication whether the operation succeeded.
*
* If the operation fails the error will be handled within the method.
*/
bool Download::writeBufferToOutputDevice(size_t optionIndex)
{
OptionData &optionData = m_optionData[optionIndex];
streamsize read;
qint64 written;
if (optionData.m_buffer && optionData.m_outputDevice && optionData.m_outputDeviceReady) {
optionData.m_buffer->seekg(0);
char buffer[1024];
while (optionData.m_buffer->good()) {
optionData.m_buffer->read(buffer, sizeof(buffer));
read = optionData.m_buffer->gcount();
if ((written = optionData.m_outputDevice->write(buffer, read)) >= 0) {
optionData.m_bytesWritten += written;
} else {
optionData.m_buffer.reset();
optionData.m_requestingNewOutputDevice = false;
optionData.m_stillWriting = false;
optionData.m_downloadAbortedInternally = true;
abortDownload(); // ensure download is aborted
reportFinalDownloadStatus(optionIndex, false, tr("Unable to write to provided output device."));
return false;
}
}
optionData.m_buffer.reset();
}
return true;
}
/*!
* \brief Report that a redirection is available.
*
* Needs to be called when subclassing if there's a redirection available. An option for the
* redirection needs to be added before.
* It is recommendable to specify the index of the new option because the index might be used
* when asking the user for if the redirection should accepted.
*/
void Download::reportRedirectionAvailable(size_t originalOptionIndex)
{
OptionData &optionData = m_optionData.at(originalOptionIndex);
switch (optionData.m_redirectPermission) {
case PermissionStatus::Unknown:
setStatus(DownloadStatus::Waiting);
optionData.m_redirectPermission = PermissionStatus::Asking;
emit redirectionPermissonRequired(this, originalOptionIndex, optionData.m_redirectionOf);
break;
case PermissionStatus::Allowed:
case PermissionStatus::AlwaysAllowed:
usePermission(optionData.m_redirectPermission);
m_range.resetCurrentOffset(); // reset current offset
if (followRedirection(optionData.m_redirectsTo)) {
optionData.m_downloadComplete = false;
setStatus(DownloadStatus::Downloading);
} else {
reportFinalDownloadStatus(originalOptionIndex, false, tr("Follwing redirection failed."));
}
break;
case PermissionStatus::Refused:
case PermissionStatus::AlwaysRefused:
usePermission(optionData.m_redirectPermission);
reportFinalDownloadStatus(originalOptionIndex, true, tr("Download finished, redirection rejected."));
break;
default:;
}
}
/*!
* \brief Reports that authentication credentials are required.
*
* \param optionIndex Specifies the concerning option index. Use Network::InvalidOptionIndex to request
* credentials for initialization.
* \param realm Specifies the realm.
*
* Needs to be called when subclassing to ask for credentials. The download will emit authenticationRequired().
*/
void Download::reportAuthenticationRequired(size_t optionIndex, const QString &realm)
{
AuthenticationCredentials &credentials = optionIndex == InvalidOptionIndex ? m_initAuthData : m_optionData.at(optionIndex).m_authData;
if (!credentials.m_requested) {
credentials.m_requested = true;
emit authenticationRequired(this, optionIndex, realm);
}
}
/*!
* \brief Reports that SSL errors occured.
*
* \param optionIndex Specifies the concerning option index. Use a negative value when the
* errors occured during initialization.
* \param reply Specifies the concerning reply.
* \param sslErrors Specifies which SSL errors occured.
*
* Needs to be called when subclassing if SSL errors occured.
*/
void Download::reportSslErrors(size_t optionIndex, QNetworkReply *reply, const QList<QSslError> &sslErrors)
{
OptionData &optionData = m_optionData.at(optionIndex);
switch (optionData.m_ignoreSslErrorsPermission) {
case PermissionStatus::Unknown:
optionData.m_ignoreSslErrorsPermission = PermissionStatus::Asking;
emit this->sslErrors(this, optionIndex, sslErrors);
break;
case PermissionStatus::Refused:
usePermission(optionData.m_ignoreSslErrorsPermission);
[[fallthrough]];
case PermissionStatus::Allowed:
case PermissionStatus::AlwaysAllowed:
reply->ignoreSslErrors(sslErrors);
break;
default:;
}
}
/*!
* \brief Reports that the download with the specified \a optionIndex is complete.
*
* Needs to be called when subclassing after the download is complete.
*/
void Download::reportDownloadComplete(size_t optionIndex)
{
OptionData &optionData = m_optionData[optionIndex];
optionData.m_downloadComplete = true; // everything downloaded
if (!optionData.m_stillWriting) {
// not writing anymore -> check status and clear
checkStatusAndClear(optionIndex);
} else {
if (optionData.m_outputDevice && optionData.m_outputDeviceReady) {
// there's an output device and the download is still writing buffered data to it
setStatus(DownloadStatus::FinishOuputFile);
} else {
// an output device is needed, the download is currently just waiting
setStatus(DownloadStatus::Waiting);
if (!optionData.m_requestingNewOutputDevice) {
// request an output device if not done yet
optionData.m_requestingNewOutputDevice = true;
emit outputDeviceRequired(this, optionIndex);
}
}
}
}
/*!
* \brief Adds a download URL.
* \param optionName Specifies a name for the option.
* \param url Specifies the URL to be added.
* \param redirectionOf Specifies the index of the original URL if the URL is a redirection; provide Network::InvalidOptionIndex otherwise.
* \return Returns the option index for the new URL.
*
* If the specified \a url already exists, no new option will be appended. The old option will just be updated. The index of the updated option
* is returned in this case.
*
* Needs to be called when subclassing.
*/
size_t Download::addDownloadUrl(const QString &optionName, const QUrl &url, size_t redirectionOf)
{
size_t optionCount = m_optionData.size();
if (redirectionOf == InvalidOptionIndex || redirectionOf >= optionCount) {
redirectionOf = optionCount;
}
// check if the URL is already present
for (size_t index = 0; index < optionCount; ++index) {
OptionData &data = m_optionData[index];
if (data.m_url == url) {
// URL has already been added previously -> just update it
data.m_name = optionName;
data.m_redirectionOf = redirectionOf;
goto end;
}
}
// the URL hasn't been added yet
m_optionData.emplace_back(optionName, url, optionCount, redirectionOf);
m_availableOptionsChanged = true;
end: // update "m_redirectsTo" of original option
if (redirectionOf != optionCount) {
m_optionData.at(redirectionOf).m_redirectsTo = optionCount;
}
return optionCount;
}
/*!
* \brief Changes the download URL with the specified \a optionIndex.
*/
void Download::changeDownloadUrl(size_t optionIndex, const QUrl &value)
{
m_optionData.at(optionIndex).m_url = value;
m_availableOptionsChanged = true;
}
/*!
* \brief Provides an output device.
* \param optionIndex Specifies the index of the option the output device is provided for.
* \param device Specifies the output device.
* \param giveOwnership Specifies whether the ownership is transfered to download.
*
* Use this method to provide an output device after the outputDeviceRequired() signal has
* been emitted.
* If \a device is nullptr the download will be aborted.
*
* In any case: This method does nothing if there is already a ready output device.
*/
void Download::provideOutputDevice(size_t optionIndex, QIODevice *device, bool giveOwnership)
{
OptionData &optionData = m_optionData[optionIndex];
optionData.m_requestingNewOutputDevice = false;
if (!optionData.m_outputDevice || !optionData.m_outputDeviceReady) { // an output device has been requested
finalizeOutputDevice(optionIndex); // finalize last output device
if (device) { // a device has been provided
if (prepareOutputDevice(optionIndex, device, giveOwnership)) { // prepare the output device
if (optionData.m_outputDeviceReady) { // only proceed if output device is ready
optionData.m_hasOutputDeviceOwnership = giveOwnership;
if (!writeBufferToOutputDevice(optionIndex)) { // if there's buffered data write it to the provided device
return; // error already handled within writeBufferToOutputDevice(), so just return here
}
optionData.m_stillWriting = false; // not writing anymore
if (optionData.m_downloadComplete) { // download has ended, too
checkStatusAndClear(optionIndex);
}
}
} // else: the provided device couldn't be prepared, handled within prepareOutputDevice()
} else if (isStarted()) { // no device has been provided -> abort the download
optionData.m_stillWriting = false; // not writing anymore
abortDownload(); // ensure download is aborted
optionData.m_buffer.reset();
reportFinalDownloadStatus(optionIndex, false, tr("No output device provided."));
}
}
}
/*!
* \brief Provides authentication credentials.
*
* Use this method when subclassing to supply authentication credentials after the authenticationRequired() signal has been emitted.
*
* The credentials will be used for the option with the specified \a optionIndex. If \a optionIndex equals Network::InvalidOptionIndex
* the credentials will be used for the initialization.
*
* Does nothing if the authentication credentials (for the specified purpose) haven't been requested.
*/
void Download::provideAuthenticationCredentials(size_t optionIndex, const AuthenticationCredentials &credentials)
{
if (optionIndex == InvalidOptionIndex) {
// credentials are provided for initialization
if (m_initAuthData.m_requested) {
m_initAuthData = credentials;
}
} else {
// credentials are provided for specific option
OptionData &optionData = m_optionData.at(optionIndex);
if (optionData.m_authData.m_requested) {
optionData.m_authData = credentials;
if (!isStarted()) {
// restart the download
//range().resetCurrentOffset();
start();
}
}
}
}
/*!
* \brief Sets the current progress.
*
* This method is only for internal use. When subclassing use reportDownloadProgressUpdate().
*/
void Download::setProgress(qint64 bytesReceived, qint64 bytesToReceive)
{
if (m_bytesReceived != bytesReceived || m_bytesToReceive != bytesToReceive) {
if (bytesReceived > m_bytesReceived && m_time.elapsed()) {
m_speed = (static_cast<double>(bytesReceived - m_bytesReceived) * 0.008) / (static_cast<double>(m_time.restart()) * 0.001);
}
m_bytesReceived = bytesReceived;
m_bytesToReceive = bytesToReceive;
emit progressChanged(this);
}
}
/*!
* \brief Sets whether the download is allowed to overwrite files.
*
* The permission is only set for the option with the specified \a optionIndex.
*
* The permission state PermissionStatus::Asking can not be set using this method
* and will be ignored (this state can are only set internally).
*
* \sa OptionData::overwritePermission()
*/
void Download::setOverwritePermission(size_t optionIndex, PermissionStatus permission)
{
OptionData &data = m_optionData.at(optionIndex);
switch (permission) {
case PermissionStatus::Unknown:
data.m_overwritePermission = PermissionStatus::Unknown;
[[fallthrough]];
case PermissionStatus::Asking:
return; // can not be set here
default:
if (data.m_overwritePermission == PermissionStatus::Asking) {
// the download previously asked for that permission
data.m_overwritePermission = permission;
ensureOutputDeviceIsPrepared(optionIndex);
} else {
// just keep the permission
data.m_overwritePermission = permission;
}
}
}
/*!
* \brief Sets whether the download is allowed to append to a existing file.
*
* The permission states PermissionStatus::Unknown and PermissionStatus::Asking
* can not be set using this method and will be ignored (these states can are only set internally).
*
* \sa OptionData::appendPermission()
*/
void Download::setAppendPermission(size_t optionIndex, PermissionStatus permission)
{
OptionData &data = m_optionData.at(optionIndex);
switch (permission) {
case PermissionStatus::Unknown:
case PermissionStatus::Asking:
return; // can not be set here
default:
if (data.m_appendPermission == PermissionStatus::Asking) {
// the download previously asked for that permission
data.m_appendPermission = permission;
ensureOutputDeviceIsPrepared(optionIndex);
} else {
// just keep the permission
data.m_appendPermission = permission;
}
}
}
/*!
* \brief Sets whether the download is allowed to follow redirections.
*
* The permission states PermissionStatus::Unknown and PermissionStatus::Asking
* can not be set using this method and will be ignored (these states can are only set internally).
*
* \sa OptionData::redirectPermission()
*/
void Download::setRedirectPermission(size_t originalOptionIndex, PermissionStatus permission)
{
OptionData &data = m_optionData.at(originalOptionIndex);
switch (permission) {
case PermissionStatus::Unknown:
case PermissionStatus::Asking:
return; // can not be set here
default:
if (data.m_redirectPermission == PermissionStatus::Asking) {
data.m_redirectPermission = permission;
// the download previously asked for that permission
reportRedirectionAvailable(originalOptionIndex);
} else {
// just keep the permission
data.m_redirectPermission = permission;
}
}
}
/*!
* \brief Sets whether the download is allowed to ignore SSL errors.
*
* The permission states PermissionStatus::Unknown and PermissionStatus::Asking
* can not be set using this method and will be ignored (these states can are only set internally).
*
* \sa OptionData::ignoreSslErrorsPermission()
*/
void Download::setIgnoreSslErrorsPermission(size_t optionIndex, PermissionStatus permission)
{
OptionData &data = m_optionData.at(optionIndex);
switch (permission) {
case PermissionStatus::Unknown:
case PermissionStatus::Asking:
return; // can not be set here
default:
if (data.m_ignoreSslErrorsPermission == PermissionStatus::Asking) {
data.m_ignoreSslErrorsPermission = permission;
switch (data.m_ignoreSslErrorsPermission) {
case PermissionStatus::Allowed:
case PermissionStatus::AlwaysAllowed:
if (networkError() == QNetworkReply::SslHandshakeFailedError) {
start(); // restart the download if it failed because of the SSL error
}
break;
case PermissionStatus::Refused:
case PermissionStatus::AlwaysRefused:
usePermission(data.m_ignoreSslErrorsPermission);
break;
default:;
}
} else {
// just keep the permission
data.m_ignoreSslErrorsPermission = permission;
}
}
}
// docs for signals and pure virtual methods
/*!
* \fn Download::statusChanged()
* \brief Emitted when the status changed.
*/
/*!
* \fn Download::progressChanged()
* \brief Emitted when the progress changed, e. g. new bytes have been received.
*
* The frequency of emmitation can be adjusted using the Download::setProgressUpdateInterval().
*/
/*!
* \fn Download::statusInfo()
* \brief Emitted when the status info changed.
*/
/*!
* \fn Download::overwriteingPermissionRequired()
* \brief Emitted when overwriting an existing file needs to be confirmed.
*
* The permission might be given or refused using the Download::setOverwritePermission() method.
* \sa PermissionStatus
*/
/*!
* \fn Download::appendingPermissionRequired()
* \brief Emitted when appending data to an existing file needs to be confirmed.
*
* The permission might be given or refused using the Download::setAppendPermission() method.
* \sa PermissionStatus
*/
/*!
* \fn Download::redirectionPermissonRequired()
* \brief Emitted when a redirection needs to be confirmed.
*
* The permission might be given or refused using the Download::setRedirectPermission() method.
* \sa PermissionStatus
*/
/*!
* \fn Download::outputDeviceRequired()
* \brief Emitted when an output device needs to be provided.
*
* The output device needs to be provided using the Download::provideOutputDevice() method.
*/
/*!
* \fn Download::authenticationRequired()
* \brief Emitted when authentication credentials needs to be supplied.
*
* The credentials needs to be supplied using the Download::provideAuthenticationCredentials() method.
*/
/*!
* \fn Download::sslErrors()
* \brief Emitted when SSL errors occur.
*/
/*!
* \fn Download::typeName()
* \brief Returns the type of the download as string (e. g. "Youtube Download").
*/
/*!
* \fn Download::doDownload()
* \brief Starts the actual download.
*
* To be implemented when subclassing.
*/
/*!
* \fn Download::abortDownload()
* \brief Aborts the current download.
*
* To be implemented when subclassing.
*/
/*!
* \fn Download::doInit()
* \brief Initiates the download; called before the actual download.
*
* To be implemented when subclassing.
*/
/*!
* \fn Download::checkStatusAndClear()
* \brief Checks the status and frees resources after the download has been completed; called
* after reportDownloadComplete() has been called.
* \remarks Should call reportFinalDownloadStatus().
*
* To be implemented when subclassing.
*/
} // namespace Network