diff --git a/sphaira/include/download.hpp b/sphaira/include/download.hpp index 19e39cc..ec9c176 100644 --- a/sphaira/include/download.hpp +++ b/sphaira/include/download.hpp @@ -1,41 +1,159 @@ #pragma once +#include "fs.hpp" #include #include #include +#include +#include #include -namespace sphaira { +namespace sphaira::curl { -using DownloadCallback = std::function& data, bool success)>; -using ProgressCallback = std::function; - -enum class DownloadPriority { +enum class Priority { Normal, // gets pushed to the back of the queue High, // gets pushed to the front of the queue }; +using Path = fs::FsPath; +using Header = std::unordered_map; +using OnComplete = std::function& data, bool success, long code)>; +using OnProgress = std::function; + +struct Url { + Url() = default; + Url(const std::string& str) : m_str{str} {} + std::string m_str; +}; + +struct Fields { + Fields() = default; + Fields(const std::string& str) : m_str{str} {} + std::string m_str; +}; + +struct Api; + struct DownloadEventData { - DownloadCallback callback; + OnComplete callback; std::vector data; + long code; bool result; }; -auto DownloadInit() -> bool; -void DownloadExit(); +auto Init() -> bool; +void Exit(); // sync functions -auto DownloadMemory(const std::string& url, const std::string& post, ProgressCallback pcallback = nullptr) -> std::vector; -auto DownloadFile(const std::string& url, const std::string& out, const std::string& post, ProgressCallback pcallback = nullptr) -> bool; +auto ToMemory(const Api& e) -> std::vector; +auto ToFile(const Api& e) -> bool; + // async functions -// starts the downloads in a new thread, pushes an event when complete -// then, the callback will be called on the main thread. -// auto DownloadMemoryAsync(const std::string& url, DownloadCallback callback, DownloadPriority prio = DownloadPriority::Normal) -> bool; -// auto DownloadFileAsync(const std::string& url, const std::string& out, DownloadCallback callback, DownloadPriority prio = DownloadPriority::Normal) -> bool; +auto ToMemoryAsync(const Api& e) -> bool; +auto ToFileAsync(const Api& e) -> bool; -auto DownloadMemoryAsync(const std::string& url, const std::string& post, DownloadCallback callback, ProgressCallback pcallback = nullptr, DownloadPriority prio = DownloadPriority::Normal) -> bool; -auto DownloadFileAsync(const std::string& url, const std::string& out, const std::string& post, DownloadCallback callback, ProgressCallback pcallback = nullptr, DownloadPriority prio = DownloadPriority::Normal) -> bool; +// removes url from cache (todo: deprecate this) +void ClearCache(const Url& url); -void DownloadClearCache(const std::string& url); +struct Api { + Api() = default; -} // namespace sphaira + template + Api(Ts&&... ts) { + Api::set_option(std::forward(ts)...); + } + + template + auto To(Ts&&... ts) { + if constexpr(std::disjunction_v...>) { + return ToFile(std::forward(ts)...); + } else { + return ToMemory(std::forward(ts)...); + } + } + + template + auto ToAsync(Ts&&... ts) { + if constexpr(std::disjunction_v...>) { + return ToFileAsync(std::forward(ts)...); + } else { + return ToMemoryAsync(std::forward(ts)...); + } + } + + template + auto ToMemory(Ts&&... ts) { + static_assert(std::disjunction_v...>, "Url must be specified"); + Api::set_option(std::forward(ts)...); + return curl::ToMemory(*this); + } + + template + auto ToFile(Ts&&... ts) { + static_assert(std::disjunction_v...>, "Url must be specified"); + static_assert(std::disjunction_v...>, "Path must be specified"); + Api::set_option(std::forward(ts)...); + return curl::ToFile(*this); + } + + template + auto ToMemoryAsync(Ts&&... ts) { + static_assert(std::disjunction_v...>, "Url must be specified"); + static_assert(std::disjunction_v...>, "OnComplete must be specified"); + Api::set_option(std::forward(ts)...); + return curl::ToMemoryAsync(*this); + } + + template + auto ToFileAsync(Ts&&... ts) { + static_assert(std::disjunction_v...>, "Url must be specified"); + static_assert(std::disjunction_v...>, "Path must be specified"); + static_assert(std::disjunction_v...>, "OnComplete must be specified"); + Api::set_option(std::forward(ts)...); + return curl::ToFileAsync(*this); + } + + Url m_url; + Fields m_fields{}; + Header m_header{}; + Path m_path{}; + OnComplete m_on_complete = nullptr; + OnProgress m_on_progress = nullptr; + Priority m_prio = Priority::Normal; + +private: + void SetOption(Url&& v) { + m_url = v; + } + void SetOption(Fields&& v) { + m_fields = v; + } + void SetOption(Header&& v) { + m_header = v; + } + void SetOption(Path&& v) { + m_path = v; + } + void SetOption(OnComplete&& v) { + m_on_complete = v; + } + void SetOption(OnProgress&& v) { + m_on_progress = v; + } + void SetOption(Priority&& v) { + m_prio = v; + } + + template + void set_option(T&& t) { + SetOption(std::forward(t)); + } + + template + void set_option(T&& t, Ts&&... ts) { + set_option(std::forward(t)); + set_option(std::forward(ts)...); + } +}; + +} // namespace sphaira::curl diff --git a/sphaira/include/evman.hpp b/sphaira/include/evman.hpp index 43c4d7b..092bd62 100644 --- a/sphaira/include/evman.hpp +++ b/sphaira/include/evman.hpp @@ -26,7 +26,7 @@ using EventData = std::variant< ExitEventData, HazeCallbackData, NxlinkCallbackData, - DownloadEventData + curl::DownloadEventData >; // returns number of events diff --git a/sphaira/include/ui/progress_box.hpp b/sphaira/include/ui/progress_box.hpp index 73bea83..fdb75c8 100644 --- a/sphaira/include/ui/progress_box.hpp +++ b/sphaira/include/ui/progress_box.hpp @@ -30,6 +30,16 @@ struct ProgressBox final : Widget { auto CopyFile(const fs::FsPath& src, const fs::FsPath& dst) -> Result; void Yield(); + auto OnDownloadProgressCallback() { + return [this](u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow){ + if (this->ShouldExit()) { + return false; + } + this->UpdateTransfer(dlnow, dltotal); + return true; + }; + } + public: struct ThreadData { ProgressBox* pbox; diff --git a/sphaira/source/app.cpp b/sphaira/source/app.cpp index d7192ac..a3c6d82 100644 --- a/sphaira/source/app.cpp +++ b/sphaira/source/app.cpp @@ -276,9 +276,9 @@ void App::Loop() { App::Notify("Nxlink Finished"_i18n); break; } - } else if constexpr(std::is_same_v) { + } else if constexpr(std::is_same_v) { log_write("[DownloadEventData] got event\n"); - arg.callback(arg.data, arg.result); + arg.callback(arg.data, arg.result, arg.code); } else { static_assert(false, "non-exhaustive visitor!"); } @@ -941,7 +941,7 @@ App::App(const char* argv0) { nxlinkInitialize(nxlink_callback); } - DownloadInit(); + curl::Init(); // Create the deko3d device this->device = dk::DeviceMaker{} @@ -1096,7 +1096,7 @@ App::~App() { log_write("starting to exit\n"); i18n::exit(); - DownloadExit(); + curl::Exit(); // this has to be called before any cleanup to ensure the lifetime of // nvg is still active as some widgets may need to free images. diff --git a/sphaira/source/download.cpp b/sphaira/source/download.cpp index e677989..b7a78e0 100644 --- a/sphaira/source/download.cpp +++ b/sphaira/source/download.cpp @@ -11,9 +11,11 @@ #include #include -namespace sphaira { +namespace sphaira::curl { namespace { +using DownloadResult = std::pair; + #define CURL_EASY_SETOPT_LOG(handle, opt, v) \ if (auto r = curl_easy_setopt(handle, opt, v); r != CURLE_OK) { \ log_write("curl_easy_setopt(%s, %s) msg: %s\n", #opt, #v, curl_easy_strerror(r)); \ @@ -39,11 +41,14 @@ CURLSH* g_curl_share{}; Mutex g_mutex_share[CURL_LOCK_DATA_LAST]{}; struct UrlCache { - auto AddToCache(const std::string& url, bool force = false) { + auto AddToCache(const Url& url, bool force = false) { mutexLock(&mutex); ON_SCOPE_EXIT(mutexUnlock(&mutex)); - auto it = std::find(cache.begin(), cache.end(), url); - if (it == cache.end()) { + auto it = std::find_if(cache.cbegin(), cache.cend(), [&url](const auto& e){ + return e.m_str == url.m_str; + }); + + if (it == cache.cend()) { cache.emplace_back(url); return true; } else { @@ -55,16 +60,19 @@ struct UrlCache { } } - void RemoveFromCache(const std::string& url) { + void RemoveFromCache(const Url& url) { mutexLock(&mutex); ON_SCOPE_EXIT(mutexUnlock(&mutex)); - auto it = std::find(cache.begin(), cache.end(), url); - if (it != cache.end()) { + auto it = std::find_if(cache.cbegin(), cache.cend(), [&url](const auto& e){ + return e.m_str == url.m_str; + }); + + if (it != cache.cend()) { cache.erase(it); } } - std::vector cache; + std::vector cache; Mutex mutex{}; }; @@ -101,7 +109,7 @@ struct ThreadEntry { return m_in_progress == true; } - auto Setup(DownloadCallback callback, ProgressCallback pcallback, std::string url, std::string file, std::string post) -> bool { + auto Setup(const Api& api) -> bool { assert(m_in_progress == false && "Setting up thread while active"); mutexLock(&m_mutex); ON_SCOPE_EXIT(mutexUnlock(&m_mutex)); @@ -109,11 +117,7 @@ struct ThreadEntry { if (m_in_progress) { return false; } - m_url = url; - m_file = file; - m_post = post; - m_callback = callback; - m_pcallback = pcallback; + m_api = api; m_in_progress = true; // log_write("started download :)\n"); ueventSignal(&m_uevent); @@ -122,22 +126,14 @@ struct ThreadEntry { CURL* m_curl{}; Thread m_thread{}; - std::string m_url{}; - std::string m_file{}; // if empty, downloads to buffer - std::string m_post{}; // if empty, downloads to buffer - DownloadCallback m_callback{}; - ProgressCallback m_pcallback{}; + Api m_api{}; std::atomic_bool m_in_progress{}; Mutex m_mutex{}; UEvent m_uevent{}; }; struct ThreadQueueEntry { - std::string url; - std::string file; - std::string post; - DownloadCallback callback; - ProgressCallback pcallback; + Api api; bool m_delete{}; }; @@ -160,22 +156,18 @@ struct ThreadQueue { threadClose(&m_thread); } - auto Add(DownloadPriority prio, DownloadCallback callback, ProgressCallback pcallback, std::string url, std::string file, std::string post) -> bool { + auto Add(const Api& api) -> bool { mutexLock(&m_mutex); ON_SCOPE_EXIT(mutexUnlock(&m_mutex)); ThreadQueueEntry entry{}; - entry.url = url; - entry.file = file; - entry.post = post; - entry.callback = callback; - entry.pcallback = pcallback; + entry.api = api; - switch (prio) { - case DownloadPriority::Normal: + switch (api.m_prio) { + case Priority::Normal: m_entries.emplace_back(entry); break; - case DownloadPriority::High: + case Priority::High: m_entries.emplace_front(entry); break; } @@ -216,7 +208,7 @@ auto ProgressCallbackFunc2(void *clientp, curl_off_t dltotal, curl_off_t dlnow, } // log_write("pcall called %u %u %u %u\n", dltotal, dlnow, ultotal, ulnow); - auto callback = *static_cast(clientp); + auto callback = *static_cast(clientp); if (!callback(dltotal, dlnow, ultotal, ulnow)) { return 1; } @@ -283,36 +275,36 @@ auto WriteFileCallback(void *contents, size_t size, size_t num_files, void *user return realsize; } -auto DownloadInternal(CURL* curl, DataStruct& chunk, ProgressCallback pcallback, const std::string& url, const std::string& file, const std::string& post) -> bool { +auto DownloadInternal(CURL* curl, DataStruct& chunk, const Api& e) -> DownloadResult { fs::FsPath safe_buf; fs::FsPath tmp_buf; - const bool has_file = !file.empty() && file != ""; - const bool has_post = !post.empty() && post != ""; + const bool has_file = !e.m_path.empty() && e.m_path != ""; + const bool has_post = !e.m_fields.m_str.empty() && e.m_fields.m_str != ""; ON_SCOPE_EXIT(if (has_file) { fsFsClose(&chunk.fs); } ); if (has_file) { - std::strcpy(safe_buf, file.c_str()); + std::strcpy(safe_buf, e.m_path); GetDownloadTempPath(tmp_buf); - R_TRY_RESULT(fsOpenSdCardFileSystem(&chunk.fs), false); + R_TRY_RESULT(fsOpenSdCardFileSystem(&chunk.fs), {}); fs::CreateDirectoryRecursivelyWithPath(&chunk.fs, tmp_buf); if (auto rc = fsFsCreateFile(&chunk.fs, tmp_buf, 0, 0); R_FAILED(rc) && rc != FsError_PathAlreadyExists) { log_write("failed to create file: %s\n", tmp_buf); - return false; + return {}; } if (R_FAILED(fsFsOpenFile(&chunk.fs, tmp_buf, FsOpenMode_Write|FsOpenMode_Append, &chunk.f))) { log_write("failed to open file: %s\n", tmp_buf); - return false; + return {}; } } // reserve the first chunk chunk.data.reserve(CHUNK_SIZE); - CURL_EASY_SETOPT_LOG(curl, CURLOPT_URL, url.c_str()); + CURL_EASY_SETOPT_LOG(curl, CURLOPT_URL, e.m_url.m_str.c_str()); CURL_EASY_SETOPT_LOG(curl, CURLOPT_USERAGENT, "TotalJustice"); CURL_EASY_SETOPT_LOG(curl, CURLOPT_FOLLOWLOCATION, 1L); CURL_EASY_SETOPT_LOG(curl, CURLOPT_SSL_VERIFYPEER, 0L); @@ -322,13 +314,36 @@ auto DownloadInternal(CURL* curl, DataStruct& chunk, ProgressCallback pcallback, CURL_EASY_SETOPT_LOG(curl, CURLOPT_BUFFERSIZE, 1024*512); if (has_post) { - CURL_EASY_SETOPT_LOG(curl, CURLOPT_POSTFIELDS, post.c_str()); - log_write("setting post field: %s\n", post.c_str()); + CURL_EASY_SETOPT_LOG(curl, CURLOPT_POSTFIELDS, e.m_fields.m_str.c_str()); + log_write("setting post field: %s\n", e.m_fields.m_str.c_str()); + } + + struct curl_slist* list = NULL; + ON_SCOPE_EXIT(if (list) { curl_slist_free_all(list); } ); + + for (auto& [key, value] : e.m_header) { + // append value (if any). + auto header_str = key; + if (value.empty()) { + header_str += ":"; + } else { + header_str += ": " + value; + } + + // try to append header chunk. + auto temp = curl_slist_append(list, header_str.c_str()); + if (temp) { + list = temp; + } + } + + if (list) { + CURL_EASY_SETOPT_LOG(curl, CURLOPT_HTTPHEADER, list); } // progress calls. - if (pcallback) { - CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFODATA, &pcallback); + if (e.m_on_progress) { + CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFODATA, &e.m_on_progress); CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFOFUNCTION, ProgressCallbackFunc2); } else { CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFOFUNCTION, ProgressCallbackFunc1); @@ -343,6 +358,9 @@ auto DownloadInternal(CURL* curl, DataStruct& chunk, ProgressCallback pcallback, const auto res = curl_easy_perform(curl); bool success = res == CURLE_OK; + long http_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + if (has_file) { if (res == CURLE_OK && chunk.offset) { fsFileWrite(&chunk.f, chunk.file_offset, chunk.data.data(), chunk.offset, FsWriteOption_None); @@ -365,18 +383,18 @@ auto DownloadInternal(CURL* curl, DataStruct& chunk, ProgressCallback pcallback, } } - log_write("Downloaded %s %s\n", url.c_str(), curl_easy_strerror(res)); - return success; + log_write("Downloaded %s %s\n", e.m_url.m_str.c_str(), curl_easy_strerror(res)); + return {success, http_code}; } -auto DownloadInternal(DataStruct& chunk, ProgressCallback pcallback, const std::string& url, const std::string& file, const std::string& post) -> bool { +auto DownloadInternal(DataStruct& chunk, const Api& e) -> DownloadResult { auto curl = curl_easy_init(); if (!curl) { log_write("curl init failed\n"); - return false; + return {}; } ON_SCOPE_EXIT(curl_easy_cleanup(curl)); - return DownloadInternal(curl, chunk, pcallback, url, file, post); + return DownloadInternal(curl, chunk, e); } void DownloadThread(void* p) { @@ -393,9 +411,9 @@ void DownloadThread(void* p) { DataStruct chunk; #if 1 - const auto result = DownloadInternal(data->m_curl, chunk, data->m_pcallback, data->m_url, data->m_file, data->m_post); + const auto [result, code] = DownloadInternal(data->m_curl, chunk, data->m_api); if (g_running) { - DownloadEventData event_data{data->m_callback, std::move(chunk.data), result}; + DownloadEventData event_data{data->m_api.m_on_complete, std::move(chunk.data), code, result}; evman::push(std::move(event_data), false); } else { break; @@ -444,7 +462,7 @@ void DownloadThreadQueue(void* p) { } if (!thread.InProgress()) { - thread.Setup(entry.callback, entry.pcallback, entry.url, entry.file, entry.post); + thread.Setup(entry.api); // log_write("[dl queue] starting download\n"); // mark entry for deletion entry.m_delete = true; @@ -467,13 +485,6 @@ void DownloadThreadQueue(void* p) { for (u32 i = 0; i < pop_count; i++) { data->m_entries.pop_front(); } - // if (delete_any) { - // data->m_entries.clear(); - // data->m_entries. - // data->m_entries.erase(std::remove_if(data->m_entries.begin(), data->m_entries.end(), [](auto& a) { - // return a.m_delete; - // })); - // } } log_write("exited download thread queue\n"); @@ -489,7 +500,7 @@ void my_unlock(CURL *handle, curl_lock_data data, void *useptr) { } // namespace -auto DownloadInit() -> bool { +auto Init() -> bool { if (CURLE_OK != curl_global_init(CURL_GLOBAL_DEFAULT)) { return false; } @@ -521,7 +532,7 @@ auto DownloadInit() -> bool { return true; } -void DownloadExit() { +void Exit() { g_running = false; g_thread_queue.Close(); @@ -538,30 +549,38 @@ void DownloadExit() { curl_global_cleanup(); } -auto DownloadMemory(const std::string& url, const std::string& post, ProgressCallback pcallback) -> std::vector { - if (g_url_cache.AddToCache(url)) { +auto ToMemory(const Api& e) -> std::vector { + if (!e.m_path.empty()) { + return {}; + } + + if (g_url_cache.AddToCache(e.m_url)) { DataStruct chunk{}; - if (DownloadInternal(chunk, pcallback, url, "", post)) { + if (DownloadInternal(chunk, e).first) { return chunk.data; } } return {}; } -auto DownloadFile(const std::string& url, const std::string& out, const std::string& post, ProgressCallback pcallback) -> bool { - if (g_url_cache.AddToCache(url)) { +auto ToFile(const Api& e) -> bool { + if (e.m_path.empty()) { + return false; + } + + if (g_url_cache.AddToCache(e.m_url)) { DataStruct chunk{}; - if (DownloadInternal(chunk, pcallback, url, out, post)) { + if (DownloadInternal(chunk, e).first) { return true; } } return false; } -auto DownloadMemoryAsync(const std::string& url, const std::string& post, DownloadCallback callback, ProgressCallback pcallback, DownloadPriority prio) -> bool { +auto ToMemoryAsync(const Api& api) -> bool { #if USE_THREAD_QUEUE - if (g_url_cache.AddToCache(url)) { - return g_thread_queue.Add(prio, callback, pcallback, url, "", post); + if (g_url_cache.AddToCache(api.m_url)) { + return g_thread_queue.Add(api); } else { return false; } @@ -580,10 +599,10 @@ auto DownloadMemoryAsync(const std::string& url, const std::string& post, Downlo #endif } -auto DownloadFileAsync(const std::string& url, const std::string& out, const std::string& post, DownloadCallback callback, ProgressCallback pcallback, DownloadPriority prio) -> bool { +auto ToFileAsync(const Api& e) -> bool { #if USE_THREAD_QUEUE - if (g_url_cache.AddToCache(url)) { - return g_thread_queue.Add(prio, callback, pcallback, url, out, post); + if (g_url_cache.AddToCache(e.m_url)) { + return g_thread_queue.Add(e); } else { return false; } @@ -602,9 +621,9 @@ auto DownloadFileAsync(const std::string& url, const std::string& out, const std #endif } -void DownloadClearCache(const std::string& url) { +void ClearCache(const Url& url) { g_url_cache.AddToCache(url); g_url_cache.RemoveFromCache(url); } -} // namespace sphaira +} // namespace sphaira::curl diff --git a/sphaira/source/ui/menus/appstore.cpp b/sphaira/source/ui/menus/appstore.cpp index c92fe72..0c63b65 100644 --- a/sphaira/source/ui/menus/appstore.cpp +++ b/sphaira/source/ui/menus/appstore.cpp @@ -398,17 +398,13 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> bool { log_write("starting download\n"); const auto url = BuildZipUrl(entry); - if (!DownloadFile(url, zip_out, "", [pbox](u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow){ - if (pbox->ShouldExit()) { - return false; - } - pbox->UpdateTransfer(dlnow, dltotal); - return true; - })) { + if (!curl::Api().ToFile( + curl::Url{url}, + curl::Path{zip_out}, + curl::OnProgress{pbox->OnDownloadProgressCallback()} + )) { log_write("error with download\n"); - // push popup error box return false; - // return appletEnterFatalSection(); } } @@ -682,36 +678,29 @@ EntryMenu::EntryMenu(Entry& entry, const LazyImage& default_icon, Menu& menu) const auto post = "name=" "switch_user" "&package=" + m_entry.name + "&message=" + out; const auto file = BuildFeedbackCachePath(m_entry); - DownloadFileAsync(URL_POST_FEEDBACK, file, post, [](std::vector& data, bool success){ - if (success) { - log_write("got feedback!\n"); - } else { - log_write("failed to send feedback :("); + curl::Api().ToAsync( + curl::Url{URL_POST_FEEDBACK}, + curl::Path{file}, + curl::Fields{post}, + curl::OnComplete{[](std::vector& data, bool success, long code){ + if (success) { + log_write("got feedback!\n"); + } else { + log_write("failed to send feedback :("); + } } }); } }, true)); App::Push(sidebar); }}), - // std::make_pair(Button::A, Action{m_entry.status == EntryStatus::Update ? "Update" : "Install", [this](){ - // App::Push(std::make_shared("App Install", [this](auto pbox){ - // InstallApp(pbox, m_entry); - // }, 2)); - // }}), std::make_pair(Button::B, Action{"Back"_i18n, [this](){ SetPop(); }}) ); - // SidebarEntryCallback - // if (!m_entries_current.empty() && !GetEntry().url.empty()) { - // options->Add(std::make_shared("Show Release Page")) - // } - SetTitleSubHeading("by " + m_entry.author); - // const char* very_long = "total@fedora:~/dev/switch/sphaira$ nxlink build/MinSizeRel/*.nro total@fedora:~/dev/switch/sphaira$ nxlink build/MinSizeRel/*.nro total@fedora:~/dev/switch/sphaira$ nxlink build/MinSizeRel/*.nro total@fedora:~/dev/switch/sphaira$ nxlink build/MinSizeRel/*.nro"; - m_details = std::make_shared(m_entry.details, 0, 374, 250, 768, 18); m_changelog = std::make_shared(m_entry.changelog, 0, 374, 250, 768, 18); @@ -727,31 +716,18 @@ EntryMenu::EntryMenu(Entry& entry, const LazyImage& default_icon, Menu& menu) // race condition if we pop the widget before the download completes if (!m_banner.image) { - DownloadFileAsync(url, path, "", [this, path](std::vector& data, bool success){ - if (success) { - EntryLoadImageFile(path, m_banner); + curl::Api().ToFileAsync( + curl::Url{url}, + curl::Path{path}, + curl::Priority::High, + curl::OnComplete{[this, path](std::vector& data, bool success, long code){ + if (success) { + EntryLoadImageFile(path, m_banner); + } } - }, nullptr, DownloadPriority::High); + }); } - // ignore screen shots, most apps don't have any sadly. - #if 0 - m_screens.resize(m_entry.screens); - - for (u32 i = 0; i < m_screens.size(); i++) { - path = BuildScreensCachePath(m_entry.name, i); - url = BuildScreensUrl(m_entry.name, i); - - if (fs::file_exists(path.c_str())) { - EntryLoadImageFile(path, m_screens[i]); - } else { - DownloadFileAsync(url.c_str(), path.c_str(), [this, i, path](std::vector& data, bool success){ - EntryLoadImageFile(path, m_screens[i]); - }, nullptr, DownloadPriority::High); - } - } - #endif - SetSubHeading(m_entry.binary); SetSubHeading(m_entry.description); UpdateOptions(); @@ -780,30 +756,17 @@ void EntryMenu::Draw(NVGcontext* vg, Theme* theme) { DrawIcon(vg, m_banner, m_entry.image.image ? m_entry.image : m_default_icon, banner_vec, false); DrawIcon(vg, m_entry.image, m_default_icon, icon_vec); - // gfx::drawImage(vg, icon_vec, m_entry.image.image); constexpr float text_start_x = icon_vec.x;// - 10; float text_start_y = 218 + line_vec.y; const float text_inc_y = 32; const float font_size = 20; - // gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->elements[ThemeEntryID_TEXT].colour, "%s", m_entry.name.c_str()); - // gfx::drawTextBox(vg, text_start_x - 20, text_start_y, font_size, icon_vec.w + 20*2, theme->elements[ThemeEntryID_TEXT].colour, m_entry.description.c_str(), NVG_ALIGN_CENTER); - // text_start_y += text_inc_y * 2.0; - - // gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->elements[ThemeEntryID_TEXT].colour, "author: %s", m_entry.author.c_str()); - // text_start_y += text_inc_y; gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->elements[ThemeEntryID_TEXT].colour, "version: %s"_i18n.c_str(), m_entry.version.c_str()); text_start_y += text_inc_y; gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->elements[ThemeEntryID_TEXT].colour, "updated: %s"_i18n.c_str(), m_entry.updated.c_str()); text_start_y += text_inc_y; gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->elements[ThemeEntryID_TEXT].colour, "category: %s"_i18n.c_str(), m_entry.category.c_str()); text_start_y += text_inc_y; - // gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->elements[ThemeEntryID_TEXT].colour, "license: %s", m_entry.license.c_str()); - // text_start_y += text_inc_y; - // gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->elements[ThemeEntryID_TEXT].colour, "title: %s", m_entry.title.c_str()); - // text_start_y += text_inc_y; - // gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->elements[ThemeEntryID_TEXT].colour, "filesize: %.2f MiB", (double)m_entry.filesize / 1024.0); - // text_start_y += text_inc_y; gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->elements[ThemeEntryID_TEXT].colour, "extracted: %.2f MiB"_i18n.c_str(), (double)m_entry.extracted / 1024.0); text_start_y += text_inc_y; gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->elements[ThemeEntryID_TEXT].colour, "app_dls: %s"_i18n.c_str(), AppDlToStr(m_entry.app_dls).c_str()); @@ -1058,20 +1021,6 @@ Menu::Menu(const std::vector& nro_entries) : MenuBase{"AppStore"_i18n} ); m_repo_download_state = ImageDownloadState::Progress; - #if 0 - DownloadMemoryAsync(URL_JSON, [this](std::vector& data, bool success){ - if (success) { - repo_json = data; - repo_json.push_back('\0'); - m_repo_download_state = ImageDownloadState::Done; - if (HasFocus()) { - ScanHomebrew(); - } - } else { - m_repo_download_state = ImageDownloadState::Failed; - } - }); - #else FsTimeStampRaw time_stamp{}; u64 current_time{}; bool download_file = false; @@ -1102,20 +1051,23 @@ Menu::Menu(const std::vector& nro_entries) : MenuBase{"AppStore"_i18n} // download_file = true; if (download_file) { - DownloadFileAsync(URL_JSON, REPO_PATH, "", [this](std::vector& data, bool success){ - if (success) { - m_repo_download_state = ImageDownloadState::Done; - if (HasFocus()) { - ScanHomebrew(); + curl::Api().ToFileAsync( + curl::Url{URL_JSON}, + curl::Path{REPO_PATH}, + curl::OnComplete{[this](std::vector& data, bool success, long code){ + if (success) { + m_repo_download_state = ImageDownloadState::Done; + if (HasFocus()) { + ScanHomebrew(); + } + } else { + m_repo_download_state = ImageDownloadState::Failed; } - } else { - m_repo_download_state = ImageDownloadState::Failed; } }); } else { m_repo_download_state = ImageDownloadState::Done; } - #endif m_filter = (Filter)ini_getl(INI_SECTION, "filter", m_filter, App::CONFIG_PATH); m_sort = (SortType)ini_getl(INI_SECTION, "sort", m_sort, App::CONFIG_PATH); @@ -1174,14 +1126,19 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) { } else { const auto url = BuildIconUrl(e); e.image.state = ImageDownloadState::Progress; - DownloadFileAsync(url, path, "", [this, index](std::vector& data, bool success) { - if (success) { - m_entries[index].image.state = ImageDownloadState::Done; - } else { - m_entries[index].image.state = ImageDownloadState::Failed; - log_write("failed to download image\n"); + curl::Api().ToFileAsync( + curl::Url{url}, + curl::Path{path}, + curl::Priority::High, + curl::OnComplete{[this, index](std::vector& data, bool success, long code) { + if (success) { + m_entries[index].image.state = ImageDownloadState::Done; + } else { + m_entries[index].image.state = ImageDownloadState::Failed; + log_write("failed to download image\n"); + } } - }, nullptr, DownloadPriority::High); + }); } } break; case ImageDownloadState::Progress: { diff --git a/sphaira/source/ui/menus/filebrowser.cpp b/sphaira/source/ui/menus/filebrowser.cpp index d30f3e5..8b4666c 100644 --- a/sphaira/source/ui/menus/filebrowser.cpp +++ b/sphaira/source/ui/menus/filebrowser.cpp @@ -232,15 +232,12 @@ auto GetRomIcon(ProgressBox* pbox, std::string filename, std::string extension, // try and download icon if (!pbox->ShouldExit()) { - // auto png_image = DownloadMemory(ra_thumbnail_url.c_str()); pbox->NewTransfer("Downloading "_i18n + gh_thumbnail_url); - auto png_image = DownloadMemory(gh_thumbnail_url, "", [pbox](u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow){ - if (pbox->ShouldExit()) { - return false; - } - pbox->UpdateTransfer(dlnow, dltotal); - return true; - }); + auto png_image = curl::Api().ToMemory( + curl::Url{gh_thumbnail_url}, + curl::OnProgress{pbox->OnDownloadProgressCallback()} + ); + if (!png_image.empty()) { return png_image; } diff --git a/sphaira/source/ui/menus/ghdl.cpp b/sphaira/source/ui/menus/ghdl.cpp index 431d531..c7a7a92 100644 --- a/sphaira/source/ui/menus/ghdl.cpp +++ b/sphaira/source/ui/menus/ghdl.cpp @@ -67,34 +67,30 @@ void from_json(const std::vector& data, GhApiEntry& e) { ); } -auto DownloadApp(ProgressBox* pbox, const GhApiAsset* gh_asset, const AssetEntry& entry = {}) -> bool { +auto DownloadApp(ProgressBox* pbox, const GhApiAsset& gh_asset, const AssetEntry& entry = {}) -> bool { + static const fs::FsPath temp_file{"/switch/sphaira/cache/ghdl.temp"}; constexpr auto chunk_size = 1024 * 512; // 512KiB fs::FsNativeSd fs; R_TRY_RESULT(fs.GetFsOpenResult(), false); - if (!gh_asset || gh_asset->browser_download_url.empty()) { + if (gh_asset.browser_download_url.empty()) { log_write("failed to find asset\n"); return false; } - static fs::FsPath temp_file{"/switch/sphaira/cache/ghdl.temp"}; - // 2. download the asset if (!pbox->ShouldExit()) { - pbox->NewTransfer("Downloading "_i18n + gh_asset->name); - log_write("starting download: %s\n", gh_asset->browser_download_url.c_str()); + pbox->NewTransfer("Downloading "_i18n + gh_asset.name); + log_write("starting download: %s\n", gh_asset.browser_download_url.c_str()); - DownloadClearCache(gh_asset->browser_download_url); - if (!DownloadFile(gh_asset->browser_download_url, temp_file, "", [pbox](u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow){ - if (pbox->ShouldExit()) { - return false; - } - pbox->UpdateTransfer(dlnow, dltotal); - return true; - })) { + curl::ClearCache(gh_asset.browser_download_url); + if (!curl::Api().ToFile( + curl::Url{gh_asset.browser_download_url}, + curl::Path{temp_file}, + curl::OnProgress{pbox->OnDownloadProgressCallback()} + )){ log_write("error with download\n"); - // push popup error box return false; } } @@ -107,7 +103,7 @@ auto DownloadApp(ProgressBox* pbox, const GhApiAsset* gh_asset, const AssetEntry } // 3. extract the zip / file - if (gh_asset->content_type == "application/zip") { + if (gh_asset.content_type == "application/zip") { log_write("found zip\n"); auto zfile = unzOpen64(temp_file); if (!zfile) { @@ -207,14 +203,11 @@ auto DownloadAssets(ProgressBox* pbox, const std::string& url, GhApiEntry& out) pbox->NewTransfer("Downloading json"_i18n); log_write("starting download\n"); - DownloadClearCache(url); - const auto json = DownloadMemory(url, "", [pbox](u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow){ - if (pbox->ShouldExit()) { - return false; - } - pbox->UpdateTransfer(dlnow, dltotal); - return true; - }); + curl::ClearCache(url); + const auto json = curl::Api().ToMemory( + curl::Url{url}, + curl::OnProgress{pbox->OnDownloadProgressCallback()} + ); if (json.empty()) { log_write("error with download\n"); @@ -241,19 +234,18 @@ auto DownloadApp(ProgressBox* pbox, const std::string& url, const AssetEntry& en return false; } - GhApiAsset* gh_asset{}; - for (auto& p : gh_entry.assets) { - if (p.name == entry.name) { - gh_asset = &p; + const auto it = std::find_if( + gh_entry.assets.cbegin(), gh_entry.assets.cend(), [&entry](auto& e) { + return entry.name == e.name; } - } + ); - if (!gh_asset || gh_asset->browser_download_url.empty()) { + if (it == gh_entry.assets.cend() || it->browser_download_url.empty()) { log_write("failed to find asset\n"); return false; } - return DownloadApp(pbox, gh_asset, entry); + return DownloadApp(pbox, *it, entry); } } // namespace @@ -313,7 +305,7 @@ Menu::Menu() : MenuBase{"GitHub"_i18n} { const auto index = *op_index; const auto& asset_entry = gh_entry.assets[index]; App::Push(std::make_shared("Downloading "_i18n + GetEntry().name, [this, &asset_entry](auto pbox){ - return DownloadApp(pbox, &asset_entry); + return DownloadApp(pbox, asset_entry); }, [this](bool success){ if (success) { App::Notify("Downloaded " + GetEntry().name); diff --git a/sphaira/source/ui/menus/main_menu.cpp b/sphaira/source/ui/menus/main_menu.cpp index bc3c8aa..a47fc9d 100644 --- a/sphaira/source/ui/menus/main_menu.cpp +++ b/sphaira/source/ui/menus/main_menu.cpp @@ -35,16 +35,13 @@ auto InstallUpdate(ProgressBox* pbox, const std::string url, const std::string v pbox->NewTransfer("Downloading "_i18n + version); log_write("starting download: %s\n", url.c_str()); - DownloadClearCache(url); - if (!DownloadFile(url, zip_out, "", [pbox](u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow){ - if (pbox->ShouldExit()) { - return false; - } - pbox->UpdateTransfer(dlnow, dltotal); - return true; - })) { + curl::ClearCache(url); + if (!curl::Api().ToFile( + curl::Url{url}, + curl::Path{zip_out}, + curl::OnProgress{pbox->OnDownloadProgressCallback()} + )) { log_write("error with download\n"); - // push popup error box return false; } } @@ -143,58 +140,61 @@ auto InstallUpdate(ProgressBox* pbox, const std::string url, const std::string v } // namespace MainMenu::MainMenu() { - DownloadMemoryAsync("https://api.github.com/repos/ITotalJustice/sphaira/releases/latest", "", [this](std::vector& data, bool success){ - m_update_state = UpdateState::Error; - ON_SCOPE_EXIT( log_write("update status: %u\n", (u8)m_update_state) ); + curl::Api().ToMemoryAsync( + curl::Url{"https://api.github.com/repos/ITotalJustice/sphaira/releases/latest"}, + curl::OnComplete{[this](std::vector& data, bool success, long code){ + m_update_state = UpdateState::Error; + ON_SCOPE_EXIT( log_write("update status: %u\n", (u8)m_update_state) ); - if (!success) { - return false; - } + if (!success) { + return false; + } - auto json = yyjson_read((const char*)data.data(), data.size(), 0); - R_UNLESS(json, false); - ON_SCOPE_EXIT(yyjson_doc_free(json)); + auto json = yyjson_read((const char*)data.data(), data.size(), 0); + R_UNLESS(json, false); + ON_SCOPE_EXIT(yyjson_doc_free(json)); - auto root = yyjson_doc_get_root(json); - R_UNLESS(root, false); + auto root = yyjson_doc_get_root(json); + R_UNLESS(root, false); - auto tag_key = yyjson_obj_get(root, "tag_name"); - R_UNLESS(tag_key, false); + auto tag_key = yyjson_obj_get(root, "tag_name"); + R_UNLESS(tag_key, false); + + const auto version = yyjson_get_str(tag_key); + R_UNLESS(version, false); + if (std::strcmp(APP_VERSION, version) >= 0) { + m_update_state = UpdateState::None; + return true; + } + + auto body_key = yyjson_obj_get(root, "body"); + R_UNLESS(body_key, false); + + const auto body = yyjson_get_str(body_key); + R_UNLESS(body, false); + + auto assets = yyjson_obj_get(root, "assets"); + R_UNLESS(assets, false); + + auto idx0 = yyjson_arr_get(assets, 0); + R_UNLESS(idx0, false); + + auto url_key = yyjson_obj_get(idx0, "browser_download_url"); + R_UNLESS(url_key, false); + + const auto url = yyjson_get_str(url_key); + R_UNLESS(url, false); + + m_update_version = version; + m_update_url = url; + m_update_description = body; + m_update_state = UpdateState::Update; + log_write("found url: %s\n", url); + log_write("found body: %s\n", body); + App::Notify("Update avaliable: "_i18n + m_update_version); - const auto version = yyjson_get_str(tag_key); - R_UNLESS(version, false); - if (std::strcmp(APP_VERSION, version) >= 0) { - m_update_state = UpdateState::None; return true; } - - auto body_key = yyjson_obj_get(root, "body"); - R_UNLESS(body_key, false); - - const auto body = yyjson_get_str(body_key); - R_UNLESS(body, false); - - auto assets = yyjson_obj_get(root, "assets"); - R_UNLESS(assets, false); - - auto idx0 = yyjson_arr_get(assets, 0); - R_UNLESS(idx0, false); - - auto url_key = yyjson_obj_get(idx0, "browser_download_url"); - R_UNLESS(url_key, false); - - const auto url = yyjson_get_str(url_key); - R_UNLESS(url, false); - - m_update_version = version; - m_update_url = url; - m_update_description = body; - m_update_state = UpdateState::Update; - log_write("found url: %s\n", url); - log_write("found body: %s\n", body); - App::Notify("Update avaliable: "_i18n + m_update_version); - - return true; }); AddOnLPress(); diff --git a/sphaira/source/ui/menus/themezer.cpp b/sphaira/source/ui/menus/themezer.cpp index 5c51f3b..02346e6 100644 --- a/sphaira/source/ui/menus/themezer.cpp +++ b/sphaira/source/ui/menus/themezer.cpp @@ -272,7 +272,7 @@ void from_json(const std::vector& data, PackList& e) { } auto InstallTheme(ProgressBox* pbox, const PackListEntry& entry) -> bool { - static fs::FsPath zip_out{"/switch/sphaira/cache/themezer/temp.zip"}; + static const fs::FsPath zip_out{"/switch/sphaira/cache/themezer/temp.zip"}; constexpr auto chunk_size = 1024 * 512; // 512KiB fs::FsNativeSd fs; @@ -287,18 +287,15 @@ auto InstallTheme(ProgressBox* pbox, const PackListEntry& entry) -> bool { const auto url = apiBuildUrlDownloadPack(entry); log_write("using url: %s\n", url.c_str()); - DownloadClearCache(url); - const auto data = DownloadMemory(url, "", [pbox](u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow){ - if (pbox->ShouldExit()) { - return false; - } - pbox->UpdateTransfer(dlnow, dltotal); - return true; - }); + curl::ClearCache(url); + const auto data = curl::Api().ToMemory( + curl::Url{url}, + curl::OnProgress{pbox->OnDownloadProgressCallback()} + ); if (data.empty()) { log_write("error with download: %s\n", url.c_str()); - // push popup error box + return false; } @@ -310,16 +307,19 @@ auto InstallTheme(ProgressBox* pbox, const PackListEntry& entry) -> bool { pbox->NewTransfer("Downloading "_i18n + entry.details.name); log_write("starting download: %s\n", download_pack.url.c_str()); - DownloadClearCache(download_pack.url); - if (!DownloadFile(download_pack.url, zip_out, "", [pbox](u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow){ - if (pbox->ShouldExit()) { - return false; + curl::ClearCache(download_pack.url); + if (!curl::Api().ToFile( + curl::Url{download_pack.url}, + curl::Path{zip_out}, + curl::OnProgress{[pbox](u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow){ + if (pbox->ShouldExit()) { + return false; + } + pbox->UpdateTransfer(dlnow, dltotal); + return true; } - pbox->UpdateTransfer(dlnow, dltotal); - return true; })) { log_write("error with download\n"); - // push popup error box return false; } } @@ -638,15 +638,20 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) { const auto url = theme.preview.thumb; log_write("downloading url: %s\n", url.c_str()); image.state = ImageDownloadState::Progress; - DownloadFileAsync(url, path, "", [this, index, &image](std::vector& data, bool success) { - if (success) { - image.state = ImageDownloadState::Done; - log_write("downloaded themezer image\n"); - } else { - image.state = ImageDownloadState::Failed; - log_write("failed to download image\n"); + curl::Api().ToFileAsync( + curl::Url{url}, + curl::Path{path}, + curl::Priority::High, + curl::OnComplete{[this, index, &image](std::vector& data, bool success, long code) { + if (success) { + image.state = ImageDownloadState::Done; + log_write("downloaded themezer image\n"); + } else { + image.state = ImageDownloadState::Failed; + log_write("failed to download image\n"); + } } - }, nullptr, DownloadPriority::High); + }); } } break; case ImageDownloadState::Progress: { @@ -711,34 +716,38 @@ void Menu::PackListDownload() { log_write("\npackList_url: %s\n\n", packList_url.c_str()); log_write("\nthemeList_url: %s\n\n", themeList_url.c_str()); - DownloadClearCache(packList_url); - DownloadMemoryAsync(packList_url, "", [this, page_index](std::vector& data, bool success){ - log_write("got themezer data\n"); - if (!success) { + curl::ClearCache(packList_url); + curl::Api().ToMemoryAsync( + curl::Url{packList_url}, + curl::Priority::High, + curl::OnComplete{[this, page_index](std::vector& data, bool success, long code){ + log_write("got themezer data\n"); + if (!success) { + auto& page = m_pages[page_index-1]; + page.m_ready = PageLoadState::Error; + log_write("failed to get themezer data...\n"); + return; + } + + PackList a; + from_json(data, a); + + m_pages.resize(a.pagination.page_count); auto& page = m_pages[page_index-1]; - page.m_ready = PageLoadState::Error; - log_write("failed to get themezer data...\n"); - return; + + page.m_packList = a.packList; + page.m_pagination = a.pagination; + page.m_ready = PageLoadState::Done; + m_page_index_max = a.pagination.page_count; + + char subheading[128]; + std::snprintf(subheading, sizeof(subheading), "Page %zu / %zu"_i18n.c_str(), m_page_index+1, m_page_index_max); + SetSubHeading(subheading); + + log_write("a.pagination.page: %u\n", a.pagination.page); + log_write("a.pagination.page_count: %u\n", a.pagination.page_count); } - - PackList a; - from_json(data, a); - - m_pages.resize(a.pagination.page_count); - auto& page = m_pages[page_index-1]; - - page.m_packList = a.packList; - page.m_pagination = a.pagination; - page.m_ready = PageLoadState::Done; - m_page_index_max = a.pagination.page_count; - - char subheading[128]; - std::snprintf(subheading, sizeof(subheading), "Page %zu / %zu"_i18n.c_str(), m_page_index+1, m_page_index_max); - SetSubHeading(subheading); - - log_write("a.pagination.page: %u\n", a.pagination.page); - log_write("a.pagination.page_count: %u\n", a.pagination.page_count); - }, nullptr, DownloadPriority::High); + }); } } // namespace sphaira::ui::menu::themezer