#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 #include #include #include #include #include #include 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. * *

Methods to be implemented when subclassing:

* - 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. * *

Methods to be called when subclassing:

* - 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. * *

Signals to be handled when using the class:

* - 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::m_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 (m_defaultUserAgent < 0 || m_defaultUserAgent > 6) { scrambleDefaultUserAgent(); } return agents[m_defaultUserAgent]; } /*! * \brief Scrambles the default user agent. * \sa defaultUserAgent() */ void Download::scrambleDefaultUserAgent() { random_device rd; default_random_engine engine(rd()); uniform_int_distribution distribution(0, 6); m_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 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_shiftRemainingTime(TimeSpan()) , m_statusInfo(QString()) , m_time(QTime()) , 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(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(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(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 &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(bytesReceived - m_bytesReceived) * 0.008) / (static_cast(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