NOISSUE reorganize and document libraries
This commit is contained in:
		
							
								
								
									
										105
									
								
								api/logic/net/ByteArrayDownload.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								api/logic/net/ByteArrayDownload.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,105 @@ | ||||
| /* Copyright 2013-2015 MultiMC Contributors | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
|  | ||||
| #include "ByteArrayDownload.h" | ||||
| #include "Env.h" | ||||
| #include <QDebug> | ||||
|  | ||||
| ByteArrayDownload::ByteArrayDownload(QUrl url) : NetAction() | ||||
| { | ||||
| 	m_url = url; | ||||
| 	m_status = Job_NotStarted; | ||||
| } | ||||
|  | ||||
| void ByteArrayDownload::start() | ||||
| { | ||||
| 	qDebug() << "Downloading " << m_url.toString(); | ||||
| 	QNetworkRequest request(m_url); | ||||
| 	request.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Uncached)"); | ||||
| 	auto worker = ENV.qnam(); | ||||
| 	QNetworkReply *rep = worker->get(request); | ||||
|  | ||||
| 	m_reply.reset(rep); | ||||
| 	connect(rep, SIGNAL(downloadProgress(qint64, qint64)), | ||||
| 			SLOT(downloadProgress(qint64, qint64))); | ||||
| 	connect(rep, SIGNAL(finished()), SLOT(downloadFinished())); | ||||
| 	connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), | ||||
| 			SLOT(downloadError(QNetworkReply::NetworkError))); | ||||
| 	connect(rep, SIGNAL(readyRead()), SLOT(downloadReadyRead())); | ||||
| } | ||||
|  | ||||
| void ByteArrayDownload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) | ||||
| { | ||||
| 	m_total_progress = bytesTotal; | ||||
| 	m_progress = bytesReceived; | ||||
| 	emit netActionProgress(m_index_within_job, bytesReceived, bytesTotal); | ||||
| } | ||||
|  | ||||
| void ByteArrayDownload::downloadError(QNetworkReply::NetworkError error) | ||||
| { | ||||
| 	// error happened during download. | ||||
| 	qCritical() << "Error getting URL:" << m_url.toString().toLocal8Bit() | ||||
| 				 << "Network error: " << error; | ||||
| 	m_status = Job_Failed; | ||||
| 	m_errorString = m_reply->errorString(); | ||||
| } | ||||
|  | ||||
| void ByteArrayDownload::downloadFinished() | ||||
| { | ||||
| 	QVariant redirect = m_reply->header(QNetworkRequest::LocationHeader); | ||||
| 	QString redirectURL; | ||||
| 	if(redirect.isValid()) | ||||
| 	{ | ||||
| 		redirectURL = redirect.toString(); | ||||
| 	} | ||||
| 	// FIXME: This is a hack for https://bugreports.qt-project.org/browse/QTBUG-41061 | ||||
| 	else if(m_reply->hasRawHeader("Location")) | ||||
| 	{ | ||||
| 		auto data = m_reply->rawHeader("Location"); | ||||
| 		if(data.size() > 2 && data[0] == '/' && data[1] == '/') | ||||
| 			redirectURL = m_reply->url().scheme() + ":" + data; | ||||
| 	} | ||||
| 	if (!redirectURL.isEmpty()) | ||||
| 	{ | ||||
| 		m_url = QUrl(redirect.toString()); | ||||
| 		qDebug() << "Following redirect to " << m_url.toString(); | ||||
| 		start(); | ||||
| 		return; | ||||
| 	} | ||||
|  | ||||
| 	// if the download succeeded | ||||
| 	if (m_status != Job_Failed) | ||||
| 	{ | ||||
| 		// nothing went wrong... | ||||
| 		m_status = Job_Finished; | ||||
| 		m_data = m_reply->readAll(); | ||||
| 		m_content_type = m_reply->header(QNetworkRequest::ContentTypeHeader).toString(); | ||||
| 		m_reply.reset(); | ||||
| 		emit succeeded(m_index_within_job); | ||||
| 		return; | ||||
| 	} | ||||
| 	// else the download failed | ||||
| 	else | ||||
| 	{ | ||||
| 		m_reply.reset(); | ||||
| 		emit failed(m_index_within_job); | ||||
| 		return; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void ByteArrayDownload::downloadReadyRead() | ||||
| { | ||||
| 	// ~_~ | ||||
| } | ||||
							
								
								
									
										48
									
								
								api/logic/net/ByteArrayDownload.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								api/logic/net/ByteArrayDownload.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | ||||
| /* Copyright 2013-2015 MultiMC Contributors | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
|  | ||||
| #pragma once | ||||
| #include "NetAction.h" | ||||
|  | ||||
| #include "multimc_logic_export.h" | ||||
|  | ||||
| typedef std::shared_ptr<class ByteArrayDownload> ByteArrayDownloadPtr; | ||||
| class MULTIMC_LOGIC_EXPORT ByteArrayDownload : public NetAction | ||||
| { | ||||
| 	Q_OBJECT | ||||
| public: | ||||
| 	ByteArrayDownload(QUrl url); | ||||
| 	static ByteArrayDownloadPtr make(QUrl url) | ||||
| 	{ | ||||
| 		return ByteArrayDownloadPtr(new ByteArrayDownload(url)); | ||||
| 	} | ||||
|     virtual ~ByteArrayDownload() {}; | ||||
| public: | ||||
| 	/// if not saving to file, downloaded data is placed here | ||||
| 	QByteArray m_data; | ||||
|  | ||||
| 	QString m_errorString; | ||||
|  | ||||
| public | ||||
| slots: | ||||
| 	virtual void start(); | ||||
|  | ||||
| protected | ||||
| slots: | ||||
| 	void downloadProgress(qint64 bytesReceived, qint64 bytesTotal); | ||||
| 	void downloadError(QNetworkReply::NetworkError error); | ||||
| 	void downloadFinished(); | ||||
| 	void downloadReadyRead(); | ||||
| }; | ||||
							
								
								
									
										192
									
								
								api/logic/net/CacheDownload.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										192
									
								
								api/logic/net/CacheDownload.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,192 @@ | ||||
| /* Copyright 2013-2015 MultiMC Contributors | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
|  | ||||
| #include "CacheDownload.h" | ||||
|  | ||||
| #include <QCryptographicHash> | ||||
| #include <QFileInfo> | ||||
| #include <QDateTime> | ||||
| #include <QDebug> | ||||
| #include "Env.h" | ||||
| #include <FileSystem.h> | ||||
|  | ||||
| CacheDownload::CacheDownload(QUrl url, MetaEntryPtr entry) | ||||
| 	: NetAction(), md5sum(QCryptographicHash::Md5) | ||||
| { | ||||
| 	m_url = url; | ||||
| 	m_entry = entry; | ||||
| 	m_target_path = entry->getFullPath(); | ||||
| 	m_status = Job_NotStarted; | ||||
| } | ||||
|  | ||||
| void CacheDownload::start() | ||||
| { | ||||
| 	m_status = Job_InProgress; | ||||
| 	if (!m_entry->isStale()) | ||||
| 	{ | ||||
| 		m_status = Job_Finished; | ||||
| 		emit succeeded(m_index_within_job); | ||||
| 		return; | ||||
| 	} | ||||
| 	// create a new save file | ||||
| 	m_output_file.reset(new QSaveFile(m_target_path)); | ||||
|  | ||||
| 	// if there already is a file and md5 checking is in effect and it can be opened | ||||
| 	if (!FS::ensureFilePathExists(m_target_path)) | ||||
| 	{ | ||||
| 		qCritical() << "Could not create folder for " + m_target_path; | ||||
| 		m_status = Job_Failed; | ||||
| 		emit failed(m_index_within_job); | ||||
| 		return; | ||||
| 	} | ||||
| 	if (!m_output_file->open(QIODevice::WriteOnly)) | ||||
| 	{ | ||||
| 		qCritical() << "Could not open " + m_target_path + " for writing"; | ||||
| 		m_status = Job_Failed; | ||||
| 		emit failed(m_index_within_job); | ||||
| 		return; | ||||
| 	} | ||||
| 	qDebug() << "Downloading " << m_url.toString(); | ||||
| 	QNetworkRequest request(m_url); | ||||
|  | ||||
| 	// check file consistency first. | ||||
| 	QFile current(m_target_path); | ||||
| 	if(current.exists() && current.size() != 0) | ||||
| 	{ | ||||
| 		if (m_entry->getRemoteChangedTimestamp().size()) | ||||
| 			request.setRawHeader(QString("If-Modified-Since").toLatin1(), | ||||
| 								m_entry->getRemoteChangedTimestamp().toLatin1()); | ||||
| 		if (m_entry->getETag().size()) | ||||
| 			request.setRawHeader(QString("If-None-Match").toLatin1(), m_entry->getETag().toLatin1()); | ||||
| 	} | ||||
|  | ||||
| 	request.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Cached)"); | ||||
|  | ||||
| 	auto worker = ENV.qnam(); | ||||
| 	QNetworkReply *rep = worker->get(request); | ||||
|  | ||||
| 	m_reply.reset(rep); | ||||
| 	connect(rep, SIGNAL(downloadProgress(qint64, qint64)), | ||||
| 			SLOT(downloadProgress(qint64, qint64))); | ||||
| 	connect(rep, SIGNAL(finished()), SLOT(downloadFinished())); | ||||
| 	connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), | ||||
| 			SLOT(downloadError(QNetworkReply::NetworkError))); | ||||
| 	connect(rep, SIGNAL(readyRead()), SLOT(downloadReadyRead())); | ||||
| } | ||||
|  | ||||
| void CacheDownload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) | ||||
| { | ||||
| 	m_total_progress = bytesTotal; | ||||
| 	m_progress = bytesReceived; | ||||
| 	emit netActionProgress(m_index_within_job, bytesReceived, bytesTotal); | ||||
| } | ||||
|  | ||||
| void CacheDownload::downloadError(QNetworkReply::NetworkError error) | ||||
| { | ||||
| 	// error happened during download. | ||||
| 	qCritical() << "Failed " << m_url.toString() << " with reason " << error; | ||||
| 	m_status = Job_Failed; | ||||
| } | ||||
| void CacheDownload::downloadFinished() | ||||
| { | ||||
| 	QVariant redirect = m_reply->header(QNetworkRequest::LocationHeader); | ||||
| 	QString redirectURL; | ||||
| 	if(redirect.isValid()) | ||||
| 	{ | ||||
| 		redirectURL = redirect.toString(); | ||||
| 	} | ||||
| 	// FIXME: This is a hack for https://bugreports.qt-project.org/browse/QTBUG-41061 | ||||
| 	else if(m_reply->hasRawHeader("Location")) | ||||
| 	{ | ||||
| 		auto data = m_reply->rawHeader("Location"); | ||||
| 		if(data.size() > 2 && data[0] == '/' && data[1] == '/') | ||||
| 			redirectURL = m_reply->url().scheme() + ":" + data; | ||||
| 	} | ||||
| 	if (!redirectURL.isEmpty()) | ||||
| 	{ | ||||
| 		m_url = QUrl(redirect.toString()); | ||||
| 		qDebug() << "Following redirect to " << m_url.toString(); | ||||
| 		start(); | ||||
| 		return; | ||||
| 	} | ||||
|  | ||||
| 	// if the download succeeded | ||||
| 	if (m_status == Job_Failed) | ||||
| 	{ | ||||
| 		m_output_file->cancelWriting(); | ||||
| 		m_reply.reset(); | ||||
| 		emit failed(m_index_within_job); | ||||
| 		return; | ||||
| 	} | ||||
|  | ||||
| 	// if we wrote any data to the save file, we try to commit the data to the real file. | ||||
| 	if (wroteAnyData) | ||||
| 	{ | ||||
| 		// nothing went wrong... | ||||
| 		if (m_output_file->commit()) | ||||
| 		{ | ||||
| 			m_status = Job_Finished; | ||||
| 			m_entry->setMD5Sum(md5sum.result().toHex().constData()); | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			qCritical() << "Failed to commit changes to " << m_target_path; | ||||
| 			m_output_file->cancelWriting(); | ||||
| 			m_reply.reset(); | ||||
| 			m_status = Job_Failed; | ||||
| 			emit failed(m_index_within_job); | ||||
| 			return; | ||||
| 		} | ||||
| 	} | ||||
| 	else | ||||
| 	{ | ||||
| 		m_status = Job_Finished; | ||||
| 	} | ||||
|  | ||||
| 	// then get rid of the save file | ||||
| 	m_output_file.reset(); | ||||
|  | ||||
| 	QFileInfo output_file_info(m_target_path); | ||||
|  | ||||
| 	m_entry->setETag(m_reply->rawHeader("ETag").constData()); | ||||
| 	if (m_reply->hasRawHeader("Last-Modified")) | ||||
| 	{ | ||||
| 		m_entry->setRemoteChangedTimestamp(m_reply->rawHeader("Last-Modified").constData()); | ||||
| 	} | ||||
| 	m_entry->setLocalChangedTimestamp(output_file_info.lastModified().toUTC().toMSecsSinceEpoch()); | ||||
| 	m_entry->setStale(false); | ||||
| 	ENV.metacache()->updateEntry(m_entry); | ||||
|  | ||||
| 	m_reply.reset(); | ||||
| 	emit succeeded(m_index_within_job); | ||||
| 	return; | ||||
| } | ||||
|  | ||||
| void CacheDownload::downloadReadyRead() | ||||
| { | ||||
| 	QByteArray ba = m_reply->readAll(); | ||||
| 	md5sum.addData(ba); | ||||
| 	if (m_output_file->write(ba) != ba.size()) | ||||
| 	{ | ||||
| 		qCritical() << "Failed writing into " + m_target_path; | ||||
| 		m_status = Job_Failed; | ||||
| 		m_output_file->cancelWriting(); | ||||
| 		m_output_file.reset(); | ||||
| 		emit failed(m_index_within_job); | ||||
| 		wroteAnyData = false; | ||||
| 		return; | ||||
| 	} | ||||
| 	wroteAnyData = true; | ||||
| } | ||||
							
								
								
									
										63
									
								
								api/logic/net/CacheDownload.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								api/logic/net/CacheDownload.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | ||||
| /* Copyright 2013-2015 MultiMC Contributors | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include "NetAction.h" | ||||
| #include "HttpMetaCache.h" | ||||
| #include <QCryptographicHash> | ||||
| #include <QSaveFile> | ||||
|  | ||||
| #include "multimc_logic_export.h" | ||||
|  | ||||
| typedef std::shared_ptr<class CacheDownload> CacheDownloadPtr; | ||||
| class MULTIMC_LOGIC_EXPORT CacheDownload : public NetAction | ||||
| { | ||||
| 	Q_OBJECT | ||||
| private: | ||||
| 	MetaEntryPtr m_entry; | ||||
| 	/// if saving to file, use the one specified in this string | ||||
| 	QString m_target_path; | ||||
|  | ||||
| 	/// this is the output file, if any | ||||
| 	std::unique_ptr<QSaveFile> m_output_file; | ||||
|  | ||||
| 	/// the hash-as-you-download | ||||
| 	QCryptographicHash md5sum; | ||||
|  | ||||
| 	bool wroteAnyData = false; | ||||
|  | ||||
| public: | ||||
| 	explicit CacheDownload(QUrl url, MetaEntryPtr entry); | ||||
| 	static CacheDownloadPtr make(QUrl url, MetaEntryPtr entry) | ||||
| 	{ | ||||
| 		return CacheDownloadPtr(new CacheDownload(url, entry)); | ||||
| 	} | ||||
| 	virtual ~CacheDownload(){}; | ||||
| 	QString getTargetFilepath() | ||||
| 	{ | ||||
| 		return m_target_path; | ||||
| 	} | ||||
| protected | ||||
| slots: | ||||
| 	virtual void downloadProgress(qint64 bytesReceived, qint64 bytesTotal); | ||||
| 	virtual void downloadError(QNetworkReply::NetworkError error); | ||||
| 	virtual void downloadFinished(); | ||||
| 	virtual void downloadReadyRead(); | ||||
|  | ||||
| public | ||||
| slots: | ||||
| 	virtual void start(); | ||||
| }; | ||||
							
								
								
									
										273
									
								
								api/logic/net/HttpMetaCache.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										273
									
								
								api/logic/net/HttpMetaCache.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,273 @@ | ||||
| /* Copyright 2013-2015 MultiMC Contributors | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
|  | ||||
| #include "Env.h" | ||||
| #include "HttpMetaCache.h" | ||||
| #include "FileSystem.h" | ||||
|  | ||||
| #include <QFileInfo> | ||||
| #include <QFile> | ||||
| #include <QDateTime> | ||||
| #include <QCryptographicHash> | ||||
|  | ||||
| #include <QDebug> | ||||
|  | ||||
| #include <QJsonDocument> | ||||
| #include <QJsonArray> | ||||
| #include <QJsonObject> | ||||
|  | ||||
| QString MetaEntry::getFullPath() | ||||
| { | ||||
| 	// FIXME: make local? | ||||
| 	return FS::PathCombine(basePath, relativePath); | ||||
| } | ||||
|  | ||||
| HttpMetaCache::HttpMetaCache(QString path) : QObject() | ||||
| { | ||||
| 	m_index_file = path; | ||||
| 	saveBatchingTimer.setSingleShot(true); | ||||
| 	saveBatchingTimer.setTimerType(Qt::VeryCoarseTimer); | ||||
| 	connect(&saveBatchingTimer, SIGNAL(timeout()), SLOT(SaveNow())); | ||||
| } | ||||
|  | ||||
| HttpMetaCache::~HttpMetaCache() | ||||
| { | ||||
| 	saveBatchingTimer.stop(); | ||||
| 	SaveNow(); | ||||
| } | ||||
|  | ||||
| MetaEntryPtr HttpMetaCache::getEntry(QString base, QString resource_path) | ||||
| { | ||||
| 	// no base. no base path. can't store | ||||
| 	if (!m_entries.contains(base)) | ||||
| 	{ | ||||
| 		// TODO: log problem | ||||
| 		return MetaEntryPtr(); | ||||
| 	} | ||||
| 	EntryMap &map = m_entries[base]; | ||||
| 	if (map.entry_list.contains(resource_path)) | ||||
| 	{ | ||||
| 		return map.entry_list[resource_path]; | ||||
| 	} | ||||
| 	return MetaEntryPtr(); | ||||
| } | ||||
|  | ||||
| MetaEntryPtr HttpMetaCache::resolveEntry(QString base, QString resource_path, QString expected_etag) | ||||
| { | ||||
| 	auto entry = getEntry(base, resource_path); | ||||
| 	// it's not present? generate a default stale entry | ||||
| 	if (!entry) | ||||
| 	{ | ||||
| 		return staleEntry(base, resource_path); | ||||
| 	} | ||||
|  | ||||
| 	auto &selected_base = m_entries[base]; | ||||
| 	QString real_path = FS::PathCombine(selected_base.base_path, resource_path); | ||||
| 	QFileInfo finfo(real_path); | ||||
|  | ||||
| 	// is the file really there? if not -> stale | ||||
| 	if (!finfo.isFile() || !finfo.isReadable()) | ||||
| 	{ | ||||
| 		// if the file doesn't exist, we disown the entry | ||||
| 		selected_base.entry_list.remove(resource_path); | ||||
| 		return staleEntry(base, resource_path); | ||||
| 	} | ||||
|  | ||||
| 	if (!expected_etag.isEmpty() && expected_etag != entry->etag) | ||||
| 	{ | ||||
| 		// if the etag doesn't match expected, we disown the entry | ||||
| 		selected_base.entry_list.remove(resource_path); | ||||
| 		return staleEntry(base, resource_path); | ||||
| 	} | ||||
|  | ||||
| 	// if the file changed, check md5sum | ||||
| 	qint64 file_last_changed = finfo.lastModified().toUTC().toMSecsSinceEpoch(); | ||||
| 	if (file_last_changed != entry->local_changed_timestamp) | ||||
| 	{ | ||||
| 		QFile input(real_path); | ||||
| 		input.open(QIODevice::ReadOnly); | ||||
| 		QString md5sum = QCryptographicHash::hash(input.readAll(), QCryptographicHash::Md5) | ||||
| 							 .toHex() | ||||
| 							 .constData(); | ||||
| 		if (entry->md5sum != md5sum) | ||||
| 		{ | ||||
| 			selected_base.entry_list.remove(resource_path); | ||||
| 			return staleEntry(base, resource_path); | ||||
| 		} | ||||
| 		// md5sums matched... keep entry and save the new state to file | ||||
| 		entry->local_changed_timestamp = file_last_changed; | ||||
| 		SaveEventually(); | ||||
| 	} | ||||
|  | ||||
| 	// entry passed all the checks we cared about. | ||||
| 	entry->basePath = getBasePath(base); | ||||
| 	return entry; | ||||
| } | ||||
|  | ||||
| bool HttpMetaCache::updateEntry(MetaEntryPtr stale_entry) | ||||
| { | ||||
| 	if (!m_entries.contains(stale_entry->baseId)) | ||||
| 	{ | ||||
| 		qCritical() << "Cannot add entry with unknown base: " | ||||
| 					 << stale_entry->baseId.toLocal8Bit(); | ||||
| 		return false; | ||||
| 	} | ||||
| 	if (stale_entry->stale) | ||||
| 	{ | ||||
| 		qCritical() << "Cannot add stale entry: " << stale_entry->getFullPath().toLocal8Bit(); | ||||
| 		return false; | ||||
| 	} | ||||
| 	m_entries[stale_entry->baseId].entry_list[stale_entry->relativePath] = stale_entry; | ||||
| 	SaveEventually(); | ||||
| 	return true; | ||||
| } | ||||
|  | ||||
| bool HttpMetaCache::evictEntry(MetaEntryPtr entry) | ||||
| { | ||||
| 	if(entry) | ||||
| 	{ | ||||
| 		entry->stale = true; | ||||
| 		SaveEventually(); | ||||
| 		return true; | ||||
| 	} | ||||
| 	return false; | ||||
| } | ||||
|  | ||||
| MetaEntryPtr HttpMetaCache::staleEntry(QString base, QString resource_path) | ||||
| { | ||||
| 	auto foo = new MetaEntry(); | ||||
| 	foo->baseId = base; | ||||
| 	foo->basePath = getBasePath(base); | ||||
| 	foo->relativePath = resource_path; | ||||
| 	foo->stale = true; | ||||
| 	return MetaEntryPtr(foo); | ||||
| } | ||||
|  | ||||
| void HttpMetaCache::addBase(QString base, QString base_root) | ||||
| { | ||||
| 	// TODO: report error | ||||
| 	if (m_entries.contains(base)) | ||||
| 		return; | ||||
| 	// TODO: check if the base path is valid | ||||
| 	EntryMap foo; | ||||
| 	foo.base_path = base_root; | ||||
| 	m_entries[base] = foo; | ||||
| } | ||||
|  | ||||
| QString HttpMetaCache::getBasePath(QString base) | ||||
| { | ||||
| 	if (m_entries.contains(base)) | ||||
| 	{ | ||||
| 		return m_entries[base].base_path; | ||||
| 	} | ||||
| 	return QString(); | ||||
| } | ||||
|  | ||||
| void HttpMetaCache::Load() | ||||
| { | ||||
| 	if(m_index_file.isNull()) | ||||
| 		return; | ||||
|  | ||||
| 	QFile index(m_index_file); | ||||
| 	if (!index.open(QIODevice::ReadOnly)) | ||||
| 		return; | ||||
|  | ||||
| 	QJsonDocument json = QJsonDocument::fromJson(index.readAll()); | ||||
| 	if (!json.isObject()) | ||||
| 		return; | ||||
| 	auto root = json.object(); | ||||
| 	// check file version first | ||||
| 	auto version_val = root.value("version"); | ||||
| 	if (!version_val.isString()) | ||||
| 		return; | ||||
| 	if (version_val.toString() != "1") | ||||
| 		return; | ||||
|  | ||||
| 	// read the entry array | ||||
| 	auto entries_val = root.value("entries"); | ||||
| 	if (!entries_val.isArray()) | ||||
| 		return; | ||||
| 	QJsonArray array = entries_val.toArray(); | ||||
| 	for (auto element : array) | ||||
| 	{ | ||||
| 		if (!element.isObject()) | ||||
| 			return; | ||||
| 		auto element_obj = element.toObject(); | ||||
| 		QString base = element_obj.value("base").toString(); | ||||
| 		if (!m_entries.contains(base)) | ||||
| 			continue; | ||||
| 		auto &entrymap = m_entries[base]; | ||||
| 		auto foo = new MetaEntry(); | ||||
| 		foo->baseId = base; | ||||
| 		QString path = foo->relativePath = element_obj.value("path").toString(); | ||||
| 		foo->md5sum = element_obj.value("md5sum").toString(); | ||||
| 		foo->etag = element_obj.value("etag").toString(); | ||||
| 		foo->local_changed_timestamp = element_obj.value("last_changed_timestamp").toDouble(); | ||||
| 		foo->remote_changed_timestamp = | ||||
| 			element_obj.value("remote_changed_timestamp").toString(); | ||||
| 		// presumed innocent until closer examination | ||||
| 		foo->stale = false; | ||||
| 		entrymap.entry_list[path] = MetaEntryPtr(foo); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void HttpMetaCache::SaveEventually() | ||||
| { | ||||
| 	// reset the save timer | ||||
| 	saveBatchingTimer.stop(); | ||||
| 	saveBatchingTimer.start(30000); | ||||
| } | ||||
|  | ||||
| void HttpMetaCache::SaveNow() | ||||
| { | ||||
| 	if(m_index_file.isNull()) | ||||
| 		return; | ||||
| 	QJsonObject toplevel; | ||||
| 	toplevel.insert("version", QJsonValue(QString("1"))); | ||||
| 	QJsonArray entriesArr; | ||||
| 	for (auto group : m_entries) | ||||
| 	{ | ||||
| 		for (auto entry : group.entry_list) | ||||
| 		{ | ||||
| 			// do not save stale entries. they are dead. | ||||
| 			if(entry->stale) | ||||
| 			{ | ||||
| 				continue; | ||||
| 			} | ||||
| 			QJsonObject entryObj; | ||||
| 			entryObj.insert("base", QJsonValue(entry->baseId)); | ||||
| 			entryObj.insert("path", QJsonValue(entry->relativePath)); | ||||
| 			entryObj.insert("md5sum", QJsonValue(entry->md5sum)); | ||||
| 			entryObj.insert("etag", QJsonValue(entry->etag)); | ||||
| 			entryObj.insert("last_changed_timestamp", | ||||
| 							QJsonValue(double(entry->local_changed_timestamp))); | ||||
| 			if (!entry->remote_changed_timestamp.isEmpty()) | ||||
| 				entryObj.insert("remote_changed_timestamp", | ||||
| 								QJsonValue(entry->remote_changed_timestamp)); | ||||
| 			entriesArr.append(entryObj); | ||||
| 		} | ||||
| 	} | ||||
| 	toplevel.insert("entries", entriesArr); | ||||
|  | ||||
| 	QJsonDocument doc(toplevel); | ||||
| 	try | ||||
| 	{ | ||||
| 		FS::write(m_index_file, doc.toJson()); | ||||
| 	} | ||||
| 	catch (Exception & e) | ||||
| 	{ | ||||
| 		qWarning() << e.what(); | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										125
									
								
								api/logic/net/HttpMetaCache.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								api/logic/net/HttpMetaCache.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,125 @@ | ||||
| /* Copyright 2013-2015 MultiMC Contributors | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
|  | ||||
| #pragma once | ||||
| #include <QString> | ||||
| #include <QMap> | ||||
| #include <qtimer.h> | ||||
| #include <memory> | ||||
|  | ||||
| #include "multimc_logic_export.h" | ||||
|  | ||||
| class HttpMetaCache; | ||||
|  | ||||
| class MULTIMC_LOGIC_EXPORT MetaEntry | ||||
| { | ||||
| friend class HttpMetaCache; | ||||
| protected: | ||||
| 	MetaEntry() {} | ||||
| public: | ||||
| 	bool isStale() | ||||
| 	{ | ||||
| 		return stale; | ||||
| 	} | ||||
| 	void setStale(bool stale) | ||||
| 	{ | ||||
| 		this->stale = stale; | ||||
| 	} | ||||
| 	QString getFullPath(); | ||||
| 	QString getRemoteChangedTimestamp() | ||||
| 	{ | ||||
| 		return remote_changed_timestamp; | ||||
| 	} | ||||
| 	void setRemoteChangedTimestamp(QString remote_changed_timestamp) | ||||
| 	{ | ||||
| 		this->remote_changed_timestamp = remote_changed_timestamp; | ||||
| 	} | ||||
| 	void setLocalChangedTimestamp(qint64 timestamp) | ||||
| 	{ | ||||
| 		local_changed_timestamp = timestamp; | ||||
| 	} | ||||
| 	QString getETag() | ||||
| 	{ | ||||
| 		return etag; | ||||
| 	} | ||||
| 	void setETag(QString etag) | ||||
| 	{ | ||||
| 		this->etag = etag; | ||||
| 	} | ||||
| 	QString getMD5Sum() | ||||
| 	{ | ||||
| 		return md5sum; | ||||
| 	} | ||||
| 	void setMD5Sum(QString md5sum) | ||||
| 	{ | ||||
| 		this->md5sum = md5sum; | ||||
| 	} | ||||
| protected: | ||||
| 	QString baseId; | ||||
| 	QString basePath; | ||||
| 	QString relativePath; | ||||
| 	QString md5sum; | ||||
| 	QString etag; | ||||
| 	qint64 local_changed_timestamp = 0; | ||||
| 	QString remote_changed_timestamp; // QString for now, RFC 2822 encoded time | ||||
| 	bool stale = true; | ||||
| }; | ||||
|  | ||||
| typedef std::shared_ptr<MetaEntry> MetaEntryPtr; | ||||
|  | ||||
| class MULTIMC_LOGIC_EXPORT HttpMetaCache : public QObject | ||||
| { | ||||
| 	Q_OBJECT | ||||
| public: | ||||
| 	// supply path to the cache index file | ||||
| 	HttpMetaCache(QString path = QString()); | ||||
| 	~HttpMetaCache(); | ||||
|  | ||||
| 	// get the entry solely from the cache | ||||
| 	// you probably don't want this, unless you have some specific caching needs. | ||||
| 	MetaEntryPtr getEntry(QString base, QString resource_path); | ||||
|  | ||||
| 	// get the entry from cache and verify that it isn't stale (within reason) | ||||
| 	MetaEntryPtr resolveEntry(QString base, QString resource_path, | ||||
| 							  QString expected_etag = QString()); | ||||
|  | ||||
| 	// add a previously resolved stale entry | ||||
| 	bool updateEntry(MetaEntryPtr stale_entry); | ||||
|  | ||||
| 	// evict selected entry from cache | ||||
| 	bool evictEntry(MetaEntryPtr entry); | ||||
|  | ||||
| 	void addBase(QString base, QString base_root); | ||||
|  | ||||
| 	// (re)start a timer that calls SaveNow later. | ||||
| 	void SaveEventually(); | ||||
| 	void Load(); | ||||
| 	QString getBasePath(QString base); | ||||
| public | ||||
| slots: | ||||
| 	void SaveNow(); | ||||
|  | ||||
| private: | ||||
| 	// create a new stale entry, given the parameters | ||||
| 	MetaEntryPtr staleEntry(QString base, QString resource_path); | ||||
| 	struct EntryMap | ||||
| 	{ | ||||
| 		QString base_path; | ||||
| 		QMap<QString, MetaEntryPtr> entry_list; | ||||
| 	}; | ||||
| 	QMap<QString, EntryMap> m_entries; | ||||
| 	QString m_index_file; | ||||
| 	QTimer saveBatchingTimer; | ||||
| }; | ||||
							
								
								
									
										155
									
								
								api/logic/net/MD5EtagDownload.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								api/logic/net/MD5EtagDownload.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,155 @@ | ||||
| /* Copyright 2013-2015 MultiMC Contributors | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
|  | ||||
| #include "Env.h" | ||||
| #include "MD5EtagDownload.h" | ||||
| #include <FileSystem.h> | ||||
| #include <QCryptographicHash> | ||||
| #include <QDebug> | ||||
|  | ||||
| MD5EtagDownload::MD5EtagDownload(QUrl url, QString target_path) : NetAction() | ||||
| { | ||||
| 	m_url = url; | ||||
| 	m_target_path = target_path; | ||||
| 	m_status = Job_NotStarted; | ||||
| } | ||||
|  | ||||
| void MD5EtagDownload::start() | ||||
| { | ||||
| 	QString filename = m_target_path; | ||||
| 	m_output_file.setFileName(filename); | ||||
| 	// if there already is a file and md5 checking is in effect and it can be opened | ||||
| 	if (m_output_file.exists() && m_output_file.open(QIODevice::ReadOnly)) | ||||
| 	{ | ||||
| 		// get the md5 of the local file. | ||||
| 		m_local_md5 = | ||||
| 			QCryptographicHash::hash(m_output_file.readAll(), QCryptographicHash::Md5) | ||||
| 				.toHex() | ||||
| 				.constData(); | ||||
| 		m_output_file.close(); | ||||
| 		// if we are expecting some md5sum, compare it with the local one | ||||
| 		if (!m_expected_md5.isEmpty()) | ||||
| 		{ | ||||
| 			// skip if they match | ||||
| 			if(m_local_md5 == m_expected_md5) | ||||
| 			{ | ||||
| 				qDebug() << "Skipping " << m_url.toString() << ": md5 match."; | ||||
| 				emit succeeded(m_index_within_job); | ||||
| 				return; | ||||
| 			} | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			// no expected md5. we use the local md5sum as an ETag | ||||
| 		} | ||||
| 	} | ||||
| 	if (!FS::ensureFilePathExists(filename)) | ||||
| 	{ | ||||
| 		emit failed(m_index_within_job); | ||||
| 		return; | ||||
| 	} | ||||
|  | ||||
| 	QNetworkRequest request(m_url); | ||||
|  | ||||
| 	qDebug() << "Downloading " << m_url.toString() << " local MD5: " << m_local_md5; | ||||
|  | ||||
| 	if(!m_local_md5.isEmpty()) | ||||
| 	{ | ||||
| 		request.setRawHeader(QString("If-None-Match").toLatin1(), m_local_md5.toLatin1()); | ||||
| 	} | ||||
| 	if(!m_expected_md5.isEmpty()) | ||||
| 		qDebug() << "Expecting " << m_expected_md5; | ||||
|  | ||||
| 	request.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Uncached)"); | ||||
|  | ||||
| 	// Go ahead and try to open the file. | ||||
| 	// If we don't do this, empty files won't be created, which breaks the updater. | ||||
| 	// Plus, this way, we don't end up starting a download for a file we can't open. | ||||
| 	if (!m_output_file.open(QIODevice::WriteOnly)) | ||||
| 	{ | ||||
| 		emit failed(m_index_within_job); | ||||
| 		return; | ||||
| 	} | ||||
|  | ||||
| 	auto worker = ENV.qnam(); | ||||
| 	QNetworkReply *rep = worker->get(request); | ||||
|  | ||||
| 	m_reply.reset(rep); | ||||
| 	connect(rep, SIGNAL(downloadProgress(qint64, qint64)), | ||||
| 			SLOT(downloadProgress(qint64, qint64))); | ||||
| 	connect(rep, SIGNAL(finished()), SLOT(downloadFinished())); | ||||
| 	connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), | ||||
| 			SLOT(downloadError(QNetworkReply::NetworkError))); | ||||
| 	connect(rep, SIGNAL(readyRead()), SLOT(downloadReadyRead())); | ||||
| } | ||||
|  | ||||
| void MD5EtagDownload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) | ||||
| { | ||||
| 	m_total_progress = bytesTotal; | ||||
| 	m_progress = bytesReceived; | ||||
| 	emit netActionProgress(m_index_within_job, bytesReceived, bytesTotal); | ||||
| } | ||||
|  | ||||
| void MD5EtagDownload::downloadError(QNetworkReply::NetworkError error) | ||||
| { | ||||
| 	qCritical() << "Error" << error << ":" << m_reply->errorString() << "while downloading" | ||||
| 				 << m_reply->url(); | ||||
| 	m_status = Job_Failed; | ||||
| } | ||||
|  | ||||
| void MD5EtagDownload::downloadFinished() | ||||
| { | ||||
| 	// if the download succeeded | ||||
| 	if (m_status != Job_Failed) | ||||
| 	{ | ||||
| 		// nothing went wrong... | ||||
| 		m_status = Job_Finished; | ||||
| 		m_output_file.close(); | ||||
|  | ||||
| 		// FIXME: compare with the real written data md5sum | ||||
| 		// this is just an ETag | ||||
| 		qDebug() << "Finished " << m_url.toString() << " got " << m_reply->rawHeader("ETag").constData(); | ||||
|  | ||||
| 		m_reply.reset(); | ||||
| 		emit succeeded(m_index_within_job); | ||||
| 		return; | ||||
| 	} | ||||
| 	// else the download failed | ||||
| 	else | ||||
| 	{ | ||||
| 		m_output_file.close(); | ||||
| 		m_output_file.remove(); | ||||
| 		m_reply.reset(); | ||||
| 		emit failed(m_index_within_job); | ||||
| 		return; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void MD5EtagDownload::downloadReadyRead() | ||||
| { | ||||
| 	if (!m_output_file.isOpen()) | ||||
| 	{ | ||||
| 		if (!m_output_file.open(QIODevice::WriteOnly)) | ||||
| 		{ | ||||
| 			/* | ||||
| 			* Can't open the file... the job failed | ||||
| 			*/ | ||||
| 			m_reply->abort(); | ||||
| 			emit failed(m_index_within_job); | ||||
| 			return; | ||||
| 		} | ||||
| 	} | ||||
| 	m_output_file.write(m_reply->readAll()); | ||||
| } | ||||
							
								
								
									
										52
									
								
								api/logic/net/MD5EtagDownload.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								api/logic/net/MD5EtagDownload.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| /* Copyright 2013-2015 MultiMC Contributors | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include "NetAction.h" | ||||
| #include <QFile> | ||||
|  | ||||
| typedef std::shared_ptr<class MD5EtagDownload> Md5EtagDownloadPtr; | ||||
| class MD5EtagDownload : public NetAction | ||||
| { | ||||
| 	Q_OBJECT | ||||
| public: | ||||
| 	/// the expected md5 checksum. Only set from outside | ||||
| 	QString m_expected_md5; | ||||
| 	/// the md5 checksum of a file that already exists. | ||||
| 	QString m_local_md5; | ||||
| 	/// if saving to file, use the one specified in this string | ||||
| 	QString m_target_path; | ||||
| 	/// this is the output file, if any | ||||
| 	QFile m_output_file; | ||||
|  | ||||
| public: | ||||
| 	explicit MD5EtagDownload(QUrl url, QString target_path); | ||||
| 	static Md5EtagDownloadPtr make(QUrl url, QString target_path) | ||||
| 	{ | ||||
| 		return Md5EtagDownloadPtr(new MD5EtagDownload(url, target_path)); | ||||
| 	} | ||||
| 	virtual ~MD5EtagDownload(){}; | ||||
| protected | ||||
| slots: | ||||
| 	virtual void downloadProgress(qint64 bytesReceived, qint64 bytesTotal); | ||||
| 	virtual void downloadError(QNetworkReply::NetworkError error); | ||||
| 	virtual void downloadFinished(); | ||||
| 	virtual void downloadReadyRead(); | ||||
|  | ||||
| public | ||||
| slots: | ||||
| 	virtual void start(); | ||||
| }; | ||||
							
								
								
									
										96
									
								
								api/logic/net/NetAction.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								api/logic/net/NetAction.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,96 @@ | ||||
| /* Copyright 2013-2015 MultiMC Contributors | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include <QObject> | ||||
| #include <QUrl> | ||||
| #include <memory> | ||||
| #include <QNetworkReply> | ||||
| #include <QObjectPtr.h> | ||||
|  | ||||
| #include "multimc_logic_export.h" | ||||
|  | ||||
| enum JobStatus | ||||
| { | ||||
| 	Job_NotStarted, | ||||
| 	Job_InProgress, | ||||
| 	Job_Finished, | ||||
| 	Job_Failed | ||||
| }; | ||||
|  | ||||
| typedef std::shared_ptr<class NetAction> NetActionPtr; | ||||
| class MULTIMC_LOGIC_EXPORT NetAction : public QObject | ||||
| { | ||||
| 	Q_OBJECT | ||||
| protected: | ||||
| 	explicit NetAction() : QObject(0) {}; | ||||
|  | ||||
| public: | ||||
| 	virtual ~NetAction() {}; | ||||
|  | ||||
| public: | ||||
| 	virtual qint64 totalProgress() const | ||||
| 	{ | ||||
| 		return m_total_progress; | ||||
| 	} | ||||
| 	virtual qint64 currentProgress() const | ||||
| 	{ | ||||
| 		return m_progress; | ||||
| 	} | ||||
| 	virtual qint64 numberOfFailures() const | ||||
| 	{ | ||||
| 		return m_failures; | ||||
| 	} | ||||
|  | ||||
| public: | ||||
| 	/// the network reply | ||||
| 	unique_qobject_ptr<QNetworkReply> m_reply; | ||||
|  | ||||
| 	/// the content of the content-type header | ||||
| 	QString m_content_type; | ||||
|  | ||||
| 	/// source URL | ||||
| 	QUrl m_url; | ||||
|  | ||||
| 	/// The file's status | ||||
| 	JobStatus m_status = Job_NotStarted; | ||||
|  | ||||
| 	/// index within the parent job | ||||
| 	int m_index_within_job = 0; | ||||
|  | ||||
| 	qint64 m_progress = 0; | ||||
| 	qint64 m_total_progress = 1; | ||||
|  | ||||
| 	/// number of failures up to this point | ||||
| 	int m_failures = 0; | ||||
|  | ||||
| signals: | ||||
| 	void started(int index); | ||||
| 	void netActionProgress(int index, qint64 current, qint64 total); | ||||
| 	void succeeded(int index); | ||||
| 	void failed(int index); | ||||
|  | ||||
| protected | ||||
| slots: | ||||
| 	virtual void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) = 0; | ||||
| 	virtual void downloadError(QNetworkReply::NetworkError error) = 0; | ||||
| 	virtual void downloadFinished() = 0; | ||||
| 	virtual void downloadReadyRead() = 0; | ||||
|  | ||||
| public | ||||
| slots: | ||||
| 	virtual void start() = 0; | ||||
| }; | ||||
							
								
								
									
										125
									
								
								api/logic/net/NetJob.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								api/logic/net/NetJob.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,125 @@ | ||||
| /* Copyright 2013-2015 MultiMC Contributors | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
|  | ||||
| #include "NetJob.h" | ||||
| #include "MD5EtagDownload.h" | ||||
| #include "ByteArrayDownload.h" | ||||
| #include "CacheDownload.h" | ||||
|  | ||||
| #include <QDebug> | ||||
|  | ||||
| void NetJob::partSucceeded(int index) | ||||
| { | ||||
| 	// do progress. all slots are 1 in size at least | ||||
| 	auto &slot = parts_progress[index]; | ||||
| 	partProgress(index, slot.total_progress, slot.total_progress); | ||||
|  | ||||
| 	m_doing.remove(index); | ||||
| 	m_done.insert(index); | ||||
| 	downloads[index].get()->disconnect(this); | ||||
| 	startMoreParts(); | ||||
| } | ||||
|  | ||||
| void NetJob::partFailed(int index) | ||||
| { | ||||
| 	m_doing.remove(index); | ||||
| 	auto &slot = parts_progress[index]; | ||||
| 	if (slot.failures == 3) | ||||
| 	{ | ||||
| 		m_failed.insert(index); | ||||
| 	} | ||||
| 	else | ||||
| 	{ | ||||
| 		slot.failures++; | ||||
| 		m_todo.enqueue(index); | ||||
| 	} | ||||
| 	downloads[index].get()->disconnect(this); | ||||
| 	startMoreParts(); | ||||
| } | ||||
|  | ||||
| void NetJob::partProgress(int index, qint64 bytesReceived, qint64 bytesTotal) | ||||
| { | ||||
| 	auto &slot = parts_progress[index]; | ||||
|  | ||||
| 	current_progress -= slot.current_progress; | ||||
| 	slot.current_progress = bytesReceived; | ||||
| 	current_progress += slot.current_progress; | ||||
|  | ||||
| 	total_progress -= slot.total_progress; | ||||
| 	slot.total_progress = bytesTotal; | ||||
| 	total_progress += slot.total_progress; | ||||
| 	setProgress(current_progress, total_progress); | ||||
| } | ||||
|  | ||||
| void NetJob::executeTask() | ||||
| { | ||||
| 	qDebug() << m_job_name.toLocal8Bit() << " started."; | ||||
| 	m_running = true; | ||||
| 	for (int i = 0; i < downloads.size(); i++) | ||||
| 	{ | ||||
| 		m_todo.enqueue(i); | ||||
| 	} | ||||
| 	// hack that delays early failures so they can be caught easier | ||||
| 	QMetaObject::invokeMethod(this, "startMoreParts", Qt::QueuedConnection); | ||||
| } | ||||
|  | ||||
| void NetJob::startMoreParts() | ||||
| { | ||||
| 	// check for final conditions if there's nothing in the queue | ||||
| 	if(!m_todo.size()) | ||||
| 	{ | ||||
| 		if(!m_doing.size()) | ||||
| 		{ | ||||
| 			if(!m_failed.size()) | ||||
| 			{ | ||||
| 				qDebug() << m_job_name << "succeeded."; | ||||
| 				emitSucceeded(); | ||||
| 			} | ||||
| 			else | ||||
| 			{ | ||||
| 				qCritical() << m_job_name << "failed."; | ||||
| 				emitFailed(tr("Job '%1' failed to process:\n%2").arg(m_job_name).arg(getFailedFiles().join("\n"))); | ||||
| 			} | ||||
| 		} | ||||
| 		return; | ||||
| 	} | ||||
| 	// otherwise try to start more parts | ||||
| 	while (m_doing.size() < 6) | ||||
| 	{ | ||||
| 		if(!m_todo.size()) | ||||
| 			return; | ||||
| 		int doThis = m_todo.dequeue(); | ||||
| 		m_doing.insert(doThis); | ||||
| 		auto part = downloads[doThis]; | ||||
| 		// connect signals :D | ||||
| 		connect(part.get(), SIGNAL(succeeded(int)), SLOT(partSucceeded(int))); | ||||
| 		connect(part.get(), SIGNAL(failed(int)), SLOT(partFailed(int))); | ||||
| 		connect(part.get(), SIGNAL(netActionProgress(int, qint64, qint64)), | ||||
| 				SLOT(partProgress(int, qint64, qint64))); | ||||
| 		part->start(); | ||||
| 	} | ||||
| } | ||||
|  | ||||
|  | ||||
| QStringList NetJob::getFailedFiles() | ||||
| { | ||||
| 	QStringList failed; | ||||
| 	for (auto index: m_failed) | ||||
| 	{ | ||||
| 		failed.push_back(downloads[index]->m_url.toString()); | ||||
| 	} | ||||
| 	failed.sort(); | ||||
| 	return failed; | ||||
| } | ||||
							
								
								
									
										117
									
								
								api/logic/net/NetJob.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								api/logic/net/NetJob.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,117 @@ | ||||
| /* Copyright 2013-2015 MultiMC Contributors | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
|  | ||||
| #pragma once | ||||
| #include <QtNetwork> | ||||
| #include "NetAction.h" | ||||
| #include "ByteArrayDownload.h" | ||||
| #include "MD5EtagDownload.h" | ||||
| #include "CacheDownload.h" | ||||
| #include "HttpMetaCache.h" | ||||
| #include "tasks/Task.h" | ||||
| #include "QObjectPtr.h" | ||||
|  | ||||
| #include "multimc_logic_export.h" | ||||
|  | ||||
| class NetJob; | ||||
| typedef shared_qobject_ptr<NetJob> NetJobPtr; | ||||
|  | ||||
| class MULTIMC_LOGIC_EXPORT NetJob : public Task | ||||
| { | ||||
| 	Q_OBJECT | ||||
| public: | ||||
| 	explicit NetJob(QString job_name) : Task(), m_job_name(job_name) {} | ||||
| 	virtual ~NetJob() {} | ||||
| 	bool addNetAction(NetActionPtr action) | ||||
| 	{ | ||||
| 		action->m_index_within_job = downloads.size(); | ||||
| 		downloads.append(action); | ||||
| 		part_info pi; | ||||
| 		{ | ||||
| 			pi.current_progress = action->currentProgress(); | ||||
| 			pi.total_progress = action->totalProgress(); | ||||
| 			pi.failures = action->numberOfFailures(); | ||||
| 		} | ||||
| 		parts_progress.append(pi); | ||||
| 		total_progress += pi.total_progress; | ||||
| 		// if this is already running, the action needs to be started right away! | ||||
| 		if (isRunning()) | ||||
| 		{ | ||||
| 			setProgress(current_progress, total_progress); | ||||
| 			connect(action.get(), SIGNAL(succeeded(int)), SLOT(partSucceeded(int))); | ||||
| 			connect(action.get(), SIGNAL(failed(int)), SLOT(partFailed(int))); | ||||
| 			connect(action.get(), SIGNAL(netActionProgress(int, qint64, qint64)), | ||||
| 					SLOT(partProgress(int, qint64, qint64))); | ||||
| 			action->start(); | ||||
| 		} | ||||
| 		return true; | ||||
| 	} | ||||
|  | ||||
| 	NetActionPtr operator[](int index) | ||||
| 	{ | ||||
| 		return downloads[index]; | ||||
| 	} | ||||
| 	const NetActionPtr at(const int index) | ||||
| 	{ | ||||
| 		return downloads.at(index); | ||||
| 	} | ||||
| 	NetActionPtr first() | ||||
| 	{ | ||||
| 		if (downloads.size()) | ||||
| 			return downloads[0]; | ||||
| 		return NetActionPtr(); | ||||
| 	} | ||||
| 	int size() const | ||||
| 	{ | ||||
| 		return downloads.size(); | ||||
| 	} | ||||
| 	virtual bool isRunning() const | ||||
| 	{ | ||||
| 		return m_running; | ||||
| 	} | ||||
| 	QStringList getFailedFiles(); | ||||
|  | ||||
| private slots: | ||||
| 	void startMoreParts(); | ||||
|  | ||||
| public slots: | ||||
| 	virtual void executeTask(); | ||||
| 	// FIXME: implement | ||||
| 	virtual bool abort() {return false;}; | ||||
|  | ||||
| private slots: | ||||
| 	void partProgress(int index, qint64 bytesReceived, qint64 bytesTotal); | ||||
| 	void partSucceeded(int index); | ||||
| 	void partFailed(int index); | ||||
|  | ||||
| private: | ||||
| 	struct part_info | ||||
| 	{ | ||||
| 		qint64 current_progress = 0; | ||||
| 		qint64 total_progress = 1; | ||||
| 		int failures = 0; | ||||
| 		bool connected = false; | ||||
| 	}; | ||||
| 	QString m_job_name; | ||||
| 	QList<NetActionPtr> downloads; | ||||
| 	QList<part_info> parts_progress; | ||||
| 	QQueue<int> m_todo; | ||||
| 	QSet<int> m_doing; | ||||
| 	QSet<int> m_done; | ||||
| 	QSet<int> m_failed; | ||||
| 	qint64 current_progress = 0; | ||||
| 	qint64 total_progress = 0; | ||||
| 	bool m_running = false; | ||||
| }; | ||||
							
								
								
									
										99
									
								
								api/logic/net/PasteUpload.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								api/logic/net/PasteUpload.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,99 @@ | ||||
| #include "PasteUpload.h" | ||||
| #include "Env.h" | ||||
| #include <QDebug> | ||||
| #include <QJsonObject> | ||||
| #include <QJsonDocument> | ||||
|  | ||||
| PasteUpload::PasteUpload(QWidget *window, QString text, QString key) : m_window(window) | ||||
| { | ||||
| 	m_key = key; | ||||
| 	QByteArray temp; | ||||
| 	temp = text.toUtf8(); | ||||
| 	temp.replace('\n', "\r\n"); | ||||
| 	m_textSize = temp.size(); | ||||
| 	m_text = "key=" + m_key.toLatin1() + "&description=MultiMC5+Log+File&language=plain&format=json&expire=2592000&paste=" + temp.toPercentEncoding(); | ||||
| 	buf =  new QBuffer(&m_text); | ||||
| } | ||||
|  | ||||
| PasteUpload::~PasteUpload() | ||||
| { | ||||
| 	if(buf) | ||||
| 	{ | ||||
| 		delete buf; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| bool PasteUpload::validateText() | ||||
| { | ||||
| 	return m_textSize <= maxSize(); | ||||
| } | ||||
|  | ||||
| void PasteUpload::executeTask() | ||||
| { | ||||
| 	QNetworkRequest request(QUrl("http://paste.ee/api")); | ||||
| 	request.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Uncached)"); | ||||
|  | ||||
| 	request.setRawHeader("Content-Type", "application/x-www-form-urlencoded"); | ||||
| 	request.setRawHeader("Content-Length", QByteArray::number(m_text.size())); | ||||
|  | ||||
| 	auto worker = ENV.qnam(); | ||||
| 	QNetworkReply *rep = worker->post(request, buf); | ||||
|  | ||||
| 	m_reply = std::shared_ptr<QNetworkReply>(rep); | ||||
| 	setStatus(tr("Uploading to paste.ee")); | ||||
| 	connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress); | ||||
| 	connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError))); | ||||
| 	connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished())); | ||||
| } | ||||
|  | ||||
| void PasteUpload::downloadError(QNetworkReply::NetworkError error) | ||||
| { | ||||
| 	// error happened during download. | ||||
| 	qCritical() << "Network error: " << error; | ||||
| 	emitFailed(m_reply->errorString()); | ||||
| } | ||||
|  | ||||
| void PasteUpload::downloadFinished() | ||||
| { | ||||
| 	// if the download succeeded | ||||
| 	if (m_reply->error() == QNetworkReply::NetworkError::NoError) | ||||
| 	{ | ||||
| 		QByteArray data = m_reply->readAll(); | ||||
| 		m_reply.reset(); | ||||
| 		QJsonParseError jsonError; | ||||
| 		QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); | ||||
| 		if (jsonError.error != QJsonParseError::NoError) | ||||
| 		{ | ||||
| 			emitFailed(jsonError.errorString()); | ||||
| 			return; | ||||
| 		} | ||||
| 		if (!parseResult(doc)) | ||||
| 		{ | ||||
| 			emitFailed(tr("paste.ee returned an error. Please consult the logs for more information")); | ||||
| 			return; | ||||
| 		} | ||||
| 	} | ||||
| 	// else the download failed | ||||
| 	else | ||||
| 	{ | ||||
| 		emitFailed(QString("Network error: %1").arg(m_reply->errorString())); | ||||
| 		m_reply.reset(); | ||||
| 		return; | ||||
| 	} | ||||
| 	emitSucceeded(); | ||||
| } | ||||
|  | ||||
| bool PasteUpload::parseResult(QJsonDocument doc) | ||||
| { | ||||
| 	auto object = doc.object(); | ||||
| 	auto status = object.value("status").toString("error"); | ||||
| 	if (status == "error") | ||||
| 	{ | ||||
| 		qCritical() << "paste.ee reported error:" << QString(object.value("error").toString()); | ||||
| 		return false; | ||||
| 	} | ||||
| 	m_pasteLink = object.value("paste").toObject().value("link").toString(); | ||||
| 	m_pasteID = object.value("paste").toObject().value("id").toString(); | ||||
| 	return true; | ||||
| } | ||||
|  | ||||
							
								
								
									
										50
									
								
								api/logic/net/PasteUpload.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								api/logic/net/PasteUpload.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | ||||
| #pragma once | ||||
| #include "tasks/Task.h" | ||||
| #include <QNetworkReply> | ||||
| #include <QBuffer> | ||||
| #include <memory> | ||||
|  | ||||
| #include "multimc_logic_export.h" | ||||
|  | ||||
| class MULTIMC_LOGIC_EXPORT PasteUpload : public Task | ||||
| { | ||||
| 	Q_OBJECT | ||||
| public: | ||||
| 	PasteUpload(QWidget *window, QString text, QString key = "public"); | ||||
| 	virtual ~PasteUpload(); | ||||
| 	QString pasteLink() | ||||
| 	{ | ||||
| 		return m_pasteLink; | ||||
| 	} | ||||
| 	QString pasteID() | ||||
| 	{ | ||||
| 		return m_pasteID; | ||||
| 	} | ||||
| 	uint32_t maxSize() | ||||
| 	{ | ||||
| 		// 2MB for paste.ee - public | ||||
| 		if(m_key == "public") | ||||
| 			return 1024*1024*2; | ||||
| 		// 12MB for paste.ee - with actual key | ||||
| 		return 1024*1024*12; | ||||
| 	} | ||||
| 	bool validateText(); | ||||
| protected: | ||||
| 	virtual void executeTask(); | ||||
|  | ||||
| private: | ||||
| 	bool parseResult(QJsonDocument doc); | ||||
| 	QByteArray m_text; | ||||
| 	QString m_error; | ||||
| 	QWidget *m_window; | ||||
| 	QString m_pasteID; | ||||
| 	QString m_pasteLink; | ||||
| 	QString m_key; | ||||
| 	int m_textSize = 0; | ||||
| 	QBuffer * buf = nullptr; | ||||
| 	std::shared_ptr<QNetworkReply> m_reply; | ||||
| public | ||||
| slots: | ||||
| 	void downloadError(QNetworkReply::NetworkError); | ||||
| 	void downloadFinished(); | ||||
| }; | ||||
							
								
								
									
										16
									
								
								api/logic/net/URLConstants.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								api/logic/net/URLConstants.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| #include "URLConstants.h" | ||||
|  | ||||
| namespace URLConstants { | ||||
|  | ||||
| QString getLegacyJarUrl(QString version) | ||||
| { | ||||
| 	return "http://" + AWS_DOWNLOAD_VERSIONS + getJarPath(version); | ||||
| } | ||||
|  | ||||
| QString getJarPath(QString version) | ||||
| { | ||||
| 	return version + "/" + version + ".jar"; | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
							
								
								
									
										40
									
								
								api/logic/net/URLConstants.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								api/logic/net/URLConstants.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| /* Copyright 2013-2015 MultiMC Contributors | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include <QString> | ||||
|  | ||||
| namespace URLConstants | ||||
| { | ||||
| const QString AWS_DOWNLOAD_VERSIONS("s3.amazonaws.com/Minecraft.Download/versions/"); | ||||
| const QString RESOURCE_BASE("resources.download.minecraft.net/"); | ||||
| const QString LIBRARY_BASE("libraries.minecraft.net/"); | ||||
| //const QString SKINS_BASE("skins.minecraft.net/MinecraftSkins/"); | ||||
| const QString SKINS_BASE("crafatar.com/skins/"); | ||||
| const QString AUTH_BASE("authserver.mojang.com/"); | ||||
| const QString FORGE_LEGACY_URL("http://files.minecraftforge.net/minecraftforge/json"); | ||||
| const QString FORGE_GRADLE_URL("http://files.minecraftforge.net/maven/net/minecraftforge/forge/json"); | ||||
| const QString MOJANG_STATUS_URL("http://status.mojang.com/check"); | ||||
| const QString MOJANG_STATUS_NEWS_URL("http://status.mojang.com/news"); | ||||
| const QString LITELOADER_URL("http://dl.liteloader.com/versions/versions.json"); | ||||
| const QString IMGUR_BASE_URL("https://api.imgur.com/3/"); | ||||
| const QString FMLLIBS_OUR_BASE_URL("http://files.multimc.org/fmllibs/"); | ||||
| const QString FMLLIBS_FORGE_BASE_URL("http://files.minecraftforge.net/fmllibs/"); | ||||
| const QString TRANSLATIONS_BASE_URL("http://files.multimc.org/translations/"); | ||||
|  | ||||
| QString getJarPath(QString version); | ||||
| QString getLegacyJarUrl(QString version); | ||||
| } | ||||
		Reference in New Issue
	
	Block a user