devoptab: deprecate locations.ini in favour of hdd/network mounts, better handle folder creation errors.

This commit is contained in:
ITotalJustice
2025-09-07 13:30:53 +01:00
parent b99d1e5dea
commit 6e1eabbe0f
11 changed files with 28 additions and 364 deletions

View File

@@ -23,8 +23,6 @@ enum DumpLocationType {
DumpLocationType_DevNull,
// dump to stdio, ideal for custom mount points using devoptab, such as hdd.
DumpLocationType_Stdio,
// dump to custom locations found in locations.ini.
DumpLocationType_Network,
};
enum DumpLocationFlag {
@@ -33,8 +31,7 @@ enum DumpLocationFlag {
DumpLocationFlag_UsbS2S = 1 << DumpLocationType_UsbS2S,
DumpLocationFlag_DevNull = 1 << DumpLocationType_DevNull,
DumpLocationFlag_Stdio = 1 << DumpLocationType_Stdio,
DumpLocationFlag_Network = 1 << DumpLocationType_Network,
DumpLocationFlag_All = DumpLocationFlag_SdCard | DumpLocationFlag_Usb | DumpLocationFlag_UsbS2S | DumpLocationFlag_DevNull | DumpLocationFlag_Stdio | DumpLocationFlag_Network,
DumpLocationFlag_All = DumpLocationFlag_SdCard | DumpLocationFlag_Usb | DumpLocationFlag_UsbS2S | DumpLocationFlag_DevNull | DumpLocationFlag_Stdio,
};
struct DumpEntry {
@@ -44,7 +41,6 @@ struct DumpEntry {
struct DumpLocation {
DumpEntry entry{};
location::Entries network{};
location::StdioEntries stdio{};
};

View File

@@ -11,21 +11,6 @@ namespace sphaira::location {
using FsEntryFlag = ui::menu::filebrowser::FsEntryFlag;
struct Entry {
std::string name{};
std::string url{};
std::string user{};
std::string pass{};
std::string bearer{};
std::string pub_key{};
std::string priv_key{};
u16 port{};
};
using Entries = std::vector<Entry>;
auto Load() -> Entries;
void Add(const Entry& e);
// helper for hdd devices.
// this doesn't really belong in this header, however
// locations likely will be renamed to something more generic soon.
@@ -36,6 +21,8 @@ struct StdioEntry {
std::string name{};
// FsEntryFlag
u32 flags{};
// optional dump path inside the mount point.
std::string dump_path{};
};
using StdioEntries = std::vector<StdioEntry>;

View File

@@ -16,18 +16,16 @@ enum FsOption : u32 {
// can split screen.
FsOption_CanSplit = BIT(0),
// can upload files.
FsOption_CanUpload = BIT(1),
// can selected multiple files.
FsOption_CanSelect = BIT(2),
FsOption_CanSelect = BIT(1),
// shows the option to install.
FsOption_CanInstall = BIT(3),
FsOption_CanInstall = BIT(2),
// loads file assoc.
FsOption_LoadAssoc = BIT(4),
FsOption_LoadAssoc = BIT(3),
// do not prompt on exit even if not tabbed.
FsOption_DoNotPrompt = BIT(5),
FsOption_DoNotPrompt = BIT(4),
FsOption_Normal = FsOption_LoadAssoc | FsOption_CanInstall | FsOption_CanSplit | FsOption_CanUpload | FsOption_CanSelect,
FsOption_Normal = FsOption_LoadAssoc | FsOption_CanInstall | FsOption_CanSplit | FsOption_CanSelect,
FsOption_All = FsOption_DoNotPrompt | FsOption_Normal,
FsOption_Picker = FsOption_NONE,
};

View File

@@ -128,6 +128,7 @@ struct MountConfig {
std::string url{};
std::string user{};
std::string pass{};
std::string dump_path{};
std::optional<long> port{};
int timeout{3000}; // 3 seconds.
bool read_only{};

View File

@@ -33,8 +33,6 @@ namespace {
constexpr auto API_AGENT = "TotalJustice";
constexpr u64 CHUNK_SIZE = 1024*1024;
constexpr auto MAX_THREADS = 4;
constexpr int THREAD_PRIO = 0x2F;
constexpr int THREAD_CORE = 2;
std::atomic_bool g_running{};
CURLSH* g_curl_share{};
@@ -62,11 +60,6 @@ struct SeekCustomData {
s64 size{};
};
// helper for creating webdav folders as libcurl does not have built-in
// support for it.
// only creates the folders if they don't exist.
auto WebdavCreateFolder(CURL* curl, const Api& e) -> bool;
auto generate_key_from_path(const fs::FsPath& path) -> std::string {
const auto key = crc32Calculate(path.s, path.size());
return std::to_string(key);
@@ -595,22 +588,16 @@ auto EscapeString(CURL* curl, const std::string& str) -> std::string {
return result;
}
auto EncodeUrl(std::string url) -> std::string {
auto EncodeUrl(const std::string& url) -> std::string {
log_write("[CURL] encoding url\n");
if (url.starts_with("webdav://")) {
log_write("[CURL] updating host\n");
url.replace(0, std::strlen("webdav"), "https");
log_write("[CURL] updated host: %s\n", url.c_str());
}
auto clu = curl_url();
R_UNLESS(clu, url);
ON_SCOPE_EXIT(curl_url_cleanup(clu));
log_write("[CURL] setting url\n");
CURLUcode clu_code;
clu_code = curl_url_set(clu, CURLUPART_URL, url.c_str(), CURLU_URLENCODE);
clu_code = curl_url_set(clu, CURLUPART_URL, url.c_str(), CURLU_DEFAULT_SCHEME | CURLU_URLENCODE);
R_UNLESS(clu_code == CURLUE_OK, url);
log_write("[CURL] set url success\n");
@@ -834,13 +821,6 @@ auto UploadInternal(CURL* curl, const Api& e) -> ApiResult {
return {};
}
if (e.GetUrl().starts_with("webdav://")) {
if (!WebdavCreateFolder(curl, e)) {
log_write("[CURL] failed to create webdav folder, aborting\n");
return {};
}
}
const auto& info = e.GetUploadInfo();
const auto url = e.GetUrl() + "/" + info.m_name;
const auto encoded_url = EncodeUrl(url);
@@ -960,76 +940,6 @@ auto UploadInternal(CURL* curl, const Api& e) -> ApiResult {
return {success, http_code, header_out, chunk_out.data};
}
auto WebdavCreateFolder(CURL* curl, const Api& e) -> bool {
// if using webdav, extract the file path and create the directories.
// https://github.com/WebDAVDevs/webdav-request-samples/blob/master/webdav_curl.md
if (e.GetUrl().starts_with("webdav://")) {
log_write("[CURL] found webdav url\n");
const auto info = e.GetUploadInfo();
if (info.m_name.empty()) {
return true;
}
const auto& file_path = info.m_name;
log_write("got file path: %s\n", file_path.c_str());
const auto file_loc = file_path.find_last_of('/');
if (file_loc == file_path.npos) {
log_write("failed to find last slash\n");
return true;
}
const auto path_view = file_path.substr(0, file_loc);
log_write("got folder path: %s\n", path_view.c_str());
auto e2 = e;
e2.SetOption(Path{});
e2.SetOption(Url{e.GetUrl() + "/" + path_view});
e2.SetOption(Flags{e.GetFlags() | Flag_NoBody});
e2.SetOption(CustomRequest{"PROPFIND"});
e2.SetOption(Header{
{ "Depth", "0" },
});
// test to see if the directory exists first.
const auto exist_result = DownloadInternal(curl, e2);
if (exist_result.success) {
log_write("[CURL] folder already exist: %s\n", path_view.c_str());
return true;
} else {
log_write("[CURL] folder does NOT exist, manually creating: %s\n", path_view.c_str());
}
// make the request to create the folder.
std::string folder;
for (const auto dir : std::views::split(path_view, '/')) {
if (dir.empty()) {
continue;
}
folder += "/" + std::string{dir.data(), dir.size()};
e2.SetOption(Url{e.GetUrl() + folder});
e2.SetOption(Header{});
e2.SetOption(CustomRequest{"MKCOL"});
const auto result = DownloadInternal(curl, e2);
if (result.code == 201) {
log_write("[CURL] created webdav directory\n");
} else if (result.code == 405) {
log_write("[CURL] webdav directory already exists: %ld\n", result.code);
} else {
log_write("[CURL] failed to create webdav directory: %ld\n", result.code);
return false;
}
}
} else {
log_write("[CURL] not a webdav url: %s\n", e.GetUrl().c_str());
}
return true;
}
void my_lock(CURL *handle, curl_lock_data data, curl_lock_access laccess, void *useptr) {
mutexLock(&g_mutex_share[data]);
}

View File

@@ -372,7 +372,8 @@ Result DumpToFileNative(ui::ProgressBox* pbox, BaseSource* source, std::span<con
Result DumpToStdio(ui::ProgressBox* pbox, const location::StdioEntry& loc, BaseSource* source, std::span<const fs::FsPath> paths, const CustomTransfer& custom_transfer) {
fs::FsStdio fs{};
return DumpToFile(pbox, &fs, loc.mount, source, paths, custom_transfer);
const auto mount_path = fs::AppendPath(loc.mount, loc.dump_path);
return DumpToFile(pbox, &fs, mount_path, source, paths, custom_transfer);
}
Result DumpToUsbS2SInternal(ui::ProgressBox* pbox, UsbTest* usb) {
@@ -486,54 +487,6 @@ Result DumpToDevNull(ui::ProgressBox* pbox, BaseSource* source, std::span<const
R_SUCCEED();
}
Result DumpToNetwork(ui::ProgressBox* pbox, const location::Entry& loc, BaseSource* source, std::span<const fs::FsPath> paths) {
for (auto path : paths) {
R_TRY(pbox->ShouldExitResult());
const auto file_size = source->GetSize(path);
pbox->SetImage(source->GetIcon(path));
pbox->SetTitle(source->GetName(path));
pbox->NewTransfer(path);
R_TRY(thread::TransferPull(pbox, file_size,
[&](void* data, s64 off, s64 size, u64* bytes_read) -> Result {
return source->Read(path, data, off, size, bytes_read);
},
[&](thread::PullCallback pull) -> Result {
s64 offset{};
const auto result = curl::Api().FromMemory(
CURL_LOCATION_TO_API(loc),
curl::OnProgress{pbox->OnDownloadProgressCallback()},
curl::UploadInfo{
path, file_size,
[&](void *ptr, size_t size) -> size_t {
// curl will request past the size of the file, causing an error.
if (offset >= file_size) {
log_write("finished file upload\n");
return 0;
}
u64 bytes_read{};
if (R_FAILED(pull(ptr, size, &bytes_read))) {
log_write("failed to read in custom callback: %zd size: %zd\n", offset, size);
return 0;
}
offset += bytes_read;
return bytes_read;
}
}
);
R_UNLESS(result.success, Result_DumpFailedNetworkUpload);
R_SUCCEED();
}
));
}
R_SUCCEED();
}
} // namespace
void DumpGetLocation(const std::string& title, u32 location_flags, const OnLocation& on_loc, const CustomTransfer& custom_transfer) {
@@ -541,14 +494,6 @@ void DumpGetLocation(const std::string& title, u32 location_flags, const OnLocat
ui::PopupList::Items items;
std::vector<DumpEntry> dump_entries;
out.network = location::Load();
if (!custom_transfer && location_flags & (1 << DumpLocationType_Network)) {
for (s32 i = 0; i < std::size(out.network); i++) {
dump_entries.emplace_back(DumpLocationType_Network, i);
items.emplace_back(out.network[i].name);
}
}
out.stdio = location::GetStdio(true);
if (location_flags & (1 << DumpLocationType_Stdio)) {
for (s32 i = 0; i < std::size(out.stdio); i++) {
@@ -578,9 +523,7 @@ void DumpGetLocation(const std::string& title, u32 location_flags, const OnLocat
}
Result Dump(ui::ProgressBox* pbox, const std::shared_ptr<BaseSource>& source, const DumpLocation& location, const std::vector<fs::FsPath>& paths, const CustomTransfer& custom_transfer) {
if (location.entry.type == DumpLocationType_Network) {
R_TRY(DumpToNetwork(pbox, location.network[location.entry.index], source.get(), paths));
} else if (location.entry.type == DumpLocationType_Stdio) {
if (location.entry.type == DumpLocationType_Stdio) {
R_TRY(DumpToStdio(pbox, location.stdio[location.entry.index], source.get(), paths, custom_transfer));
} else if (location.entry.type == DumpLocationType_SdCard) {
R_TRY(DumpToFileNative(pbox, source.get(), paths, custom_transfer));

View File

@@ -215,14 +215,18 @@ Result CreateDirectoryRecursively(FsFileSystem* fs, const FsPath& _path, bool ig
rc = CreateDirectory(path, ignore_read_only);
}
std::strcat(path, "/");
}
// only check if the last folder creation failed.
// reason being is that it may try to create "/" root folder, which some network
// fs will return a EPERM/EACCES error.
// however if the last directory failed, then it is a real error.
if (R_FAILED(rc) && rc != FsError_PathAlreadyExists) {
log_write("failed to create folder: %s\n", path.s);
return rc;
}
// log_write("created_directory: %s\n", path);
std::strcat(path, "/");
}
R_SUCCEED();
}

View File

@@ -12,71 +12,8 @@
namespace sphaira::location {
namespace {
constexpr fs::FsPath location_path{"/config/sphaira/locations.ini"};
} // namespace
void Add(const Entry& e) {
if (e.name.empty() || e.url.empty()) {
return;
}
ini_puts(e.name.c_str(), "url", e.url.c_str(), location_path);
if (!e.user.empty()) {
ini_puts(e.name.c_str(), "user", e.user.c_str(), location_path);
}
if (!e.pass.empty()) {
ini_puts(e.name.c_str(), "pass", e.pass.c_str(), location_path);
}
if (!e.bearer.empty()) {
ini_puts(e.name.c_str(), "bearer", e.bearer.c_str(), location_path);
}
if (!e.pub_key.empty()) {
ini_puts(e.name.c_str(), "pub_key", e.pub_key.c_str(), location_path);
}
if (!e.priv_key.empty()) {
ini_puts(e.name.c_str(), "priv_key", e.priv_key.c_str(), location_path);
}
if (e.port) {
ini_putl(e.name.c_str(), "port", e.port, location_path);
}
}
auto Load() -> Entries {
Entries out{};
auto cb = [](const mTCHAR *Section, const mTCHAR *Key, const mTCHAR *Value, void *UserData) -> int {
auto e = static_cast<Entries*>(UserData);
// add new entry if use section changed.
if (e->empty() || std::strcmp(Section, e->back().name.c_str())) {
e->emplace_back(Section);
}
if (!std::strcmp(Key, "url")) {
e->back().url = Value;
} else if (!std::strcmp(Key, "user")) {
e->back().user = Value;
} else if (!std::strcmp(Key, "pass")) {
e->back().pass = Value;
} else if (!std::strcmp(Key, "bearer")) {
e->back().bearer = Value;
} else if (!std::strcmp(Key, "pub_key")) {
e->back().pub_key = Value;
} else if (!std::strcmp(Key, "priv_key")) {
e->back().priv_key = Value;
} else if (!std::strcmp(Key, "port")) {
e->back().port = std::atoi(Value);
}
return 1;
};
ini_browse(cb, &out, location_path);
return out;
}
auto GetStdio(bool write) -> StdioEntries {
StdioEntries out{};

View File

@@ -1005,114 +1005,6 @@ void FsView::ZipFiles(fs::FsPath zip_out) {
});
}
void FsView::UploadFiles() {
const auto targets = GetSelectedEntries();
const auto network_locations = location::Load();
if (network_locations.empty()) {
App::Notify("No upload locations set!"_i18n);
return;
}
PopupList::Items items;
for (const auto&p : network_locations) {
items.emplace_back(p.name);
}
App::Push<PopupList>(
"Select upload location"_i18n, items, [this, network_locations](auto op_index){
if (!op_index) {
return;
}
const auto loc = network_locations[*op_index];
App::Push<ProgressBox>(0, "Uploading"_i18n, "", [this, loc](auto pbox) -> Result {
auto targets = GetSelectedEntries();
const auto is_file_based_emummc = App::IsFileBaseEmummc();
const auto file_add = [&](s64 file_size, const fs::FsPath& file_path, const char* name) -> Result {
// the file name needs to be relative to the current directory.
const auto relative_file_name = file_path.s + std::strlen(m_path);
pbox->SetTitle(name);
pbox->NewTransfer(relative_file_name);
fs::File f;
R_TRY(m_fs->OpenFile(file_path, FsOpenMode_Read, &f));
return thread::TransferPull(pbox, file_size,
[&](void* data, s64 off, s64 size, u64* bytes_read) -> Result {
const auto rc = f.Read(off, data, size, FsReadOption_None, bytes_read);
if (m_fs->IsNative() && is_file_based_emummc) {
svcSleepThread(2e+6); // 2ms
}
return rc;
},
[&](thread::PullCallback pull) -> Result {
s64 offset{};
const auto result = curl::Api().FromMemory(
CURL_LOCATION_TO_API(loc),
curl::OnProgress{pbox->OnDownloadProgressCallback()},
curl::UploadInfo{
relative_file_name, file_size,
[&](void *ptr, size_t size) -> size_t {
// curl will request past the size of the file, causing an error.
if (offset >= file_size) {
log_write("finished file upload\n");
return 0;
}
u64 bytes_read{};
if (R_FAILED(pull(ptr, size, &bytes_read))) {
log_write("failed to read in custom callback: %zd size: %zd\n", offset, size);
return 0;
}
offset += bytes_read;
return bytes_read;
}
}
);
R_UNLESS(result.success, Result_FileBrowserFailedUpload);
R_SUCCEED();
}
);
};
for (auto& e : targets) {
if (e.IsFile()) {
const auto file_path = GetNewPath(e);
R_TRY(file_add(e.file_size, file_path, e.GetName().c_str()));
} else {
FsDirCollections collections;
get_collections(GetNewPath(e), e.name, collections, true);
for (const auto& collection : collections) {
for (const auto& file : collection.files) {
const auto file_path = fs::AppendPath(collection.path, file.name);
R_TRY(file_add(file.file_size, file_path, file.name));
}
}
}
}
R_SUCCEED();
}, [this](Result rc){
App::PushErrorBox(rc, "Failed to, TODO: add message here"_i18n);
m_menu->ResetSelection();
if (R_SUCCEEDED(rc)) {
App::Notify("Upload successfull!"_i18n);
log_write("Upload successfull!!!\n");
} else {
App::Notify("Upload failed!"_i18n);
log_write("Upload failed!!!\n");
}
});
}
);
}
auto FsView::Scan(fs::FsPath new_path, bool is_walk_up) -> Result {
App::SetBoostMode(true);
ON_SCOPE_EXIT(App::SetBoostMode(false));
@@ -1978,12 +1870,6 @@ void FsView::DisplayAdvancedOptions() {
});
}
if (m_entries_current.size() && (m_menu->m_options & FsOption_CanUpload)) {
options->Add<SidebarEntryCallback>("Upload"_i18n, [this](){
UploadFiles();
});
}
if (m_entries_current.size() && !m_selected_count && IsExtension(GetEntry().GetExtension(), THEME_MUSIC_EXTENSIONS)) {
options->Add<SidebarEntryCallback>("Set as background music"_i18n, [this](){
const auto rc = App::SetDefaultBackgroundMusic(GetFs(), GetNewPathCurrent());

View File

@@ -792,6 +792,8 @@ Result MountNetworkDevice(const CreateDeviceCallback& create_device, size_t file
e->back().user = Value;
} else if (!std::strcmp(Key, "pass")) {
e->back().pass = Value;
} else if (!std::strcmp(Key, "dump_path")) {
e->back().dump_path = Value;
} else if (!std::strcmp(Key, "port")) {
const auto port = ini_parse_getl(Value, -1);
if (port < 0 || port > 65535) {
@@ -1379,7 +1381,7 @@ Result GetNetworkDevices(location::StdioEntries& out) {
flags |= location::FsEntryFlag::FsEntryFlag_NoStatDir;
}
out.emplace_back(entry->mount, entry->name, flags);
out.emplace_back(entry->mount, entry->name, flags, entry->device.config.dump_path);
}
}

View File

@@ -181,7 +181,7 @@ bool Device::http_stat(const std::string& path, struct stat* st, bool is_dir) {
// handle error codes.
if (response_code != 200 && response_code != 206) {
log_write("[WEBDAV] Unexpected HTTP response code: %ld\n", response_code);
log_write("[HTTP] Unexpected HTTP response code: %ld\n", response_code);
return false;
}