re-write download code to support headers, needed for etag support.

etag support will be added later. github supports it and themezer probably does as well.
appstore does not sadly...
This commit is contained in:
ITotalJustice
2024-12-27 02:28:44 +00:00
parent 7005118876
commit 2edfe91ad6
10 changed files with 437 additions and 335 deletions

View File

@@ -1,41 +1,159 @@
#pragma once
#include "fs.hpp"
#include <vector>
#include <string>
#include <functional>
#include <unordered_map>
#include <type_traits>
#include <switch.h>
namespace sphaira {
namespace sphaira::curl {
using DownloadCallback = std::function<void(std::vector<u8>& data, bool success)>;
using ProgressCallback = std::function<bool(u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow)>;
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<std::string, std::string>;
using OnComplete = std::function<void(std::vector<u8>& data, bool success, long code)>;
using OnProgress = std::function<bool(u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow)>;
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<u8> 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<u8>;
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<u8>;
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 <typename... Ts>
Api(Ts&&... ts) {
Api::set_option(std::forward<Ts>(ts)...);
}
template <typename... Ts>
auto To(Ts&&... ts) {
if constexpr(std::disjunction_v<std::is_same<Path, Ts>...>) {
return ToFile(std::forward<Ts>(ts)...);
} else {
return ToMemory(std::forward<Ts>(ts)...);
}
}
template <typename... Ts>
auto ToAsync(Ts&&... ts) {
if constexpr(std::disjunction_v<std::is_same<Path, Ts>...>) {
return ToFileAsync(std::forward<Ts>(ts)...);
} else {
return ToMemoryAsync(std::forward<Ts>(ts)...);
}
}
template <typename... Ts>
auto ToMemory(Ts&&... ts) {
static_assert(std::disjunction_v<std::is_same<Url, Ts>...>, "Url must be specified");
Api::set_option(std::forward<Ts>(ts)...);
return curl::ToMemory(*this);
}
template <typename... Ts>
auto ToFile(Ts&&... ts) {
static_assert(std::disjunction_v<std::is_same<Url, Ts>...>, "Url must be specified");
static_assert(std::disjunction_v<std::is_same<Path, Ts>...>, "Path must be specified");
Api::set_option(std::forward<Ts>(ts)...);
return curl::ToFile(*this);
}
template <typename... Ts>
auto ToMemoryAsync(Ts&&... ts) {
static_assert(std::disjunction_v<std::is_same<Url, Ts>...>, "Url must be specified");
static_assert(std::disjunction_v<std::is_same<OnComplete, Ts>...>, "OnComplete must be specified");
Api::set_option(std::forward<Ts>(ts)...);
return curl::ToMemoryAsync(*this);
}
template <typename... Ts>
auto ToFileAsync(Ts&&... ts) {
static_assert(std::disjunction_v<std::is_same<Url, Ts>...>, "Url must be specified");
static_assert(std::disjunction_v<std::is_same<Path, Ts>...>, "Path must be specified");
static_assert(std::disjunction_v<std::is_same<OnComplete, Ts>...>, "OnComplete must be specified");
Api::set_option(std::forward<Ts>(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 <typename T>
void set_option(T&& t) {
SetOption(std::forward<T>(t));
}
template <typename T, typename... Ts>
void set_option(T&& t, Ts&&... ts) {
set_option(std::forward<T>(t));
set_option(std::forward<Ts>(ts)...);
}
};
} // namespace sphaira::curl

View File

@@ -26,7 +26,7 @@ using EventData = std::variant<
ExitEventData,
HazeCallbackData,
NxlinkCallbackData,
DownloadEventData
curl::DownloadEventData
>;
// returns number of events

View File

@@ -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;

View File

@@ -276,9 +276,9 @@ void App::Loop() {
App::Notify("Nxlink Finished"_i18n);
break;
}
} else if constexpr(std::is_same_v<T, DownloadEventData>) {
} else if constexpr(std::is_same_v<T, curl::DownloadEventData>) {
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.

View File

@@ -11,9 +11,11 @@
#include <mutex>
#include <curl/curl.h>
namespace sphaira {
namespace sphaira::curl {
namespace {
using DownloadResult = std::pair<bool, long>;
#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<std::string> cache;
std::vector<Url> 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<ProgressCallback*>(clientp);
auto callback = *static_cast<OnProgress*>(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<u8> {
if (g_url_cache.AddToCache(url)) {
auto ToMemory(const Api& e) -> std::vector<u8> {
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

View File

@@ -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<u8>& 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<u8>& 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<ProgressBox>("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<SidebarEntryCallback>("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<ScrollableText>(m_entry.details, 0, 374, 250, 768, 18);
m_changelog = std::make_shared<ScrollableText>(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<u8>& 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<u8>& 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<u8>& 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<NroEntry>& nro_entries) : MenuBase{"AppStore"_i18n}
);
m_repo_download_state = ImageDownloadState::Progress;
#if 0
DownloadMemoryAsync(URL_JSON, [this](std::vector<u8>& 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<NroEntry>& nro_entries) : MenuBase{"AppStore"_i18n}
// download_file = true;
if (download_file) {
DownloadFileAsync(URL_JSON, REPO_PATH, "", [this](std::vector<u8>& 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<u8>& 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<u8>& 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<u8>& 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: {

View File

@@ -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;
}

View File

@@ -67,34 +67,30 @@ void from_json(const std::vector<u8>& 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<ProgressBox>("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);

View File

@@ -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<u8>& 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<u8>& 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();

View File

@@ -272,7 +272,7 @@ void from_json(const std::vector<u8>& 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<u8>& 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<u8>& 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<u8>& 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<u8>& 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