more work on index

- fix index not being a static instance
 - fix small stuff related mods list UI including index updating and error messages
 - fix Unzip directories not being created relatively properly
This commit is contained in:
HJfod 2022-12-08 13:06:11 +02:00
parent 82c3179885
commit 037602ecea
8 changed files with 143 additions and 115 deletions

View file

@ -41,33 +41,12 @@ namespace geode {
IndexUpdateFilter();
};
namespace impl {
// The reason sources have private implementation events that are
// turned into the global IndexUpdateEvent is because it makes it much
// simpler to keep track of progress, what errors were received, etc.
// without having to store a ton of members
struct GEODE_DLL IndexSource final {
std::string repository;
bool isUpToDate = false;
std::string dirname() const;
};
struct GEODE_DLL SourceUpdateEvent : public Event {
const IndexSource& source;
const UpdateStatus status;
SourceUpdateEvent(IndexSource const& src, const UpdateStatus status);
};
class GEODE_DLL SourceUpdateFilter : public EventFilter<SourceUpdateEvent> {
public:
using Callback = void(SourceUpdateEvent*);
ListenerResult handle(std::function<Callback> fn, SourceUpdateEvent* event);
SourceUpdateFilter();
};
}
struct IndexSourceImpl;
struct GEODE_DLL IndexSourceImplDeleter {
void operator()(IndexSourceImpl* src);
};
struct SourceUpdateEvent;
using IndexSourcePtr = std::unique_ptr<IndexSourceImpl, IndexSourceImplDeleter>;
struct GEODE_DLL IndexItem {
std::string sourceRepository;
@ -96,17 +75,17 @@ namespace geode {
// getting the latest version of a mod as easy as items.rbegin())
using ItemVersions = std::map<size_t, IndexItemHandle>;
std::vector<impl::IndexSource> m_sources;
std::vector<IndexSourcePtr> m_sources;
std::unordered_map<std::string, UpdateStatus> m_sourceStatuses;
std::atomic<bool> m_triedToUpdate = false;
std::unordered_map<std::string, ItemVersions> m_items;
Index();
void onSourceUpdate(impl::SourceUpdateEvent* event);
void checkSourceUpdates(impl::IndexSource& src);
void downloadSource(impl::IndexSource& src);
void updateSourceFromLocal(impl::IndexSource& src);
void onSourceUpdate(SourceUpdateEvent* event);
void checkSourceUpdates(IndexSourceImpl* src);
void downloadSource(IndexSourceImpl* src);
void updateSourceFromLocal(IndexSourceImpl* src);
void cleanupItems();
public:
@ -114,7 +93,7 @@ namespace geode {
void addSource(std::string const& repository);
void removeSource(std::string const& repository);
std::vector<impl::IndexSource> getSources() const;
std::vector<std::string> getSources() const;
std::vector<IndexItemHandle> getItems() const;
bool isKnownItem(std::string const& id, std::optional<size_t> version) const;

View file

@ -9,6 +9,48 @@
USE_GEODE_NAMESPACE();
using namespace geode::impl;
// The reason sources have private implementation events that are
// turned into the global IndexUpdateEvent is because it makes it much
// simpler to keep track of progress, what errors were received, etc.
// without having to store a ton of members
struct geode::IndexSourceImpl final {
std::string repository;
bool isUpToDate = false;
std::string dirname() const {
return string::replace(this->repository, "/", "_");
}
ghc::filesystem::path path() const {
return dirs::getIndexDir() / this->dirname();
}
};
void IndexSourceImplDeleter::operator()(IndexSourceImpl* src) {
delete src;
}
struct geode::SourceUpdateEvent : public Event {
IndexSourceImpl* source;
const UpdateStatus status;
SourceUpdateEvent(IndexSourceImpl* src, const UpdateStatus status)
: source(src), status(status) {}
};
class SourceUpdateFilter : public EventFilter<SourceUpdateEvent> {
public:
using Callback = void(SourceUpdateEvent*);
ListenerResult handle(std::function<Callback> fn, SourceUpdateEvent* event) {
fn(event);
return ListenerResult::Propagate;
}
SourceUpdateFilter() {}
};
// Save data
struct IndexSourceSaveData {
std::string downloadedCommitSHA;
};
@ -19,27 +61,6 @@ struct IndexSaveData {
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(IndexSaveData, sources);
std::string IndexSource::dirname() const {
return string::replace(this->repository, "/", "_");
}
// SourceUpdateEvent
SourceUpdateEvent::SourceUpdateEvent(
IndexSource const& src,
const UpdateStatus status
) : source(src), status(status) {}
ListenerResult SourceUpdateFilter::handle(
std::function<Callback> fn,
SourceUpdateEvent* event
) {
fn(event);
return ListenerResult::Propagate;
}
SourceUpdateFilter::SourceUpdateFilter() {}
// ModInstallEvent
ListenerResult ModInstallFilter::handle(std::function<Callback> fn, ModInstallEvent* event) {
@ -120,30 +141,34 @@ Index::Index() {
}
Index* Index::get() {
auto inst = new Index();
static auto inst = new Index();
return inst;
}
void Index::addSource(std::string const& repository) {
m_sources.push_back(IndexSource {
m_sources.emplace_back(new IndexSourceImpl {
.repository = repository
});
}
void Index::removeSource(std::string const& repository) {
ranges::remove(m_sources, [repository](IndexSource const& src) {
return src.repository == repository;
ranges::remove(m_sources, [repository](IndexSourcePtr const& src) {
return src->repository == repository;
});
}
std::vector<IndexSource> Index::getSources() const {
return m_sources;
std::vector<std::string> Index::getSources() const {
std::vector<std::string> res;
for (auto& src : m_sources) {
res.push_back(src->repository);
}
return res;
}
void Index::onSourceUpdate(SourceUpdateEvent* event) {
// save status for aggregating SourceUpdateEvents to a single global
// IndexUpdateEvent
m_sourceStatuses[event->source.repository] = event->status;
m_sourceStatuses[event->source->repository] = event->status;
// figure out aggregate event
enum { Finished, Progress, Failed, } whatToPost = Finished;
@ -203,33 +228,39 @@ void Index::onSourceUpdate(SourceUpdateEvent* event) {
}
}
void Index::checkSourceUpdates(IndexSource& src) {
if (src.isUpToDate) {
void Index::checkSourceUpdates(IndexSourceImpl* src) {
if (src->isUpToDate) {
return this->updateSourceFromLocal(src);
}
SourceUpdateEvent(src, UpdateProgress(0, "Checking status")).post();
auto data = Mod::get()->getSavedMutable<IndexSaveData>("index");
auto oldSHA = data.sources[src.repository].downloadedCommitSHA;
auto oldSHA = data.sources[src->repository].downloadedCommitSHA;
web::AsyncWebRequest()
.join(fmt::format("index-update-{}", src.repository))
.join(fmt::format("index-update-{}", src->repository))
.header(fmt::format("If-None-Match: \"{}\"", oldSHA))
.header("Accept: application/vnd.github.sha")
.fetch(fmt::format("https://api.github.com/repos/{}/commits/main", src.repository))
.fetch(fmt::format("https://api.github.com/repos/{}/commits/main", src->repository))
.text()
.then([this, &src, oldSHA](std::string const& newSHA) {
// if no new hash was given (rate limited) or the new hash is the
// same as old, then just update from local cache
if (newSHA.empty() || oldSHA == newSHA) {
.then([this, src, oldSHA](std::string const& newSHA) {
// check if should just be updated from local cache
if (
// if no new hash was given (rate limited) or the new hash is the
// same as old
(newSHA.empty() || oldSHA == newSHA) &&
// make sure the downloaded local copy actually exists
ghc::filesystem::exists(src->path()) &&
ghc::filesystem::exists(src->path() / "config.json")
) {
this->updateSourceFromLocal(src);
}
// otherwise save hash and download source
else {
auto data = Mod::get()->getSavedMutable<IndexSaveData>("index");
data.sources[src.repository].downloadedCommitSHA = newSHA;
data.sources[src->repository].downloadedCommitSHA = newSHA;
this->downloadSource(src);
}
})
.expect([&src](std::string const& err) {
.expect([src](std::string const& err) {
SourceUpdateEvent(
src,
UpdateError(fmt::format("Error checking for updates: {}", err))
@ -237,17 +268,17 @@ void Index::checkSourceUpdates(IndexSource& src) {
});
}
void Index::downloadSource(IndexSource& src) {
void Index::downloadSource(IndexSourceImpl* src) {
SourceUpdateEvent(src, UpdateProgress(0, "Beginning download")).post();
auto targetFile = dirs::getIndexDir() / fmt::format("{}.zip", src.dirname());
auto targetFile = dirs::getIndexDir() / fmt::format("{}.zip", src->dirname());
web::AsyncWebRequest()
.join(fmt::format("index-download-{}", src.repository))
.fetch(fmt::format("https://github.com/{}/zipball/main", src.repository))
.join(fmt::format("index-download-{}", src->repository))
.fetch(fmt::format("https://github.com/{}/zipball/main", src->repository))
.into(targetFile)
.then([this, &src, targetFile](auto) {
auto targetDir = dirs::getIndexDir() / src.dirname();
.then([this, src, targetFile](auto) {
auto targetDir = src->path();
// delete old unzipped index
try {
if (ghc::filesystem::exists(targetDir)) {
@ -272,12 +303,12 @@ void Index::downloadSource(IndexSource& src) {
// update index
this->updateSourceFromLocal(src);
})
.expect([&src](std::string const& err) {
.expect([src](std::string const& err) {
SourceUpdateEvent(
src, UpdateError(fmt::format("Error downloading: {}", err))
).post();
})
.progress([&src](auto&, double now, double total) {
.progress([src](auto&, double now, double total) {
SourceUpdateEvent(
src,
UpdateProgress(
@ -288,12 +319,12 @@ void Index::downloadSource(IndexSource& src) {
});
}
void Index::updateSourceFromLocal(IndexSource& src) {
void Index::updateSourceFromLocal(IndexSourceImpl* src) {
SourceUpdateEvent(src, UpdateProgress(100, "Updating local cache")).post();
// delete old items from this url if such exist
for (auto& [_, versions] : m_items) {
for (auto it = versions.begin(); it != versions.end(); ) {
if (it->second->sourceRepository == src.repository) {
if (it->second->sourceRepository == src->repository) {
it = versions.erase(it);
} else {
++it;
@ -304,10 +335,8 @@ void Index::updateSourceFromLocal(IndexSource& src) {
// read directory and add new items
try {
for (auto& dir : ghc::filesystem::directory_iterator(
dirs::getIndexDir() / src.dirname()
)) {
auto addRes = IndexItem::createFromDir(src.repository, dir);
for (auto& dir : ghc::filesystem::directory_iterator(src->path() / "mods")) {
auto addRes = IndexItem::createFromDir(src->repository, dir);
if (!addRes) {
log::warn("Unable to add index item from {}: {}", dir, addRes.unwrapErr());
continue;
@ -328,12 +357,14 @@ void Index::updateSourceFromLocal(IndexSource& src) {
});
}
} catch(std::exception& e) {
log::warn("Unable to read index source {}: {}", src.dirname(), e.what());
SourceUpdateEvent(src, fmt::format(
"Unable to read source {}", src->repository
)).post();
return;
}
// mark source as finished
src.isUpToDate = true;
src->isUpToDate = true;
SourceUpdateEvent(src, UpdateFinished()).post();
}
@ -350,7 +381,7 @@ void Index::cleanupItems() {
bool Index::isUpToDate() const {
for (auto& source : m_sources) {
if (!source.isUpToDate) {
if (!source->isUpToDate) {
return false;
}
}
@ -377,9 +408,9 @@ void Index::update(bool force) {
// update sources
for (auto& src : m_sources) {
if (force) {
this->downloadSource(src);
this->downloadSource(src.get());
} else {
this->checkSourceUpdates(src);
this->checkSourceUpdates(src.get());
}
}
});

View file

@ -30,7 +30,7 @@ VersionInfo Loader::getVersion() {
}
VersionInfo Loader::minModVersion() {
return VersionInfo { 0, 7, 0 };
return VersionInfo { 0, 6, 1 };
}
VersionInfo Loader::maxModVersion() {

View file

@ -22,6 +22,8 @@ static ModListLayer* g_instance = nullptr;
bool ModListLayer::init() {
if (!CCLayer::init()) return false;
m_indexListener.bind(this, &ModListLayer::onIndexUpdate);
auto winSize = CCDirector::sharedDirector()->getWinSize();
// create background
@ -214,22 +216,23 @@ void ModListLayer::reloadList() {
m_list->removeFromParent();
}
auto items = ModListView::modsForType(g_tab);
// create new list
auto list = ModListView::create(g_tab, m_display);
auto list = ModListView::create(items, m_display);
list->setLayer(this);
// set list status
// auto status = list->getStatusAsString();
// if (status.size()) {
// m_listLabel->setVisible(true);
// m_listLabel->setString(status.c_str());
// }
// else {
if (!items->count()) {
m_listLabel->setVisible(true);
m_listLabel->setString("No mods found");
} else {
m_listLabel->setVisible(false);
// }
}
// update index if needed
if (g_tab == ModListType::Download && !Index::get()->isUpToDate()) {
if (g_tab == ModListType::Download && !Index::get()->hasTriedToUpdate()) {
m_listLabel->setVisible(true);
m_listLabel->setString("Updating index...");
if (!m_loadingCircle) {
m_loadingCircle = LoadingCircle::create();
@ -316,6 +319,18 @@ void ModListLayer::onCheckForUpdates(CCObject*) {
Index::get()->update();
}
void ModListLayer::onIndexUpdate(IndexUpdateEvent* event) {
std::visit(makeVisitor {
[&](UpdateProgress const& prog) {},
[&](UpdateFinished const&) {
this->reloadList();
},
[&](UpdateError const& error) {
this->reloadList();
}
}, event->status);
}
void ModListLayer::textChanged(CCTextInputNode* input) {
this->reloadList();
}

View file

@ -27,6 +27,7 @@ protected:
LoadingCircle* m_loadingCircle = nullptr;
CCMenuItemSpriteExtra* m_filterBtn;
ModListDisplay m_display = ModListDisplay::Concise;
EventListener<IndexUpdateFilter> m_indexListener;
virtual ~ModListLayer();
@ -43,6 +44,7 @@ protected:
void keyDown(enumKeyCodes) override;
void textChanged(CCTextInputNode*) override;
void createSearchControl();
void onIndexUpdate(IndexUpdateEvent* event);
friend class SearchFilterPopup;

View file

@ -8,6 +8,7 @@
#include <Geode/binding/CCMenuItemSpriteExtra.hpp>
#include <Geode/binding/TableView.hpp>
#include <Geode/binding/CCMenuItemToggler.hpp>
#include <Geode/binding/CCContentLayer.hpp>
#include <Geode/loader/Mod.hpp>
#include <Geode/utils/casts.hpp>
#include <Geode/utils/cocos.hpp>
@ -101,7 +102,7 @@ bool ModListView::init(CCArray* mods, ModListDisplay display) {
return CustomListView::init(mods, BoomListType::Default, 358.f, 190.f);
}
CCArray* ModListView::getModsForType(ModListType type) {
CCArray* ModListView::modsForType(ModListType type) {
auto mods = CCArray::create();
switch (type) {
default:
@ -149,7 +150,7 @@ ModListView* ModListView::create(CCArray* mods, ModListDisplay display) {
}
ModListView* ModListView::create(ModListType type, ModListDisplay display) {
return ModListView::create(getModsForType(type), display);
return ModListView::create(modsForType(type), display);
}
void ModListView::setLayer(ModListLayer* layer) {

View file

@ -59,7 +59,7 @@ protected:
public:
static ModListView* create(CCArray* mods, ModListDisplay display);
static ModListView* create(ModListType type, ModListDisplay display);
static CCArray* getModsForType(ModListType type);
static CCArray* modsForType(ModListType type);
void updateAllStates(ModListCell* except = nullptr);
void setLayer(ModListLayer* layer);

View file

@ -285,19 +285,19 @@ Result<> Unzip::extractTo(Path const& name, Path const& path) {
Result<> Unzip::extractAllTo(Path const& dir) {
GEODE_UNWRAP(file::createDirectoryAll(dir));
for (auto& [entry, info] : m_impl->entries()) {
if (info.isDirectory) {
GEODE_UNWRAP(file::createDirectoryAll(entry));
} else {
// make sure zip files like root/../../file.txt don't get extracted to
// avoid zip attacks
if (!ghc::filesystem::relative(dir / entry, dir).empty()) {
GEODE_UNWRAP(this->extractTo(entry, dir / entry));
// make sure zip files like root/../../file.txt don't get extracted to
// avoid zip attacks
if (!ghc::filesystem::relative(dir / entry, dir).empty()) {
if (info.isDirectory) {
GEODE_UNWRAP(file::createDirectoryAll(dir / entry));
} else {
log::error(
"Zip entry '{}' is not contained within zip bounds",
dir / entry
);
GEODE_UNWRAP(this->extractTo(entry, dir / entry));
}
} else {
log::error(
"Zip entry '{}' is not contained within zip bounds",
dir / entry
);
}
}
return Ok();