2022-05-02 21:26:24 +05:30
|
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
/*
|
|
|
|
* PolyMC - Minecraft Launcher
|
|
|
|
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
|
2013-11-04 07:23:05 +05:30
|
|
|
*
|
2022-05-02 21:26:24 +05:30
|
|
|
* This program is free software: you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
* the Free Software Foundation, version 3.
|
2013-11-04 07:23:05 +05:30
|
|
|
*
|
2022-05-02 21:26:24 +05:30
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU General Public License for more details.
|
2013-11-04 07:23:05 +05:30
|
|
|
*
|
2022-05-02 21:26:24 +05:30
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
*
|
|
|
|
* This file incorporates work covered by the following copyright and
|
|
|
|
* permission notice:
|
|
|
|
*
|
|
|
|
* Copyright 2013-2021 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.
|
2013-11-04 07:23:05 +05:30
|
|
|
*/
|
|
|
|
|
2013-09-07 01:28:51 +05:30
|
|
|
#include "HttpMetaCache.h"
|
2015-06-04 00:40:28 +05:30
|
|
|
#include "FileSystem.h"
|
2022-04-28 03:06:11 +05:30
|
|
|
#include "Json.h"
|
2013-09-08 05:45:20 +05:30
|
|
|
|
|
|
|
#include <QCryptographicHash>
|
2022-04-28 03:06:11 +05:30
|
|
|
#include <QDateTime>
|
|
|
|
#include <QFile>
|
|
|
|
#include <QFileInfo>
|
2013-09-08 05:45:20 +05:30
|
|
|
|
2015-02-02 06:44:14 +05:30
|
|
|
#include <QDebug>
|
2013-09-08 05:45:20 +05:30
|
|
|
|
2022-07-17 04:59:03 +05:30
|
|
|
/** Maximum time to hold a cache entry
|
|
|
|
* = 1 week in milliseconds
|
|
|
|
*/
|
|
|
|
#define TIME_TO_EXPIRE 1*7*24*60*60*1000
|
|
|
|
|
2022-04-28 03:06:11 +05:30
|
|
|
auto MetaEntry::getFullPath() -> QString
|
2013-09-08 05:45:20 +05:30
|
|
|
{
|
2018-07-15 18:21:05 +05:30
|
|
|
// FIXME: make local?
|
|
|
|
return FS::PathCombine(basePath, relativePath);
|
2013-09-08 05:45:20 +05:30
|
|
|
}
|
|
|
|
|
2022-04-28 03:06:11 +05:30
|
|
|
HttpMetaCache::HttpMetaCache(QString path) : QObject(), m_index_file(path)
|
2013-09-07 01:28:51 +05:30
|
|
|
{
|
2018-07-15 18:21:05 +05:30
|
|
|
saveBatchingTimer.setSingleShot(true);
|
|
|
|
saveBatchingTimer.setTimerType(Qt::VeryCoarseTimer);
|
2022-04-28 03:06:11 +05:30
|
|
|
|
2018-07-15 18:21:05 +05:30
|
|
|
connect(&saveBatchingTimer, SIGNAL(timeout()), SLOT(SaveNow()));
|
2013-09-07 01:28:51 +05:30
|
|
|
}
|
2013-09-08 05:45:20 +05:30
|
|
|
|
2013-09-07 01:28:51 +05:30
|
|
|
HttpMetaCache::~HttpMetaCache()
|
|
|
|
{
|
2018-07-15 18:21:05 +05:30
|
|
|
saveBatchingTimer.stop();
|
|
|
|
SaveNow();
|
2013-09-07 01:28:51 +05:30
|
|
|
}
|
|
|
|
|
2022-04-28 03:06:11 +05:30
|
|
|
auto HttpMetaCache::getEntry(QString base, QString resource_path) -> MetaEntryPtr
|
2013-09-07 01:28:51 +05:30
|
|
|
{
|
2018-07-15 18:21:05 +05:30
|
|
|
// no base. no base path. can't store
|
2022-04-28 03:06:11 +05:30
|
|
|
if (!m_entries.contains(base)) {
|
2018-07-15 18:21:05 +05:30
|
|
|
// TODO: log problem
|
2022-04-28 03:06:11 +05:30
|
|
|
return {};
|
2018-07-15 18:21:05 +05:30
|
|
|
}
|
2022-04-28 03:06:11 +05:30
|
|
|
|
|
|
|
EntryMap& map = m_entries[base];
|
|
|
|
if (map.entry_list.contains(resource_path)) {
|
2018-07-15 18:21:05 +05:30
|
|
|
return map.entry_list[resource_path];
|
|
|
|
}
|
2022-04-28 03:06:11 +05:30
|
|
|
|
|
|
|
return {};
|
2013-09-08 05:45:20 +05:30
|
|
|
}
|
|
|
|
|
2022-04-28 03:06:11 +05:30
|
|
|
auto HttpMetaCache::resolveEntry(QString base, QString resource_path, QString expected_etag) -> MetaEntryPtr
|
2013-09-08 05:45:20 +05:30
|
|
|
{
|
2018-07-15 18:21:05 +05:30
|
|
|
auto entry = getEntry(base, resource_path);
|
|
|
|
// it's not present? generate a default stale entry
|
2022-04-28 03:06:11 +05:30
|
|
|
if (!entry) {
|
2018-07-15 18:21:05 +05:30
|
|
|
return staleEntry(base, resource_path);
|
|
|
|
}
|
2013-11-04 07:23:05 +05:30
|
|
|
|
2022-04-28 03:06:11 +05:30
|
|
|
auto& selected_base = m_entries[base];
|
2018-07-15 18:21:05 +05:30
|
|
|
QString real_path = FS::PathCombine(selected_base.base_path, resource_path);
|
|
|
|
QFileInfo finfo(real_path);
|
2013-11-04 07:23:05 +05:30
|
|
|
|
2018-07-15 18:21:05 +05:30
|
|
|
// is the file really there? if not -> stale
|
2022-04-28 03:06:11 +05:30
|
|
|
if (!finfo.isFile() || !finfo.isReadable()) {
|
2018-07-15 18:21:05 +05:30
|
|
|
// if the file doesn't exist, we disown the entry
|
|
|
|
selected_base.entry_list.remove(resource_path);
|
|
|
|
return staleEntry(base, resource_path);
|
|
|
|
}
|
2013-11-04 07:23:05 +05:30
|
|
|
|
2022-04-28 03:06:11 +05:30
|
|
|
if (!expected_etag.isEmpty() && expected_etag != entry->etag) {
|
2018-07-15 18:21:05 +05:30
|
|
|
// if the etag doesn't match expected, we disown the entry
|
|
|
|
selected_base.entry_list.remove(resource_path);
|
|
|
|
return staleEntry(base, resource_path);
|
|
|
|
}
|
2013-11-04 07:23:05 +05:30
|
|
|
|
2018-07-15 18:21:05 +05:30
|
|
|
// if the file changed, check md5sum
|
|
|
|
qint64 file_last_changed = finfo.lastModified().toUTC().toMSecsSinceEpoch();
|
2022-04-28 03:06:11 +05:30
|
|
|
if (file_last_changed != entry->local_changed_timestamp) {
|
2018-07-15 18:21:05 +05:30
|
|
|
QFile input(real_path);
|
|
|
|
input.open(QIODevice::ReadOnly);
|
2022-04-28 03:06:11 +05:30
|
|
|
QString md5sum = QCryptographicHash::hash(input.readAll(), QCryptographicHash::Md5).toHex().constData();
|
|
|
|
if (entry->md5sum != md5sum) {
|
2018-07-15 18:21:05 +05:30
|
|
|
selected_base.entry_list.remove(resource_path);
|
|
|
|
return staleEntry(base, resource_path);
|
|
|
|
}
|
2022-04-28 03:06:11 +05:30
|
|
|
|
2018-07-15 18:21:05 +05:30
|
|
|
// md5sums matched... keep entry and save the new state to file
|
|
|
|
entry->local_changed_timestamp = file_last_changed;
|
|
|
|
SaveEventually();
|
|
|
|
}
|
2013-09-08 05:45:20 +05:30
|
|
|
|
2022-07-17 04:59:03 +05:30
|
|
|
// Get rid of old entries, to prevent cache problems
|
|
|
|
auto current_time = QDateTime::currentMSecsSinceEpoch();
|
|
|
|
auto remote_time = QDateTime::fromString(entry->remote_changed_timestamp).toMSecsSinceEpoch();
|
|
|
|
if (current_time - remote_time < TIME_TO_EXPIRE) {
|
|
|
|
qWarning() << "Removing cache entry because of old age!";
|
|
|
|
selected_base.entry_list.remove(resource_path);
|
|
|
|
return staleEntry(base, resource_path);
|
|
|
|
}
|
|
|
|
|
2018-07-15 18:21:05 +05:30
|
|
|
// entry passed all the checks we cared about.
|
|
|
|
entry->basePath = getBasePath(base);
|
|
|
|
return entry;
|
2013-09-08 05:45:20 +05:30
|
|
|
}
|
|
|
|
|
2022-04-28 03:06:11 +05:30
|
|
|
auto HttpMetaCache::updateEntry(MetaEntryPtr stale_entry) -> bool
|
2013-09-08 05:45:20 +05:30
|
|
|
{
|
2022-04-28 03:06:11 +05:30
|
|
|
if (!m_entries.contains(stale_entry->baseId)) {
|
|
|
|
qCritical() << "Cannot add entry with unknown base: " << stale_entry->baseId.toLocal8Bit();
|
2018-07-15 18:21:05 +05:30
|
|
|
return false;
|
|
|
|
}
|
2022-04-28 03:06:11 +05:30
|
|
|
|
|
|
|
if (stale_entry->stale) {
|
2018-07-15 18:21:05 +05:30
|
|
|
qCritical() << "Cannot add stale entry: " << stale_entry->getFullPath().toLocal8Bit();
|
|
|
|
return false;
|
|
|
|
}
|
2022-04-28 03:06:11 +05:30
|
|
|
|
2018-07-15 18:21:05 +05:30
|
|
|
m_entries[stale_entry->baseId].entry_list[stale_entry->relativePath] = stale_entry;
|
|
|
|
SaveEventually();
|
2022-04-28 03:06:11 +05:30
|
|
|
|
2018-07-15 18:21:05 +05:30
|
|
|
return true;
|
2013-09-08 05:45:20 +05:30
|
|
|
}
|
|
|
|
|
2022-04-28 03:06:11 +05:30
|
|
|
auto HttpMetaCache::evictEntry(MetaEntryPtr entry) -> bool
|
2015-04-01 03:53:17 +05:30
|
|
|
{
|
2022-04-28 03:06:11 +05:30
|
|
|
if (!entry)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
entry->stale = true;
|
|
|
|
SaveEventually();
|
|
|
|
return true;
|
2015-04-01 03:53:17 +05:30
|
|
|
}
|
|
|
|
|
2022-04-28 03:06:11 +05:30
|
|
|
auto HttpMetaCache::staleEntry(QString base, QString resource_path) -> MetaEntryPtr
|
2013-09-08 05:45:20 +05:30
|
|
|
{
|
2018-07-15 18:21:05 +05:30
|
|
|
auto foo = new MetaEntry();
|
|
|
|
foo->baseId = base;
|
|
|
|
foo->basePath = getBasePath(base);
|
|
|
|
foo->relativePath = resource_path;
|
|
|
|
foo->stale = true;
|
2022-04-28 03:06:11 +05:30
|
|
|
|
2018-07-15 18:21:05 +05:30
|
|
|
return MetaEntryPtr(foo);
|
2013-09-07 01:28:51 +05:30
|
|
|
}
|
|
|
|
|
2013-11-04 07:23:05 +05:30
|
|
|
void HttpMetaCache::addBase(QString base, QString base_root)
|
2013-09-07 01:28:51 +05:30
|
|
|
{
|
2018-07-15 18:21:05 +05:30
|
|
|
// TODO: report error
|
|
|
|
if (m_entries.contains(base))
|
|
|
|
return;
|
2022-04-28 03:06:11 +05:30
|
|
|
|
2018-07-15 18:21:05 +05:30
|
|
|
// TODO: check if the base path is valid
|
|
|
|
EntryMap foo;
|
|
|
|
foo.base_path = base_root;
|
|
|
|
m_entries[base] = foo;
|
2013-09-07 01:28:51 +05:30
|
|
|
}
|
|
|
|
|
2022-04-28 03:06:11 +05:30
|
|
|
auto HttpMetaCache::getBasePath(QString base) -> QString
|
2013-09-08 05:45:20 +05:30
|
|
|
{
|
2022-04-28 03:06:11 +05:30
|
|
|
if (m_entries.contains(base)) {
|
2018-07-15 18:21:05 +05:30
|
|
|
return m_entries[base].base_path;
|
|
|
|
}
|
2022-04-28 03:06:11 +05:30
|
|
|
|
|
|
|
return {};
|
2013-09-08 05:45:20 +05:30
|
|
|
}
|
|
|
|
|
2013-09-07 01:28:51 +05:30
|
|
|
void HttpMetaCache::Load()
|
|
|
|
{
|
2022-04-28 03:06:11 +05:30
|
|
|
if (m_index_file.isNull())
|
2018-07-15 18:21:05 +05:30
|
|
|
return;
|
2016-03-26 21:26:57 +05:30
|
|
|
|
2018-07-15 18:21:05 +05:30
|
|
|
QFile index(m_index_file);
|
|
|
|
if (!index.open(QIODevice::ReadOnly))
|
|
|
|
return;
|
2013-11-04 07:23:05 +05:30
|
|
|
|
2018-07-15 18:21:05 +05:30
|
|
|
QJsonDocument json = QJsonDocument::fromJson(index.readAll());
|
2022-04-28 03:06:11 +05:30
|
|
|
|
|
|
|
auto root = Json::requireObject(json, "HttpMetaCache root");
|
|
|
|
|
2018-07-15 18:21:05 +05:30
|
|
|
// check file version first
|
2022-04-28 03:06:11 +05:30
|
|
|
auto version_val = Json::ensureString(root, "version");
|
|
|
|
if (version_val != "1")
|
2018-07-15 18:21:05 +05:30
|
|
|
return;
|
2013-11-04 07:23:05 +05:30
|
|
|
|
2018-07-15 18:21:05 +05:30
|
|
|
// read the entry array
|
2022-04-28 03:06:11 +05:30
|
|
|
auto array = Json::ensureArray(root, "entries");
|
|
|
|
for (auto element : array) {
|
|
|
|
auto element_obj = Json::ensureObject(element);
|
|
|
|
auto base = Json::ensureString(element_obj, "base");
|
2018-07-15 18:21:05 +05:30
|
|
|
if (!m_entries.contains(base))
|
|
|
|
continue;
|
2022-04-28 03:06:11 +05:30
|
|
|
|
|
|
|
auto& entrymap = m_entries[base];
|
|
|
|
|
2018-07-15 18:21:05 +05:30
|
|
|
auto foo = new MetaEntry();
|
|
|
|
foo->baseId = base;
|
2022-04-28 03:06:11 +05:30
|
|
|
foo->relativePath = Json::ensureString(element_obj, "path");
|
|
|
|
foo->md5sum = Json::ensureString(element_obj, "md5sum");
|
|
|
|
foo->etag = Json::ensureString(element_obj, "etag");
|
|
|
|
foo->local_changed_timestamp = Json::ensureDouble(element_obj, "last_changed_timestamp");
|
|
|
|
foo->remote_changed_timestamp = Json::ensureString(element_obj, "remote_changed_timestamp");
|
2018-07-15 18:21:05 +05:30
|
|
|
// presumed innocent until closer examination
|
|
|
|
foo->stale = false;
|
2022-04-28 03:06:11 +05:30
|
|
|
|
|
|
|
entrymap.entry_list[foo->relativePath] = MetaEntryPtr(foo);
|
2018-07-15 18:21:05 +05:30
|
|
|
}
|
2013-09-07 01:28:51 +05:30
|
|
|
}
|
|
|
|
|
2013-09-08 19:29:50 +05:30
|
|
|
void HttpMetaCache::SaveEventually()
|
|
|
|
{
|
2018-07-15 18:21:05 +05:30
|
|
|
// reset the save timer
|
|
|
|
saveBatchingTimer.stop();
|
|
|
|
saveBatchingTimer.start(30000);
|
2013-09-08 19:29:50 +05:30
|
|
|
}
|
|
|
|
|
2013-09-08 05:45:20 +05:30
|
|
|
void HttpMetaCache::SaveNow()
|
2013-09-07 01:28:51 +05:30
|
|
|
{
|
2022-04-28 03:06:11 +05:30
|
|
|
if (m_index_file.isNull())
|
2018-07-15 18:21:05 +05:30
|
|
|
return;
|
2022-04-28 03:06:11 +05:30
|
|
|
|
2022-07-17 04:59:03 +05:30
|
|
|
qDebug() << "[HttpMetaCache]" << "Saving metacache with" << m_entries.size() << "entries";
|
|
|
|
|
2018-07-15 18:21:05 +05:30
|
|
|
QJsonObject toplevel;
|
2022-04-28 03:06:11 +05:30
|
|
|
Json::writeString(toplevel, "version", "1");
|
|
|
|
|
2018-07-15 18:21:05 +05:30
|
|
|
QJsonArray entriesArr;
|
2022-04-28 03:06:11 +05:30
|
|
|
for (auto group : m_entries) {
|
|
|
|
for (auto entry : group.entry_list) {
|
2018-07-15 18:21:05 +05:30
|
|
|
// do not save stale entries. they are dead.
|
2022-04-28 03:06:11 +05:30
|
|
|
if (entry->stale) {
|
2018-07-15 18:21:05 +05:30
|
|
|
continue;
|
|
|
|
}
|
2022-04-28 03:06:11 +05:30
|
|
|
|
2018-07-15 18:21:05 +05:30
|
|
|
QJsonObject entryObj;
|
2022-04-28 03:06:11 +05:30
|
|
|
Json::writeString(entryObj, "base", entry->baseId);
|
|
|
|
Json::writeString(entryObj, "path", entry->relativePath);
|
|
|
|
Json::writeString(entryObj, "md5sum", entry->md5sum);
|
|
|
|
Json::writeString(entryObj, "etag", entry->etag);
|
|
|
|
entryObj.insert("last_changed_timestamp", QJsonValue(double(entry->local_changed_timestamp)));
|
2018-07-15 18:21:05 +05:30
|
|
|
if (!entry->remote_changed_timestamp.isEmpty())
|
2022-04-28 03:06:11 +05:30
|
|
|
entryObj.insert("remote_changed_timestamp", QJsonValue(entry->remote_changed_timestamp));
|
2018-07-15 18:21:05 +05:30
|
|
|
entriesArr.append(entryObj);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
toplevel.insert("entries", entriesArr);
|
2015-06-04 00:40:28 +05:30
|
|
|
|
2022-04-28 03:06:11 +05:30
|
|
|
try {
|
|
|
|
Json::write(toplevel, m_index_file);
|
|
|
|
} catch (const Exception& e) {
|
2018-07-15 18:21:05 +05:30
|
|
|
qWarning() << e.what();
|
|
|
|
}
|
2013-09-07 01:28:51 +05:30
|
|
|
}
|