diff --git a/JGE/include/Downloader.h b/JGE/include/Downloader.h new file mode 100644 index 000000000..f03a6f998 --- /dev/null +++ b/JGE/include/Downloader.h @@ -0,0 +1,128 @@ +#ifndef DOWNLOADER_H +#define DOWNLOADER_H + +//------------------------------------------------------------------------------------- +// +// This class handles download of remote resources (any kind of file) +// All the resources are stored locally in the userPath +// For every resources, the downloader verifies if the resource was modifed +// on the server before downloading the update. The Downloader maintains a catalogue +// of resource downloaded to be able to check if they need to be updated. +// +// The interface can be used completly synchronously by the application and some +// context or message loop is needed in the implementation of this interface +// +// Note that the Downloader could in theory by implemented on top of JNetwork. +// +//------------------------------------------------------------------------------------- +#include +#include +#include +#include +#include +#include "Threading.h" +#ifdef QT_CONFIG +#include +#include +#include +#include +#endif + +using namespace std; + +class DownloadRequest + #ifdef QT_CONFIG + : public QObject + #endif +{ +#ifdef QT_CONFIG + Q_OBJECT + +private slots: +#endif + void fileDownloaded(); + void downloadProgress(qint64 bytesReceived, qint64 bytesTotal); + +#ifdef QT_CONFIG +signals: + void percentChanged(int percent); + void statusChanged(int); +#endif + +public: + typedef enum { + NOT_PRESENT, + DOWNLOADING, + DOWNLOADED, + DOWNLOAD_ERROR + } DownloadStatus; + +protected: + string mLocalPath; + string mRemoteResourceURL; + // previous one is the original, next one can change after redirection + string mRequestedRemoteResourceURL; + string mETag; + DownloadStatus mDownloadStatus; + bool mUpgradeAvailable; + uint64_t mTotalSize; + uint64_t mCurrentSize; + ofstream mFile; +#ifdef QT_CONFIG + QNetworkReply* mNetworkReply; + static QNetworkAccessManager networkAccessManager; +#endif + + +public: + DownloadRequest(string localPath="", + string remoteResourceURL="", + string ETag = "", + DownloadStatus downloadStatus=NOT_PRESENT, + uint64_t totalSize = 0, + uint64_t currentSize = 0); + ~DownloadRequest(); + static bool NetworkIsAccessible(); + + string getTempLocalPath() const { return (mLocalPath+".tmp"); }; + string getLocalPath() const { return mLocalPath; }; + string getRemoteResource() const { return mRemoteResourceURL; }; + string getETag() const { return mETag; }; + void startGet(); + void startHead(); + DownloadStatus getDownloadStatus() const { return mDownloadStatus; }; + bool upgradeAvailable() const { return mUpgradeAvailable; }; + void getSizes(uint64_t& totalSize, uint64_t¤tSize) { + totalSize = mTotalSize; + currentSize = mCurrentSize; + }; + + friend ostream& operator<<(ostream& out, const DownloadRequest& d); + friend istream& operator>>(istream&, DownloadRequest&); + friend class Downloader; +}; + + +class Downloader +{ +protected: + Downloader(string globalRemoteURL="", string localCacheRecords=""); + virtual ~Downloader(); + static Downloader* mInstance; + string mGlobalRemoteURL; + string mLocalCacheRecords; + boost::mutex mMutex; + map mRequestMap; + +public: + static Downloader* GetInstance(); + static void Release(); + + void Update(); + DownloadRequest* Get(string localPath, string remoteResourceURL=""); + + friend ostream& operator<<(ostream& out, const Downloader& d); + friend istream& operator>>(istream&, Downloader&); +}; + +#endif // DOWNLOADER_H diff --git a/JGE/include/JFileSystem.h b/JGE/include/JFileSystem.h index 6c658a8b3..25cdc7551 100644 --- a/JGE/include/JFileSystem.h +++ b/JGE/include/JFileSystem.h @@ -128,6 +128,7 @@ public: bool readIntoString(const string & FilePath, string & target); bool openForWrite(ofstream & File, const string & FilePath, ios_base::openmode mode = ios_base::out ); bool Rename(string from, string to); + bool Remove(string aFile); //Returns true if strFilename exists somewhere in the fileSystem bool FileExists(const string& strFilename); @@ -163,4 +164,4 @@ protected: -#endif \ No newline at end of file +#endif diff --git a/JGE/src/Downloader.cpp b/JGE/src/Downloader.cpp new file mode 100644 index 000000000..624c1b1e0 --- /dev/null +++ b/JGE/src/Downloader.cpp @@ -0,0 +1,298 @@ +#include "DebugRoutines.h" +#include "JFileSystem.h" +#include "Downloader.h" + +#define RECORDS_DEFAULT_FILE "cache/records.txt" + +#ifdef QT_CONFIG +QNetworkAccessManager DownloadRequest::networkAccessManager; +#endif + +DownloadRequest::DownloadRequest(string localPath, + string remoteResourceURL, + string ETag, + DownloadStatus downloadStatus, + uint64_t totalSize, + uint64_t currentSize): + mLocalPath(localPath), + mRemoteResourceURL(remoteResourceURL), + mRequestedRemoteResourceURL(remoteResourceURL), + mETag(ETag), + mDownloadStatus(downloadStatus), + mUpgradeAvailable(false), + mTotalSize(totalSize), + mCurrentSize(currentSize) +{ +} + +DownloadRequest::~DownloadRequest() +{ +} + +void DownloadRequest::startHead() +{ +#ifdef QT_CONFIG + QNetworkRequest request(QUrl(QString(mRequestedRemoteResourceURL.c_str()))); + request.setRawHeader("If-None-Match", "e42fe0b592d2b34965e2e92742b0d9c7");//mETag.c_str()); + mNetworkReply = networkAccessManager.head(request); + connect(mNetworkReply, SIGNAL(finished()), SLOT(fileDownloaded())); +#endif +} + +void DownloadRequest::startGet() +{ +#ifdef QT_CONFIG + mNetworkReply = networkAccessManager.get(QNetworkRequest(QUrl(QString(mRequestedRemoteResourceURL.c_str())))); +#endif + mFile.close(); + JFileSystem::GetInstance()->Remove(getTempLocalPath()); + JFileSystem::GetInstance()->openForWrite(mFile, getTempLocalPath()); +#ifdef QT_CONFIG + connect(mNetworkReply, SIGNAL(downloadProgress(qint64, qint64)), + SLOT(downloadProgress(qint64, qint64))); + connect(mNetworkReply, SIGNAL(finished()), SLOT(fileDownloaded())); +#endif +} + +void DownloadRequest::fileDownloaded() +{ + do { + QByteArray eTagByteArray = mNetworkReply->rawHeader("ETag"); + if(!eTagByteArray.isEmpty()) { + string oldETag = mETag; + mETag = QString(eTagByteArray).toStdString(); + if(oldETag!="" && oldETag != mETag) + mUpgradeAvailable = true; + } + + // let's check some error + if(mNetworkReply->error() != QNetworkReply::NoError) { + DebugTrace(mNetworkReply->errorString().toStdString()); + mDownloadStatus = DownloadRequest::DOWNLOAD_ERROR; + mFile.close(); + JFileSystem::GetInstance()->Remove(getTempLocalPath()); + break; + } + + // check if we're getting redirected + QVariant redirectionTarget = mNetworkReply->attribute(QNetworkRequest::RedirectionTargetAttribute); + if (!redirectionTarget.isNull()) { + QUrl newUrl = QUrl(mRequestedRemoteResourceURL.c_str()).resolved(redirectionTarget.toUrl()); + DebugTrace(string("Redirect to ")+ newUrl.toString().toStdString()); + + mRequestedRemoteResourceURL = newUrl.toString().toStdString(); + mNetworkReply->deleteLater(); + if(mFile.is_open()) + startGet(); + else + startHead(); + return; + } + + if(mFile.is_open()) + { + QByteArray byteArray = mNetworkReply->readAll(); + mFile.write(byteArray.constData(), byteArray.size()); + mFile.close(); + if(!JFileSystem::GetInstance()->Rename(getTempLocalPath(), mLocalPath)) { + mDownloadStatus = DownloadRequest::DOWNLOAD_ERROR; + break; + } + } + mDownloadStatus = DownloadRequest::DOWNLOADED; + } while(0); + + Downloader::GetInstance()->Update(); + mNetworkReply->deleteLater(); + + emit statusChanged((int)mDownloadStatus); +} + +void DownloadRequest::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) +{ + QByteArray byteArray = mNetworkReply->readAll(); + mFile.write(byteArray.constData(), byteArray.size()); + mCurrentSize = bytesReceived; + mTotalSize = bytesTotal; + int percent = 0; + if(bytesTotal) + percent = (bytesReceived/bytesTotal)*100; + emit percentChanged(percent); +} + +Downloader* Downloader::mInstance = 0; + +Downloader::Downloader(string globalRemoteURL, string localcacheRecords): + mGlobalRemoteURL(globalRemoteURL), + mLocalCacheRecords(localcacheRecords) +{ + JFileSystem::GetInstance()->MakeDir("cache"); + + izfstream downloadRecords; + if(mLocalCacheRecords.empty()) + mLocalCacheRecords = RECORDS_DEFAULT_FILE; + if(JFileSystem::GetInstance()->openForRead(downloadRecords, mLocalCacheRecords)) + {// File exists, let's read it. + downloadRecords >> (*this); + } + JFileSystem::GetInstance()->CloseFile(); +} + +Downloader::~Downloader() +{ + map::iterator ite; + for(ite = mRequestMap.begin(); ite != mRequestMap.end(); ite++) + { + delete (*ite).second; + } + mRequestMap.erase(mRequestMap.begin(), mRequestMap.end()); +} + +Downloader* Downloader::GetInstance() +{ + if(!mInstance) + { + mInstance = new Downloader(); + } + return mInstance; +} + +void Downloader::Release() +{ + if(mInstance) + { + delete mInstance; + mInstance = 0; + } +} + +bool DownloadRequest::NetworkIsAccessible() +{ + bool result = false; + +#ifdef QT_CONFIG + networkAccessManager.setNetworkAccessible(QNetworkAccessManager::Accessible); + result = networkAccessManager.networkAccessible(); +#endif + + return result; +} + +DownloadRequest* Downloader::Get(string localPath, string remoteResourceURL) +{ + map::iterator ite = mRequestMap.find(localPath); + if(ite == mRequestMap.end()) + { // request does not exist, let's create it + DownloadRequest* request = new DownloadRequest(localPath, remoteResourceURL); + std::pair::iterator,bool> ret; + ret = mRequestMap.insert ( std::pair(localPath, request) ); + if (ret.second==false) { + DebugTrace("Downloader::Get Error inserting request in Map"); + return 0; + } + ite = ret.first; + } + + // Now, we can check the server + if((*ite).second->getDownloadStatus() == DownloadRequest::NOT_PRESENT || + (*ite).second->upgradeAvailable()) + { // File is not here or an update is available, let's get it + (*ite).second->startGet(); + (*ite).second->mDownloadStatus = DownloadRequest::DOWNLOADING; + } + else if ((*ite).second->getDownloadStatus() == DownloadRequest::DOWNLOADED) + { // File is here, let's check if there is some update without blocking the playback + (*ite).second->startHead(); + } + + return (*ite).second; +} + +void Downloader::Update() +{ + ofstream downloadRecords; + if(JFileSystem::GetInstance()->openForWrite(downloadRecords, mLocalCacheRecords)) + { + downloadRecords << (*this); + } + downloadRecords.close(); +} + +ostream& operator<<(ostream& out, const DownloadRequest& d) +{ +// HEAD request fails, so this line erase cache record after upgrade check :( +// if(d.getDownloadStatus() == DownloadRequest::DOWNLOADED) + { + out << "localPath=" << d.mLocalPath << endl; + out << "remoteResource=" << d.mRemoteResourceURL << endl; + out << "ETag=" << d.mETag << endl; + out << "upgradeAvailable=" << d.mUpgradeAvailable <>(istream& in, DownloadRequest& d) +{ + string s; + + while(std::getline(in, s)) + { + size_t limiter = s.find("="); + string areaS; + if (limiter != string::npos) + { + areaS = s.substr(0, limiter); + if (areaS.compare("localPath") == 0) + { + d.mLocalPath = s.substr(limiter + 1); + } + else if (areaS.compare("remoteResource") == 0) + { + d.mRemoteResourceURL = s.substr(limiter + 1); + d.mRequestedRemoteResourceURL = d.mRemoteResourceURL; + } + else if (areaS.compare("ETag") == 0) + { + d.mETag = s.substr(limiter + 1); + d.mDownloadStatus = DownloadRequest::DOWNLOADED; + } + else if (areaS.compare("upgradeAvailable") == 0) + { + d.mUpgradeAvailable = (bool)atoi(s.substr(limiter + 1).c_str()); + break; + } + } + } + + return in; +} + +ostream& operator<<(ostream& out, const Downloader& d) +{ + map::const_iterator ite; + for(ite = d.mRequestMap.begin(); ite != d.mRequestMap.end(); ite++) + { + out << (*(*ite).second) << endl; + } + return out; +} + +istream& operator>>(istream& in, Downloader& d) +{ + while(!in.eof()) + { + DownloadRequest* downloadRequest = new DownloadRequest(); + in >> (*downloadRequest); + + if(!downloadRequest->getLocalPath().empty() && + !downloadRequest->getRemoteResource().empty() && + !downloadRequest->getETag().empty()) { + d.mRequestMap[downloadRequest->getLocalPath()] = downloadRequest; + } else { + delete downloadRequest; + } + } + return in; +} + diff --git a/JGE/src/JFileSystem.cpp b/JGE/src/JFileSystem.cpp index 03480ae1f..7d190c562 100644 --- a/JGE/src/JFileSystem.cpp +++ b/JGE/src/JFileSystem.cpp @@ -557,7 +557,13 @@ bool JFileSystem::Rename(string _from, string _to) string from = mUserFSPath + _from; string to = mUserFSPath + _to; std::remove(to.c_str()); - return rename(from.c_str(), to.c_str()) ? true: false; + return (rename(from.c_str(), to.c_str()) == 0); +} + +bool JFileSystem::Remove(string aFile) +{ + string toRemove = mUserFSPath + aFile; + return (std::remove(toRemove.c_str()) == 0); } int JFileSystem::GetFileSize(izfstream & file) diff --git a/JGE/src/Qtmain.cpp b/JGE/src/Qtmain.cpp index f21d6bdb7..38a2b1d31 100644 --- a/JGE/src/Qtmain.cpp +++ b/JGE/src/Qtmain.cpp @@ -9,11 +9,11 @@ #include #include "qmlapplicationviewer.h" #endif //QT_WIDGET -#include "filedownloader.h" +#include "Downloader.h" #include "GameApp.h" #include "corewrapper.h" -QWidget* g_glwidget = NULL; +WagicCore* g_glwidget = NULL; static const struct { LocalKeySym keysym; JButton keycode; } gDefaultBindings[] = { @@ -69,18 +69,22 @@ int main(int argc, char* argv[]) #endif //QT_WIDGET - if(argc >= 2 && string(argv[1]) == "testsuite") - { - int result = 0; - result += WagicCore::runTestSuite(); - return result; - } - app->setApplicationName(WagicCore::getApplicationName()); - FileDownloader fileDownloader(USERDIR, WAGIC_RESOURCE_NAME); + Downloader*downloader = Downloader::GetInstance(); + DownloadRequest* downloadRequest = downloader->Get( + "core.zip", + "https://github.com/WagicProject/wagic/releases/download/latest-master/Wagic-core-288.zip" + ); #ifdef QT_WIDGET g_glwidget = new WagicCore(); - g_glwidget->connect(&fileDownloader, SIGNAL(finished(int)), SLOT(start(int))); + if(downloadRequest->getDownloadStatus() == DownloadRequest::DOWNLOADED) + { + g_glwidget->start(0); + } + else + { + g_glwidget->connect(downloadRequest, SIGNAL(statusChanged(int)), SLOT(start(int))); + } #else qmlRegisterType("CustomComponents", 1, 0, "WagicCore"); diff --git a/projects/mtg/wagic.pri b/projects/mtg/wagic.pri index 88733dbac..7ed0465dc 100644 --- a/projects/mtg/wagic.pri +++ b/projects/mtg/wagic.pri @@ -283,6 +283,7 @@ HEADERS += \ # JGE, could probably be moved outside SOURCES += \ + ../../JGE/src/Downloader.cpp\ ../../JGE/src/Encoding.cpp\ ../../JGE/src/JAnimator.cpp\ ../../JGE/src/JApp.cpp\ @@ -320,6 +321,7 @@ SOURCES += \ ../../JGE/src/zipFS/zstream.cpp HEADERS += \ + ../../JGE/include/Downloader.h\ ../../JGE/include/Threading.h\ ../../JGE/include/decoder_prx.h\ ../../JGE/include/DebugRoutines.h\