File indexing completed on 2024-04-21 14:43:49

0001 /* GCompris - DownloadManager.h
0002  *
0003  * SPDX-FileCopyrightText: 2014 Holger Kaelberer <holger.k@elberer.de>
0004  *
0005  * Authors:
0006  *   Holger Kaelberer <holger.k@elberer.de>
0007  *
0008  *   SPDX-License-Identifier: GPL-3.0-or-later
0009  */
0010 
0011 #ifndef DOWNLOADMANAGER_H
0012 #define DOWNLOADMANAGER_H
0013 
0014 #include <QCryptographicHash>
0015 #include <QFile>
0016 #include <QNetworkAccessManager>
0017 #include <QNetworkReply>
0018 #include <QMutex>
0019 #include <QString>
0020 #include <QUrl>
0021 #include <QQmlEngine>
0022 #include <QJSEngine>
0023 
0024 namespace GCompris {
0025     Q_NAMESPACE
0026     enum ResourceType {
0027         NONE,
0028         VOICES,
0029         WORDSET,
0030         BACKGROUND_MUSIC,
0031         FULL
0032     };
0033     Q_ENUM_NS(ResourceType);
0034 }
0035 
0036 /**
0037  * @class DownloadManager
0038  * @short A singleton class responsible for downloading, updating and
0039  *        maintaining remote resources.
0040  * @ingroup infrastructure
0041  *
0042  * DownloadManager is responsible for downloading, updating and registering
0043  * additional resources (in Qt's binary .rcc format) used by GCompris.
0044  * It downloads from a upstream URL (ApplicationSettings.downloadServerUrl) to the
0045  * default local writable location. Downloads are based on common relative
0046  * resource paths, such that a URL of the form
0047  *
0048  * <tt>https://\<server-base\>/\<path/to/my/resource.rcc\></tt>
0049  *
0050  * will be downloaded to a local path
0051  *
0052  * <tt>/\<ApplicationSettings::getInstance()->cachePath()\>/\<path/to/my/resource.rcc\></tt>
0053  *
0054  * and registered with a resource root path
0055  *
0056  * <tt>qrc:/\<path/to/my\>/</tt>
0057  *
0058  * Internally resources are uniquely identified by their <em>relative resource
0059  * path</em>
0060  *
0061  * <tt>\<path/to/my/resource.rcc\></tt>
0062  * (e.g. <tt>data3/voices-ogg/voices-en.rcc></tt>)
0063  *
0064  * Downloading and verification of local files is controlled by MD5
0065  * checksums that are expected to be stored in @c Contents files in each
0066  * upstream directory according to the syntax produced by the @c md5sum
0067  * tool. The checksums are used for checking whether a local rcc file is
0068  * up-to-date (to avoid unnecesary rcc downloads) and to verify that the
0069  * transfer was complete. Only valid rcc files (with correct checksums)
0070  * are registered.
0071  *
0072  * A resource file must reference the "/gcompris/data" prefix with
0073  * \<qresource prefix="/gcompris/data"\>. All data are loaded and referenced
0074  * from this prefix. It is possible to check if a data is registered with
0075  * isDataRegistered.
0076  *
0077  * @sa DownloadDialog, ApplicationSettings.downloadServerUrl,
0078  *     ApplicationSettings.isAutomaticDownloadsEnabled,
0079  *     ApplicationSettings.cachePath
0080  */
0081 class DownloadManager : public QObject
0082 {
0083     Q_OBJECT
0084 
0085     friend class DownloadManagerTest;
0086 
0087 private:
0088     DownloadManager(); // prohibit external creation, we are a singleton!
0089     static DownloadManager *_instance; // singleton instance
0090 
0091     /** Container for a full download job */
0092     typedef struct DownloadJob
0093     {
0094         QUrl url; ///< url of the currently running sub-job
0095         QFile file; ///< target file for the currently running sub-job
0096         QNetworkReply *reply = nullptr; ///< reply object for the currently running sub-job
0097         QList<QUrl> queue; ///< q of remaining sub jobs (QList for convenience)
0098         QMap<QString, QString> contents; ///< checksum map for download verification
0099         QList<QUrl> knownContentsUrls; ///< store already tried upstream Contents files (for infinite loop protection)
0100 
0101         GCompris::ResourceType resourceType = GCompris::ResourceType::NONE;
0102         QVariantMap extraInfos = {};
0103 
0104         qint64 bytesReceived = 0;
0105         qint64 bytesTotal = 0;
0106         bool downloadFinished = false;
0107         int downloadResult = 0;
0108         explicit DownloadJob(const GCompris::ResourceType rt, const QVariantMap &extra = {}) :
0109             resourceType(rt), extraInfos(extra) { }
0110     } DownloadJob;
0111 
0112     QList<DownloadJob *> activeJobs; ///< track active jobs to allow for parallel downloads
0113     QMutex jobsMutex; ///< not sure if we need to expect concurrent access, better lockit!
0114 
0115     static const QString contentsFilename;
0116     static const QString localFolderForData;
0117     static const QCryptographicHash::Algorithm hashMethod = QCryptographicHash::Md5;
0118 
0119     QList<QString> registeredResources;
0120     QMutex rcMutex; ///< not sure if we need to expect concurrent access, better lockit!
0121 
0122     QNetworkAccessManager accessManager;
0123     QUrl serverUrl;
0124 
0125     QMap<QString, QString> resourceTypeToLocalFileName; ///< Key constructed from Contents file, values are the filenames in local
0126 
0127     /**
0128      * Get the platform-specific path storing downloaded resources.
0129      *
0130      * Uses QStandardPaths::writableLocation(QStandardPaths::CacheLocation)
0131      * which returns
0132      *   - on desktop linux $HOME/.cache/KDE/gcompris-qt/
0133      *   - on other platforms check <https://doc.qt.io/qt-5/qstandardpaths.html>
0134      *
0135      * @return An absolute path.
0136      */
0137     QString getSystemDownloadPath() const;
0138 
0139     /**
0140      * Get all paths that are used for storing resources.
0141      *
0142      * @returns A list of absolute paths used for storing local resources.
0143      *          The caller should keep the returned list order when looking for
0144      *          resources, for now the lists contains:
0145      *          1. data folder in the application path
0146      *          2. getSystemDownloadPath()
0147      *          3. QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation)/gcompris-qt
0148      *             which is
0149      *             - $HOME/.local/share/gcompris-qt (on linux desktop)
0150      *             - /storage/sdcard0/GCompris (on android)
0151      *          4. [QStandardPaths::standardLocations(QStandardPaths::ApplicationsLocation)]/gcompris-qt
0152      *             which is on GNU/Linux
0153      *             - $HOME/.local/share/KDE/gcompris-qt
0154      *             - $HOME/.local/share/gcompris-qt
0155      *             - $HOME/.local/share/applications/gcompris-qt
0156      *             - /usr/local/share/KDE/gcompris-qt
0157      *             - /usr/share/KDE/gcompris-qt
0158      */
0159     const QStringList getSystemResourcePaths() const;
0160     QString getResourceRootForFilename(const QString &filename) const;
0161     QString getFilenameForUrl(const QUrl &url) const;
0162     QUrl getUrlForFilename(const QString &filename) const;
0163 
0164     QString getLocalSubFolderForData(const GCompris::ResourceType &rt) const;
0165     /**
0166      * Transforms the passed relative path to an absolute resource path of an
0167      * existing .rcc file, honouring the order of the systemResourcePaths
0168      *
0169      * @returns The absolute path of the .rcc file if it exists, QString()
0170      *          otherwise
0171      */
0172     QString getAbsoluteResourcePath(const QString &path) const;
0173 
0174     /**
0175      * Transforms the passed absolute path to a relative resource path if
0176      * possible.
0177      *
0178      * @returns The relative path if it could be generated, QString() otherwise.
0179      */
0180     QString getRelativeResourcePath(const QString &path) const;
0181     QString tempFilenameForFilename(const QString &filename) const;
0182     QString filenameForTempFilename(const QString &tempFilename) const;
0183 
0184     bool checkDownloadRestriction() const;
0185     DownloadJob *getJobByReply(QNetworkReply *r);
0186     DownloadJob *getJobByType_locked(GCompris::ResourceType rt, const QVariantMap &data) const;
0187 
0188     /** Start a new download specified by the passed DownloadJob */
0189     bool download(DownloadJob *job);
0190 
0191     /** Parses upstream Contents file and build checksum map. */
0192     bool parseContents(DownloadJob *job);
0193 
0194     /** Compares the checksum of the file in filename with the contents map in
0195      * the passed DownloadJob */
0196     bool checksumMatches(DownloadJob *job, const QString &filename) const;
0197 
0198     bool registerResourceAbsolute(const QString &filename);
0199 
0200     /** Unregisters the passed resource
0201      *
0202      * Caller must lock rcMutex.
0203      */
0204     void unregisterResource_locked(const QString &filename);
0205     bool isRegistered(const QString &filename) const;
0206 
0207 #if 0
0208     QStringList getLocalResources();
0209 #endif
0210 
0211 private Q_SLOTS:
0212 
0213     /** Handle a finished download.
0214      *
0215      * Called whenever a single download (sub-job) has finished. Responsible
0216      * for iterating over possibly remaining sub-jobs of our DownloadJob.
0217      */
0218     void finishDownload();
0219     void finishAllDownloads(int code);
0220     void downloadReadyRead();
0221     void handleError(QNetworkReply::NetworkError code);
0222 
0223 public:
0224     // public interface:
0225 
0226     /**
0227      * Possible return codes of a finished download
0228      */
0229     enum DownloadFinishedCode {
0230         Success = 0, /**< Download finished successfully */
0231         Error = 1, /**< Download error */
0232         NoChange = 2 /**< Local files are up-to-date, no download was needed */
0233     };
0234 
0235     virtual ~DownloadManager();
0236 
0237     /**
0238      * Registers DownloadManager singleton in the QML engine.
0239      */
0240     static QObject *downloadManagerProvider(QQmlEngine *engine,
0241                                             QJSEngine *scriptEngine);
0242     static DownloadManager *getInstance();
0243 
0244     /**
0245      * Generates a relative voices resources file-path for a given @p locale.
0246      *
0247      * @param locale Locale name string of the form \<language\>_\<country\>.
0248      *
0249      * @returns A relative voices resource path.
0250      */
0251     Q_INVOKABLE QString getVoicesResourceForLocale(const QString &locale) const;
0252 
0253     Q_INVOKABLE QString getResourcePath(/*const GCompris::ResourceType &*/ int rt, const QVariantMap &data) const;
0254 
0255     // @returns A relative background music resource path.
0256     Q_INVOKABLE QString getBackgroundMusicResources() const;
0257     /**
0258      * Checks whether the given relative resource @p path exists locally.
0259      *
0260      * @param path A relative resource path.
0261      */
0262     Q_INVOKABLE bool haveLocalResource(const QString &path) const;
0263 
0264     /**
0265      * For each external dataset type check if there is a Contents file to load
0266      * and load its data if there is one
0267      */
0268     Q_INVOKABLE void initializeAssets();
0269     /**
0270      * Whether any download is currently running.
0271      */
0272     Q_INVOKABLE bool downloadIsRunning() const;
0273 
0274     /**
0275      * Whether the passed relative @p data is registered.
0276      *
0277      * For example, if you have a resource file which loads files under the
0278      * 'words' path like 'words/one.png'. You can call this method with 'words/one.png'
0279      * or with 'words'.
0280      *
0281      * @param data Relative resource path (file or directory).
0282      *
0283      * @sa areVoicesRegistered
0284      */
0285     Q_INVOKABLE bool isDataRegistered(const QString &data) const;
0286 
0287     /**
0288      * Whether voices for the locale passed in parameter are registered.
0289      *
0290      * @sa isDataRegistered
0291      */
0292     Q_INVOKABLE bool areVoicesRegistered(const QString &locale) const;
0293 
0294     /**
0295      * Registers a rcc resource file given by a relative resource path
0296      *
0297      * @param filename  Relative resource path.
0298      */
0299     Q_INVOKABLE bool registerResource(const QString &filename);
0300 
0301 public Q_SLOTS:
0302 
0303     /** Emitted when a download in progressing.
0304      *
0305      * Job is retrieved using sender() method.
0306      *
0307      * @param bytesReceived Downloaded bytes for this job.
0308      * @param bytesTotal Total bytes to download for this job.
0309      */
0310     void downloadInProgress(qint64 bytesReceived, qint64 bytesTotal);
0311 
0312     /**
0313      * Updates a resource @p path from the upstream server unless prohibited
0314      * by the settings and registers it if possible.
0315      *
0316      * If not prohibited by the setting 'isAutomaticDownloadsEnabled' checks
0317      * for an available upstream resource specified by @p path.
0318      * If the corresponding local resource does not exist or is out of date,
0319      * the resource is downloaded and registered.
0320      *
0321      * If automatic downloads/updates are prohibited and a local copy of the
0322      * specified resource exists, it is registered.
0323      *
0324      * @param path A relative resource path.
0325      *
0326      * @returns success
0327      */
0328     Q_INVOKABLE bool updateResource(/*const GCompris::ResourceType &*/ int rt, const QVariantMap &extra);
0329 
0330     /**
0331      * Download a resource specified by the relative resource @p path and
0332      * register it if possible.
0333      *
0334      * If a corresponding local resource exists, an update will only be
0335      * downloaded if it is not up-to-date according to checksum comparison.
0336      * Whenever at the end we have a valid .rcc file it will be registered.
0337      *
0338      * @param path A relative resource path.
0339      *
0340      * @returns success
0341      */
0342     Q_INVOKABLE bool downloadResource(int rt, const QVariantMap &extra = {});
0343 
0344     /**
0345      * Shutdown DownloadManager instance.
0346      *
0347      * Aborts all currently running downloads.
0348      */
0349     Q_INVOKABLE void shutdown();
0350 
0351     /**
0352      * Abort all currently running downloads.
0353      */
0354     Q_INVOKABLE void abortDownloads();
0355 
0356 #if 0
0357     Q_INVOKABLE bool checkForUpdates();  // might be helpful later with other use-cases!
0358     Q_INVOKABLE void registerLocalResources();
0359 #endif
0360 
0361 Q_SIGNALS:
0362 
0363     /** Emitted when a download error occurs.
0364      *
0365      * @param code enum NetworkError code.
0366      * @param msg Error string.
0367      */
0368     void error(int code, const QString &msg);
0369 
0370     /** Emitted when a download has started.
0371      *
0372      * @param resource Relative resource path of the started download.
0373      */
0374     void downloadStarted(const QString &resource);
0375 
0376     /** Emitted during a running download.
0377      *
0378      * All values refer to the currently active sub-job.
0379      *
0380      * @param bytesReceived Downloaded bytes.
0381      * @param bytesTotal Total bytes to download.
0382      */
0383     void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);
0384 
0385     /** Emitted when a download has finished.
0386      *
0387      * Also emitted in error cases.
0388      *
0389      * @param code DownloadFinishedCode. FIXME: when using DownloadFinishedCode
0390      *             instead of int the code will not be passed to the QML layer,
0391      *             use QENUMS?
0392      */
0393     void downloadFinished(int code);
0394 
0395     /** Emitted when all downloads have finished.
0396      *
0397      * If all downloads are successful, code is Success.
0398      * Else if at least one download is in error, code is Error.
0399      */
0400     void allDownloadsFinished(int code);
0401 
0402     /** Emitted when a resource has been registered.
0403      *
0404      * @param resource Relative resource path of the registered resource.
0405      *
0406      * @sa voicesRegistered
0407      */
0408     void resourceRegistered(const QString &resource);
0409 
0410     /** Emitted when voices resources for current locale have been registered.
0411      *
0412      * Convenience signal and special case of resourceRegistered.
0413      *
0414      * @sa resourceRegistered
0415      */
0416     void voicesRegistered();
0417 
0418     /** Emitted when background music has been registered.
0419      *
0420      * Convenience signal and special case of resourceRegistered.
0421      *
0422      * @sa resourceRegistered
0423      */
0424     void backgroundMusicRegistered();
0425 };
0426 
0427 #endif /* DOWNLOADMANAGER_H */