// SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 flowln * * 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. * * 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. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * 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. */ #include "HttpMetaCache.h" #include "FileSystem.h" #include "Json.h" #include #include #include #include #include auto MetaEntry::getFullPath() -> QString { // 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(); } auto HttpMetaCache::getEntry(QString base, QString resource_path) -> MetaEntryPtr { // no base. no base path. can't store if (!m_entries.contains(base)) { // TODO: log problem return {}; } EntryMap& map = m_entries[base]; if (map.entry_list.contains(resource_path)) { return map.entry_list[resource_path]; } return {}; } auto HttpMetaCache::resolveEntry(QString base, QString resource_path, QString expected_etag) -> MetaEntryPtr { 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(); } // Get rid of old entries, to prevent cache problems auto current_time = QDateTime::currentSecsSinceEpoch(); if (entry->isExpired(current_time - ( file_last_changed / 1000 ))) { qWarning() << "Removing cache entry because of old age!"; selected_base.entry_list.remove(resource_path); return staleEntry(base, resource_path); } // entry passed all the checks we cared about. entry->basePath = getBasePath(base); return entry; } auto HttpMetaCache::updateEntry(MetaEntryPtr stale_entry) -> bool { 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; } auto HttpMetaCache::evictEntry(MetaEntryPtr entry) -> bool { if (!entry) return false; entry->stale = true; SaveEventually(); return true; } auto HttpMetaCache::staleEntry(QString base, QString resource_path) -> MetaEntryPtr { 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; } auto HttpMetaCache::getBasePath(QString base) -> QString { if (m_entries.contains(base)) { return m_entries[base].base_path; } return {}; } 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()); auto root = Json::requireObject(json, "HttpMetaCache root"); // check file version first auto version_val = Json::ensureString(root, "version"); if (version_val != "1") return; // read the entry array auto array = Json::ensureArray(root, "entries"); for (auto element : array) { auto element_obj = Json::ensureObject(element); auto base = Json::ensureString(element_obj, "base"); if (!m_entries.contains(base)) continue; auto& entrymap = m_entries[base]; auto foo = new MetaEntry(); foo->baseId = base; 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"); foo->makeEternal(Json::ensureBoolean(element_obj, "eternal", false)); if (!foo->isEternal()) { foo->current_age = Json::ensureDouble(element_obj, "current_age"); foo->max_age = Json::ensureDouble(element_obj, "max_age"); } // presumed innocent until closer examination foo->stale = false; entrymap.entry_list[foo->relativePath] = MetaEntryPtr(foo); } } void HttpMetaCache::SaveEventually() { // reset the save timer saveBatchingTimer.stop(); saveBatchingTimer.start(30000); } void HttpMetaCache::SaveNow() { if (m_index_file.isNull()) return; qDebug() << "[HttpMetaCache]" << "Saving metacache with" << m_entries.size() << "entries"; QJsonObject toplevel; Json::writeString(toplevel, "version", "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; 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))); if (!entry->remote_changed_timestamp.isEmpty()) entryObj.insert("remote_changed_timestamp", QJsonValue(entry->remote_changed_timestamp)); if (entry->isEternal()) { entryObj.insert("eternal", true); } else { entryObj.insert("current_age", QJsonValue(double(entry->current_age))); entryObj.insert("max_age", QJsonValue(double(entry->max_age))); } entriesArr.append(entryObj); } } toplevel.insert("entries", entriesArr); try { Json::write(toplevel, m_index_file); } catch (const Exception& e) { qWarning() << e.what(); } }