support for filtering mtp/ftp mount options. use builtin config for ftp port,user,pass.

This commit is contained in:
ITotalJustice
2025-09-21 21:56:36 +01:00
parent 5edc3869cd
commit 54a2215e04
8 changed files with 496 additions and 119 deletions

View File

@@ -341,7 +341,7 @@ endif()
if (ENABLE_FTPSRV)
FetchContent_Declare(ftpsrv
GIT_REPOSITORY https://github.com/ITotalJustice/ftpsrv.git
GIT_TAG a7c2283
GIT_TAG 7770dab
SOURCE_SUBDIR NONE
)
@@ -358,7 +358,7 @@ if (ENABLE_FTPSRV)
USE_VFS_STORAGE=$<BOOL:TRUE>
# disabled as it may conflict with the gamecard menu.
USE_VFS_GC=$<BOOL:FALSE>
USE_VFS_USBHSFS=$<BOOL:${ENABLE_LIBUSBHSFS}>
USE_VFS_USBHSFS=$<BOOL:FALSE>
VFS_NX_BUFFER_IO=$<BOOL:TRUE>
# let sphaira handle init / closing of the hdd.
USE_VFS_USBHSFS_INIT=$<BOOL:FALSE>
@@ -378,14 +378,7 @@ if (ENABLE_FTPSRV)
${ftpsrv_SOURCE_DIR}/src/platform/nx/utils.c
)
if (ENABLE_LIBUSBHSFS)
target_sources(ftpsrv_helper PRIVATE
${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs/vfs_nx_stdio.c
${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs/vfs_nx_hdd.c
)
endif()
target_link_libraries(ftpsrv_helper PUBLIC ftpsrv libusbhsfs)
target_link_libraries(ftpsrv_helper PUBLIC ftpsrv)
target_include_directories(ftpsrv_helper PUBLIC ${ftpsrv_SOURCE_DIR}/src/platform)
target_compile_definitions(sphaira PRIVATE ENABLE_FTPSRV)

View File

@@ -131,6 +131,9 @@ public:
static void DisplayAdvancedOptions(bool left_side = true);
static void DisplayInstallOptions(bool left_side = true);
static void DisplayDumpOptions(bool left_side = true);
static void DisplayFtpOptions(bool left_side = true);
static void DisplayMtpOptions(bool left_side = true);
static void DisplayHddOptions(bool left_side = true);
// helper for sidebar options to toggle install on/off
static void ShowEnableInstallPromptOption(option::OptionBool& option, bool& enable);
@@ -342,9 +345,38 @@ public:
// todo: move this into it's own menu
option::OptionLong m_text_scroll_speed{"accessibility", "text_scroll_speed", 1}; // normal
// ftp options.
option::OptionLong m_ftp_port{"ftp", "port", 5000};
option::OptionBool m_ftp_anon{"ftp", "anon", true};
option::OptionString m_ftp_user{"ftp", "user", ""};
option::OptionString m_ftp_pass{"ftp", "pass", ""};
option::OptionBool m_ftp_show_album{"ftp", "show_album", true};
option::OptionBool m_ftp_show_ams_contents{"ftp", "show_ams_contents", false};
option::OptionBool m_ftp_show_bis_storage{"ftp", "show_bis_storage", false};
option::OptionBool m_ftp_show_bis_fs{"ftp", "show_bis_fs", false};
option::OptionBool m_ftp_show_content_system{"ftp", "show_content_system", false};
option::OptionBool m_ftp_show_content_user{"ftp", "show_content_user", false};
option::OptionBool m_ftp_show_content_sd{"ftp", "show_content_sd", false};
// option::OptionBool m_ftp_show_content_sd0{"ftp", "show_content_sd0", false};
// option::OptionBool m_ftp_show_custom_system{"ftp", "show_custom_system", false};
// option::OptionBool m_ftp_show_custom_sd{"ftp", "show_custom_sd", false};
option::OptionBool m_ftp_show_games{"ftp", "show_games", true};
option::OptionBool m_ftp_show_install{"ftp", "show_install", true};
option::OptionBool m_ftp_show_mounts{"ftp", "show_mounts", false};
option::OptionBool m_ftp_show_switch{"ftp", "show_switch", false};
// mtp options.
option::OptionLong m_mtp_vid{"mtp", "vid", 0x057e}; // nintendo
option::OptionLong m_mtp_pid{"mtp", "pid", 0x201d}; // switch
option::OptionLong m_mtp_vid{"mtp", "vid", 0x057e}; // nintendo (hidden from ui)
option::OptionLong m_mtp_pid{"mtp", "pid", 0x201d}; // switch (hidden from ui)
option::OptionBool m_mtp_allocate_file{"mtp", "allocate_file", true};
option::OptionBool m_mtp_show_album{"mtp", "show_album", true};
option::OptionBool m_mtp_show_content_sd{"mtp", "show_content_sd", false};
option::OptionBool m_mtp_show_content_system{"mtp", "show_content_system", false};
option::OptionBool m_mtp_show_content_user{"mtp", "show_content_user", false};
option::OptionBool m_mtp_show_games{"mtp", "show_games", true};
option::OptionBool m_mtp_show_install{"mtp", "show_install", true};
option::OptionBool m_mtp_show_mounts{"mtp", "show_mounts", false};
option::OptionBool m_mtp_show_speedtest{"mtp", "show_speedtest", false};
std::shared_ptr<fs::FsNativeSd> m_fs{};
audio::SongID m_background_music{};

View File

@@ -44,6 +44,14 @@ public:
m_depends_click = depends_click;
}
void SetDirty(bool dirty = true) {
m_dirty = dirty;
}
auto IsDirty() const -> bool {
return m_dirty;
}
protected:
auto IsEnabled() const -> bool {
if (m_depends_callback) {
@@ -69,6 +77,7 @@ private:
DependsClickCallback m_depends_click{};
ScrollingText m_scolling_title{};
ScrollingText m_scolling_value{};
bool m_dirty{};
};
template<typename T>
@@ -174,11 +183,14 @@ private:
};
class SidebarEntryTextInput final : public SidebarEntryTextBase {
public:
using Callback = std::function<void(SidebarEntryTextInput* input)>;
public:
// uses normal keyboard.
explicit SidebarEntryTextInput(const std::string& title, const std::string& value, const std::string& guide = {}, s64 len_min = -1, s64 len_max = PATH_MAX, const std::string& info = "");
explicit SidebarEntryTextInput(const std::string& title, const std::string& value, const std::string& guide = {}, s64 len_min = -1, s64 len_max = PATH_MAX, const std::string& info = "", const Callback& callback = nullptr);
// uses numpad.
explicit SidebarEntryTextInput(const std::string& title, s64 value, const std::string& guide = {}, s64 len_min = -1, s64 len_max = PATH_MAX, const std::string& info = "");
explicit SidebarEntryTextInput(const std::string& title, s64 value, const std::string& guide = {}, s64 len_min = -1, s64 len_max = PATH_MAX, const std::string& info = "", const Callback& callback = nullptr);
auto GetNumValue() const -> s64 {
return std::stoul(GetValue());
@@ -191,6 +203,7 @@ private:
const std::string m_guide;
const s64 m_len_min;
const s64 m_len_max;
const Callback m_callback;
};
class SidebarEntryFilePicker final : public SidebarEntryTextBase {
@@ -210,10 +223,12 @@ class Sidebar : public Widget {
public:
enum class Side { LEFT, RIGHT };
using Items = std::vector<std::unique_ptr<SidebarEntryBase>>;
using OnExitWhenChangedCallback = std::function<void()>;
public:
explicit Sidebar(const std::string& title, Side side, float width = 450.f);
explicit Sidebar(const std::string& title, const std::string& sub, Side side, float width = 450.f);
~Sidebar();
auto Update(Controller* controller, TouchInfo* touch) -> void override;
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
@@ -227,6 +242,12 @@ public:
return (T*)Add(std::make_unique<T>(std::forward<Args>(args)...));
}
// sets a callback that is called on exit when the any options were changed.
// the change detection isn't perfect, it just checks if the A button was pressed...
void SetOnExitWhenChanged(const OnExitWhenChangedCallback& cb) {
m_on_exit_when_changed = cb;
}
private:
void SetIndex(s64 index);
void SetupButtons();
@@ -235,16 +256,18 @@ private:
const std::string m_title;
const std::string m_sub;
const Side m_side;
Items m_items;
Items m_items{};
s64 m_index{};
std::unique_ptr<List> m_list;
std::unique_ptr<List> m_list{};
Vec4 m_top_bar{};
Vec4 m_bottom_bar{};
Vec2 m_title_pos{};
Vec4 m_base_pos{};
OnExitWhenChangedCallback m_on_exit_when_changed{};
static constexpr float m_title_size{28.f};
// static constexpr Vec2 box_size{380.f, 70.f};
static constexpr Vec2 m_box_size{400.f, 70.f};

View File

@@ -1549,9 +1549,37 @@ App::App(const char* argv0) {
else if (app->m_nsz_compress_ldm.LoadFrom(Key, Value)) {}
else if (app->m_nsz_compress_block.LoadFrom(Key, Value)) {}
else if (app->m_nsz_compress_block_exponent.LoadFrom(Key, Value)) {}
} else if (!std::strcmp(Section, "ftp")) {
if (app->m_ftp_port.LoadFrom(Key, Value)) {}
else if (app->m_ftp_anon.LoadFrom(Key, Value)) {}
else if (app->m_ftp_user.LoadFrom(Key, Value)) {}
else if (app->m_ftp_pass.LoadFrom(Key, Value)) {}
else if (app->m_ftp_show_album.LoadFrom(Key, Value)) {}
else if (app->m_ftp_show_ams_contents.LoadFrom(Key, Value)) {}
else if (app->m_ftp_show_bis_storage.LoadFrom(Key, Value)) {}
else if (app->m_ftp_show_bis_fs.LoadFrom(Key, Value)) {}
else if (app->m_ftp_show_content_system.LoadFrom(Key, Value)) {}
else if (app->m_ftp_show_content_user.LoadFrom(Key, Value)) {}
else if (app->m_ftp_show_content_sd.LoadFrom(Key, Value)) {}
// else if (app->m_ftp_show_content_sd0.LoadFrom(Key, Value)) {}
// else if (app->m_ftp_show_custom_system.LoadFrom(Key, Value)) {}
// else if (app->m_ftp_show_custom_sd.LoadFrom(Key, Value)) {}
else if (app->m_ftp_show_games.LoadFrom(Key, Value)) {}
else if (app->m_ftp_show_install.LoadFrom(Key, Value)) {}
else if (app->m_ftp_show_mounts.LoadFrom(Key, Value)) {}
else if (app->m_ftp_show_switch.LoadFrom(Key, Value)) {}
} else if (!std::strcmp(Section, "mtp")) {
if (app->m_mtp_vid.LoadFrom(Key, Value)) {}
else if (app->m_mtp_pid.LoadFrom(Key, Value)) {}
else if (app->m_mtp_allocate_file.LoadFrom(Key, Value)) {}
else if (app->m_mtp_show_album.LoadFrom(Key, Value)) {}
else if (app->m_mtp_show_content_sd.LoadFrom(Key, Value)) {}
else if (app->m_mtp_show_content_system.LoadFrom(Key, Value)) {}
else if (app->m_mtp_show_content_user.LoadFrom(Key, Value)) {}
else if (app->m_mtp_show_games.LoadFrom(Key, Value)) {}
else if (app->m_mtp_show_install.LoadFrom(Key, Value)) {}
else if (app->m_mtp_show_mounts.LoadFrom(Key, Value)) {}
else if (app->m_mtp_show_speedtest.LoadFrom(Key, Value)) {}
}
return 1;
@@ -2271,6 +2299,236 @@ void App::DisplayDumpOptions(bool left_side) {
block_size_option->Depends(App::GetApp()->m_nsz_compress_block, "NSZ block compression is disabled."_i18n);
}
void App::DisplayFtpOptions(bool left_side) {
// todo: prompt on exit to restart ftp server if options were changed.
auto options = std::make_unique<ui::Sidebar>("FTP Options"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(std::move(options)));
options->SetOnExitWhenChanged([](){
if (App::GetFtpEnable()) {
App::Notify("Restarting FTP server..."_i18n);
App::SetFtpEnable(false);
App::SetFtpEnable(true);
}
});
options->Add<ui::SidebarEntryBool>("Enable"_i18n, App::GetFtpEnable(), [](bool& enable){
App::SetFtpEnable(enable);
}, "Enable FTP server to run in the background."_i18n);
options->Add<ui::SidebarEntryTextInput>(
"Port", App::GetApp()->m_ftp_port.Get(), "Port number", 1, 5,
"Opens the FTP server on this port."_i18n,
[](auto* input){
App::GetApp()->m_ftp_port.Set(input->GetNumValue());
}
);
options->Add<ui::SidebarEntryBool>(
"Anon"_i18n, App::GetApp()->m_ftp_anon,
"Allows you to login without setting a username and password.\n"
"If disabled, you must set a user name and password below!"_i18n
);
options->Add<ui::SidebarEntryTextInput>(
"User", App::GetApp()->m_ftp_user.Get(), "Username", -1, 64,
"Sets the username, must be set if anon is disabled."_i18n,
[](auto* input){
App::GetApp()->m_ftp_user.Set(input->GetValue());
}
);
options->Add<ui::SidebarEntryTextInput>(
"Pass", App::GetApp()->m_ftp_pass.Get(), "Password", -1, 64,
"Sets the password, must be set if anon is disabled."_i18n,
[](auto* input){
App::GetApp()->m_ftp_pass.Set(input->GetValue());
}
);
options->Add<ui::SidebarEntryBool>(
"Show album"_i18n, App::GetApp()->m_ftp_show_album,
"Shows the microSD card album folder."_i18n
);
options->Add<ui::SidebarEntryBool>(
"Show Atmosphere contents"_i18n, App::GetApp()->m_ftp_show_ams_contents,
"Shows the shortcut for the /atmosphere/contents folder."_i18n
);
options->Add<ui::SidebarEntryBool>(
"Show bis storage"_i18n, App::GetApp()->m_ftp_show_bis_storage,
"Shows the bis folder which contains the following:\n"
"- BootPartition1Root.bin\n"
"- BootPartition2Root.bin\n"
"- UserDataRoot.bin\n"
"- BootConfigAndPackage2Part1.bin\n"
"- BootConfigAndPackage2Part2.bin\n"
"- BootConfigAndPackage2Part3.bin\n"
"- BootConfigAndPackage2Part4.bin\n"
"- BootConfigAndPackage2Part5.bin\n"
"- BootConfigAndPackage2Part6.bin\n"
"- CalibrationFile.bin\n"
"- SafeMode.bin\n"
"- User.bin\n"
"- System.bin\n"
"- SystemProperEncryption.bin"_i18n
);
options->Add<ui::SidebarEntryBool>(
"Show bis file systems"_i18n, App::GetApp()->m_ftp_show_bis_fs,
"Shows the following bis file systems:\n"
"- bis_calibration_file\n"
"- bis_safe_mode\n"
"- bis_user\n"
"- bis_system"_i18n
);
options->Add<ui::SidebarEntryBool>(
"Show system contents"_i18n, App::GetApp()->m_ftp_show_content_system,
"Shows the system contents folder."_i18n
);
options->Add<ui::SidebarEntryBool>(
"Show user contents"_i18n, App::GetApp()->m_ftp_show_content_user,
"Shows the user contents folder."_i18n
);
options->Add<ui::SidebarEntryBool>(
"Show microSD contents"_i18n, App::GetApp()->m_ftp_show_content_sd,
"Shows the microSD contents folder.\n\n"
"NOTE: This is not the normal microSD card storage, it is instead "
"the location where NCA's are stored. The normal microSD card is always mounted."_i18n
);
options->Add<ui::SidebarEntryBool>(
"Show games"_i18n, App::GetApp()->m_ftp_show_games,
"Shows the games folder.\n\n"
"This folder contains all of your installed games, allowing you to create "
"backups over FTP!\n\n"
"NOTE: This folder is read-only. You cannot delete games over FTP."_i18n
);
options->Add<ui::SidebarEntryBool>(
"Show install"_i18n, App::GetApp()->m_ftp_show_install,
"Shows the install folder.\n\n"
"This folder is used for installing games via FTP.\n\n"
"NOTE: You must open the \"FTP Install\" menu when trying to install a game!"_i18n
);
options->Add<ui::SidebarEntryBool>(
"Show mounts"_i18n, App::GetApp()->m_ftp_show_mounts,
"Shows the mounts folder.\n\n"
"This folder is contains all of the mounts added to Sphaira, allowing you to acces them over FTP!\n"
"For example, you can access your SMB, WebDav or other FTP mounts over FTP."_i18n
);
options->Add<ui::SidebarEntryBool>(
"Show switch"_i18n, App::GetApp()->m_ftp_show_switch,
"Shows the shortcut for the /switch folder."
"This is the folder that contains all your homebrew (NRO's)."_i18n
);
}
void App::DisplayMtpOptions(bool left_side) {
// todo: prompt on exit to restart ftp server if options were changed.
auto options = std::make_unique<ui::Sidebar>("MTP Options"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(std::move(options)));
options->SetOnExitWhenChanged([](){
if (App::GetMtpEnable()) {
App::Notify("Restarting MTP server..."_i18n);
App::SetMtpEnable(false);
App::SetMtpEnable(true);
}
});
options->Add<ui::SidebarEntryBool>("Enable"_i18n, App::GetMtpEnable(), [](bool& enable){
App::SetMtpEnable(enable);
}, "Enable MTP server to run in the background."_i18n);
// not sure if i want to expose this to users yet.
// its also stubbed currently.
#if 0
options->Add<ui::SidebarEntryBool>(
"Pre-allocate file"_i18n, App::GetApp()->m_mtp_allocate_file,
"Enables pre-allocating the file size before writing.\n"
"This speeds up file writes, however, this can cause timeouts if all these conditions are met:\n"
"- using Windows\n"
"- using emuMMC\n"
"- transferring a large file (>1GB)\n\n"
"This option should be left enabled, however if you use the above and experience timeouts, "
"then try again with this option disabled."_i18n
);
#endif
options->Add<ui::SidebarEntryBool>(
"Show album"_i18n, App::GetApp()->m_mtp_show_album,
"Shows the microSD card album folder."_i18n
);
options->Add<ui::SidebarEntryBool>(
"Show microSD contents"_i18n, App::GetApp()->m_mtp_show_content_sd,
"Shows the microSD contents folder.\n\n"
"NOTE: This is not the normal microSD card storage, it is instead "
"the location where NCA's are stored. The normal microSD card is always mounted."_i18n
);
options->Add<ui::SidebarEntryBool>(
"Show system contents"_i18n, App::GetApp()->m_mtp_show_content_system,
"Shows the system contents folder."_i18n
);
options->Add<ui::SidebarEntryBool>(
"Show user contents"_i18n, App::GetApp()->m_mtp_show_content_user,
"Shows the user contents folder."_i18n
);
options->Add<ui::SidebarEntryBool>(
"Show games"_i18n, App::GetApp()->m_mtp_show_games,
"Shows the games folder.\n\n"
"This folder contains all of your installed games, allowing you to create "
"backups over MTP!\n\n"
"NOTE: This folder is read-only. You cannot delete games over MTP."_i18n
);
options->Add<ui::SidebarEntryBool>(
"Show install"_i18n, App::GetApp()->m_mtp_show_install,
"Shows the install folder.\n\n"
"This folder is used for installing games via MTP.\n\n"
"NOTE: You must open the \"MTP Install\" menu when trying to install a game!"_i18n
);
options->Add<ui::SidebarEntryBool>(
"Show mounts"_i18n, App::GetApp()->m_mtp_show_mounts,
"Shows the mounts folder.\n\n"
"This folder is contains all of the mounts added to Sphaira, allowing you to acces them over MTP!\n"
"For example, you can access your SMB, WebDav and FTP mounts over MTP."_i18n
);
options->Add<ui::SidebarEntryBool>(
"Show DevNull"_i18n, App::GetApp()->m_mtp_show_speedtest,
"Shows the DevNull (Speed Test) folder.\n\n"
"This folder is used for benchmarking USB uploads. "
"This ia virtual folder, nothing is actally written to disk."_i18n
);
}
void App::DisplayHddOptions(bool left_side) {
auto options = std::make_unique<ui::Sidebar>("HDD Options"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(std::move(options)));
options->Add<ui::SidebarEntryBool>("Enable"_i18n, App::GetHddEnable(), [](bool& enable){
App::SetHddEnable(enable);
}, "Enable mounting of connected USB/HDD devices. "
"Connected devices can be used in the FileBrowser, as well as a backup location when dumping games and saves."_i18n
);
options->Add<ui::SidebarEntryBool>("HDD write protect"_i18n, App::GetWriteProtect(), [](bool& enable){
App::SetWriteProtect(enable);
}, "Makes the connected HDD read-only."_i18n);
}
void App::ShowEnableInstallPrompt() {
// warn the user the dangers of installing.
App::Push<ui::OptionBox>(

View File

@@ -6,7 +6,6 @@
#include "utils/thread.hpp"
#include <algorithm>
#include <minIni.h>
#include <ftpsrv.h>
#include <ftpsrv_vfs.h>
#include <nx/vfs_nx.h>
@@ -30,11 +29,12 @@ struct InstallSharedData {
bool enabled;
};
const char* INI_PATH = "/config/ftpsrv/config.ini";
FtpSrvConfig g_ftpsrv_config = {0};
std::atomic_bool g_should_exit = false;
bool g_is_running{false};
Thread g_thread;
FtpSrvConfig g_ftpsrv_config{};
int g_ftpsrv_mount_flags{};
std::vector<VfsNxCustomPath> g_custom_vfs{};
std::atomic_bool g_should_exit{};
bool g_is_running{};
Thread g_thread{};
Mutex g_mutex{};
void ftp_log_callback(enum FTP_API_LOG_TYPE type, const char* msg) {
@@ -468,74 +468,12 @@ FtpVfs g_vfs_stdio = {
void loop(void* arg) {
log_write("[FTP] loop entered\n");
// load config.
{
SCOPED_MUTEX(&g_mutex);
g_ftpsrv_config.log_callback = ftp_log_callback;
g_ftpsrv_config.progress_callback = ftp_progress_callback;
g_ftpsrv_config.anon = ini_getbool("Login", "anon", 0, INI_PATH);
int user_len = ini_gets("Login", "user", "", g_ftpsrv_config.user, sizeof(g_ftpsrv_config.user), INI_PATH);
int pass_len = ini_gets("Login", "pass", "", g_ftpsrv_config.pass, sizeof(g_ftpsrv_config.pass), INI_PATH);
g_ftpsrv_config.port = ini_getl("Network", "port", 5000, INI_PATH); // 5000 to keep compat with older sphaira
g_ftpsrv_config.timeout = ini_getl("Network", "timeout", 0, INI_PATH);
g_ftpsrv_config.use_localtime = ini_getbool("Misc", "use_localtime", 0, INI_PATH);
bool log_enabled = ini_getbool("Log", "log", 0, INI_PATH);
// get nx config
bool mount_devices = ini_getbool("Nx", "mount_devices", 1, INI_PATH);
bool mount_bis = ini_getbool("Nx", "mount_bis", 0, INI_PATH);
bool save_writable = ini_getbool("Nx", "save_writable", 0, INI_PATH);
g_ftpsrv_config.port = ini_getl("Nx", "app_port", g_ftpsrv_config.port, INI_PATH); // compat
// get Nx-App overrides
g_ftpsrv_config.anon = ini_getbool("Nx-App", "anon", g_ftpsrv_config.anon, INI_PATH);
user_len = ini_gets("Nx-App", "user", g_ftpsrv_config.user, g_ftpsrv_config.user, sizeof(g_ftpsrv_config.user), INI_PATH);
pass_len = ini_gets("Nx-App", "pass", g_ftpsrv_config.pass, g_ftpsrv_config.pass, sizeof(g_ftpsrv_config.pass), INI_PATH);
g_ftpsrv_config.port = ini_getl("Nx-App", "port", g_ftpsrv_config.port, INI_PATH);
g_ftpsrv_config.timeout = ini_getl("Nx-App", "timeout", g_ftpsrv_config.timeout, INI_PATH);
g_ftpsrv_config.use_localtime = ini_getbool("Nx-App", "use_localtime", g_ftpsrv_config.use_localtime, INI_PATH);
log_enabled = ini_getbool("Nx-App", "log", log_enabled, INI_PATH);
mount_devices = ini_getbool("Nx-App", "mount_devices", mount_devices, INI_PATH);
mount_bis = ini_getbool("Nx-App", "mount_bis", mount_bis, INI_PATH);
save_writable = ini_getbool("Nx-App", "save_writable", save_writable, INI_PATH);
g_should_exit = false;
mount_devices = true;
g_ftpsrv_config.timeout = 0;
if (!g_ftpsrv_config.port) {
g_ftpsrv_config.port = 5000;
log_write("[FTP] no port config, defaulting to 5000\n");
}
// keep compat with older sphaira
if (!user_len && !pass_len) {
g_ftpsrv_config.anon = true;
log_write("[FTP] no user pass, defaulting to anon\n");
}
fsdev_wrapMountSdmc();
static const VfsNxCustomPath custom_vfs[] = {
{
.name = "games",
.user = NULL,
.func = &g_vfs_stdio,
},
{
.name = "mounts",
.user = NULL,
.func = &g_vfs_stdio,
},
{
.name = "install",
.user = NULL,
.func = &g_vfs_install,
},
};
vfs_nx_init(custom_vfs, std::size(custom_vfs), mount_devices, save_writable, mount_bis, false);
vfs_nx_init(g_custom_vfs.data(), std::size(g_custom_vfs), g_ftpsrv_mount_flags, false);
}
ON_SCOPE_EXIT(
@@ -566,13 +504,90 @@ bool Init() {
return false;
}
// if (R_FAILED(fsdev_wrapMountSdmc())) {
// log_write("[FTP] cannot mount sdmc\n");
// return false;
// }
g_ftpsrv_mount_flags = 0;
g_ftpsrv_config = {};
g_custom_vfs.clear();
// todo: replace everything with ini_browse for faster loading.
// or load everything in the init thread.
auto app = App::GetApp();
g_ftpsrv_config.log_callback = ftp_log_callback;
g_ftpsrv_config.progress_callback = ftp_progress_callback;
g_ftpsrv_config.anon = app->m_ftp_anon.Get();
std::strncpy(g_ftpsrv_config.user, app->m_ftp_user.Get().c_str(), sizeof(g_ftpsrv_config.user) - 1);
std::strncpy(g_ftpsrv_config.pass, app->m_ftp_pass.Get().c_str(), sizeof(g_ftpsrv_config.pass) - 1);
g_ftpsrv_config.port = app->m_ftp_port.Get();
if (app->m_ftp_show_album.Get()) {
g_ftpsrv_mount_flags |= VfsNxMountFlag_ALBUM;
}
if (app->m_ftp_show_ams_contents.Get()) {
g_ftpsrv_mount_flags |= VfsNxMountFlag_AMS_CONTENTS;
}
if (app->m_ftp_show_bis_storage.Get()) {
g_ftpsrv_mount_flags |= VfsNxMountFlag_BIS_STORAGE;
}
if (app->m_ftp_show_bis_fs.Get()) {
g_ftpsrv_mount_flags |= VfsNxMountFlag_BIS_FS;
}
if (app->m_ftp_show_content_system.Get()) {
g_ftpsrv_mount_flags |= VfsNxMountFlag_CONTENT_SYSTEM;
}
if (app->m_ftp_show_content_user.Get()) {
g_ftpsrv_mount_flags |= VfsNxMountFlag_CONTENT_USER;
}
if (app->m_ftp_show_content_sd.Get()) {
g_ftpsrv_mount_flags |= VfsNxMountFlag_CONTENT_SDCARD;
}
#if 0
if (app->m_ftp_show_content_sd0.Get()) {
g_ftpsrv_mount_flags |= VfsNxMountFlag_CONTENT_SDCARD0;
}
if (app->m_ftp_show_custom_system.Get()) {
g_ftpsrv_mount_flags |= VfsNxMountFlag_CUSTOM_SYSTEM;
}
if (app->m_ftp_show_custom_sd.Get()) {
g_ftpsrv_mount_flags |= VfsNxMountFlag_CUSTOM_SDCARD;
}
#endif
if (app->m_ftp_show_switch.Get()) {
g_ftpsrv_mount_flags |= VfsNxMountFlag_SWITCH;
}
if (app->m_ftp_show_install.Get()) {
g_custom_vfs.push_back({
.name = "install",
.user = &g_shared_data,
.func = &g_vfs_install,
});
}
if (app->m_ftp_show_games.Get()) {
g_custom_vfs.push_back({
.name = "games",
.user = NULL,
.func = &g_vfs_stdio,
});
}
if (app->m_ftp_show_mounts.Get()) {
g_custom_vfs.push_back({
.name = "mounts",
.user = NULL,
.func = &g_vfs_stdio,
});
}
g_ftpsrv_config.timeout = 0;
if (!g_ftpsrv_config.port) {
g_ftpsrv_config.port = 5000;
log_write("[FTP] no port config, defaulting to 5000\n");
}
// keep compat with older sphaira
if (!std::strlen(g_ftpsrv_config.user) && !std::strlen(g_ftpsrv_config.pass)) {
g_ftpsrv_config.anon = true;
log_write("[FTP] no user pass, defaulting to anon\n");
}
Result rc;
if (R_FAILED(rc = utils::CreateThread(&g_thread, loop, nullptr))) {
@@ -602,7 +617,9 @@ void Exit() {
threadWaitForExit(&g_thread);
threadClose(&g_thread);
memset(&g_ftpsrv_config, 0, sizeof(g_ftpsrv_config));
std::memset(&g_ftpsrv_config, 0, sizeof(g_ftpsrv_config));
g_custom_vfs.clear();
g_ftpsrv_mount_flags = 0;
log_write("[FTP] exitied\n");
}

View File

@@ -669,12 +669,40 @@ bool Init() {
return false;
}
// add default mount of the sd card.
g_fs_entries.emplace_back(std::make_shared<FsProxy>(std::make_unique<fs::FsNativeSd>(), "", "microSD card"));
if (App::GetApp()->m_mtp_show_album.Get()) {
g_fs_entries.emplace_back(std::make_shared<FsProxy>(std::make_unique<fs::FsNativeImage>(FsImageDirectoryId_Sd), "Album", "Album (Image SD)"));
}
if (App::GetApp()->m_mtp_show_content_sd.Get()) {
g_fs_entries.emplace_back(std::make_shared<FsProxy>(std::make_unique<fs::FsNativeContentStorage>(FsContentStorageId_SdCard), "ContentsM", "Contents (microSD card)"));
}
if (App::GetApp()->m_mtp_show_content_system.Get()) {
g_fs_entries.emplace_back(std::make_shared<FsProxy>(std::make_unique<fs::FsNativeContentStorage>(FsContentStorageId_System), "ContentsS", "Contents (System)"));
}
if (App::GetApp()->m_mtp_show_content_user.Get()) {
g_fs_entries.emplace_back(std::make_shared<FsProxy>(std::make_unique<fs::FsNativeContentStorage>(FsContentStorageId_User), "ContentsU", "Contents (User)"));
}
if (App::GetApp()->m_mtp_show_games.Get()) {
g_fs_entries.emplace_back(std::make_shared<FsProxy>(std::make_unique<fs::FsStdio>(true, "games:/"), "Games", "Games"));
g_fs_entries.emplace_back(std::make_shared<FsProxy>(std::make_unique<fs::FsStdio>(true, "mounts:/"), "Mounts", "Mounts"));
g_fs_entries.emplace_back(std::make_shared<FsDevNullProxy>("DevNull", "DevNull (Speed Test)"));
}
if (App::GetApp()->m_mtp_show_install.Get()) {
g_fs_entries.emplace_back(std::make_shared<FsInstallProxy>("install", "Install (NSP, XCI, NSZ, XCZ)"));
}
if (App::GetApp()->m_mtp_show_mounts.Get()) {
g_fs_entries.emplace_back(std::make_shared<FsProxy>(std::make_unique<fs::FsStdio>(true, "mounts:/"), "Mounts", "Mounts"));
}
if (App::GetApp()->m_mtp_show_speedtest.Get()) {
g_fs_entries.emplace_back(std::make_shared<FsDevNullProxy>("DevNull", "DevNull (Speed Test)"));
}
g_should_exit = false;
if (!haze::Initialize(haze_callback, g_fs_entries, App::GetApp()->m_mtp_vid.Get(), App::GetApp()->m_mtp_pid.Get())) {

View File

@@ -337,15 +337,19 @@ MainMenu::MainMenu() {
});
}
options->Add<SidebarEntryBool>("FTP"_i18n, App::GetFtpEnable(), [](bool& enable){
App::SetFtpEnable(enable);
}, "Enable FTP server to run in the background.\n\n"
"The default port is 5000 with no user/pass set. "
"You can change this behaviour in /config/ftpsrv/config.ini"_i18n);
options->Add<SidebarEntryCallback>("FTP"_i18n, [](){ App::DisplayFtpOptions(); },
"Enable / modify the FTP server settings such as port, user/pass and the folders that are shown.\n\n"
"NOTE: Changing any of the options will automatically restart the FTP server when exiting the options menu."_i18n
);
options->Add<SidebarEntryBool>("MTP"_i18n, App::GetMtpEnable(), [](bool& enable){
App::SetMtpEnable(enable);
}, "Enable MTP server to run in the background."_i18n);
options->Add<SidebarEntryCallback>("MTP"_i18n, [](){ App::DisplayMtpOptions(); },
"Enable / modify the MTP responder settings such as the folders that are shown.\n\n"
"NOTE: Changing any of the options will automatically restart the MTP server when exiting the options menu."_i18n
);
options->Add<SidebarEntryCallback>("HDD"_i18n, [](){
App::DisplayHddOptions();
}, "Enable / modify the HDD mount options."_i18n);
options->Add<SidebarEntryBool>("NXlink"_i18n, App::GetNxlinkEnable(), [](bool& enable){
App::SetNxlinkEnable(enable);
@@ -353,14 +357,6 @@ MainMenu::MainMenu() {
"NXlink is used to send .nro's from PC to the switch\n\n"
"If you are not a developer, you can disable this option."_i18n);
options->Add<SidebarEntryBool>("HDD"_i18n, App::GetHddEnable(), [](bool& enable){
App::SetHddEnable(enable);
}, "Enable mounting of connected USB/HDD devices. "
"Connected devices can be used in the FileBrowser, as well as a backup location when dumping games and saves."_i18n);
options->Add<SidebarEntryBool>("HDD write protect"_i18n, App::GetWriteProtect(), [](bool& enable){
App::SetWriteProtect(enable);
}, "Makes the connected HDD read-only."_i18n);
}, "Toggle FTP, MTP, HDD and NXlink\n\n"
"If Sphaira has a update available, you can download it from this menu"_i18n);

View File

@@ -124,24 +124,27 @@ SidebarEntryBool::SidebarEntryBool(const std::string& title, bool option, const
} else {
m_option ^= 1;
m_callback(m_option);
SetDirty();
} }
});
}
SidebarEntryBool::SidebarEntryBool(const std::string& title, bool& option, const std::string& info, const std::string& true_str, const std::string& false_str)
: SidebarEntryBool{title, option, Callback{}, info, true_str, false_str} {
m_callback = [&option](bool&){
m_callback = [this, &option](bool&){
option ^= 1;
SetDirty();
};
}
SidebarEntryBool::SidebarEntryBool(const std::string& title, option::OptionBool& option, const Callback& cb, const std::string& info, const std::string& true_str, const std::string& false_str)
: SidebarEntryBool{title, option.Get(), Callback{}, info, true_str, false_str} {
m_callback = [&option, cb](bool& v_out){
m_callback = [this, &option, cb](bool& v_out){
if (cb) {
cb(v_out);
}
option.Set(v_out);
SetDirty();
};
}
@@ -166,6 +169,7 @@ SidebarEntrySlider::SidebarEntrySlider(const std::string& title, float value, fl
DependsClick();
} else {
m_value = std::clamp(m_value - m_inc, m_min, m_max);
SetDirty();
// m_callback(m_option);
} }
});
@@ -174,6 +178,7 @@ SidebarEntrySlider::SidebarEntrySlider(const std::string& title, float value, fl
DependsClick();
} else {
m_value = std::clamp(m_value + m_inc, m_min, m_max);
SetDirty();
// m_callback(m_option);
} }
});
@@ -240,6 +245,8 @@ SidebarEntryArray::SidebarEntryArray(const std::string& title, const Items& item
App::Push<PopupList>(
m_title, m_items, index, m_index
);
SetDirty();
};
}
@@ -275,6 +282,7 @@ SidebarEntryArray::SidebarEntryArray(const std::string& title, const Items& item
} else {
// m_callback(m_index);
m_list_callback();
SetDirty();
}}
});
}
@@ -291,6 +299,7 @@ SidebarEntryTextBase::SidebarEntryTextBase(const std::string& title, const std::
SetAction(Button::A, Action{"OK"_i18n, [this](){
if (m_callback) {
m_callback();
SetDirty();
}
}});
}
@@ -300,26 +309,35 @@ void SidebarEntryTextBase::Draw(NVGcontext* vg, Theme* theme, const Vec4& root_p
SidebarEntryBase::DrawEntry(vg, theme, m_title, m_value, true);
}
SidebarEntryTextInput::SidebarEntryTextInput(const std::string& title, const std::string& value, const std::string& guide, s64 len_min, s64 len_max, const std::string& info)
SidebarEntryTextInput::SidebarEntryTextInput(const std::string& title, const std::string& value, const std::string& guide, s64 len_min, s64 len_max, const std::string& info, const Callback& callback)
: SidebarEntryTextBase{title, value, {}, info}
, m_guide{guide}
, m_len_min{len_min}
, m_len_max{len_max} {
, m_len_max{len_max}
, m_callback{callback} {
SetCallback([this](){
std::string out;
if (R_SUCCEEDED(swkbd::ShowText(out, m_guide.c_str(), GetValue().c_str(), m_len_min, m_len_max))) {
SetValue(out);
if (m_callback) {
m_callback(this);
}
}
});
}
SidebarEntryTextInput::SidebarEntryTextInput(const std::string& title, s64 value, const std::string& guide, s64 len_min, s64 len_max, const std::string& info)
: SidebarEntryTextInput{title, std::to_string(value), guide, len_min, len_max, info} {
SidebarEntryTextInput::SidebarEntryTextInput(const std::string& title, s64 value, const std::string& guide, s64 len_min, s64 len_max, const std::string& info, const Callback& callback)
: SidebarEntryTextInput{title, std::to_string(value), guide, len_min, len_max, info, callback} {
SetCallback([this](){
s64 out = std::stoul(GetValue());
if (R_SUCCEEDED(swkbd::ShowNumPad(out, m_guide.c_str(), GetValue().c_str(), m_len_min, m_len_max))) {
SetValue(std::to_string(out));
if (m_callback) {
m_callback(this);
}
}
});
}
@@ -331,6 +349,7 @@ SidebarEntryFilePicker::SidebarEntryFilePicker(const std::string& title, const s
App::Push<menu::filebrowser::picker::Menu>(
[this](const fs::FsPath& path) {
SetValue(path);
SetDirty();
return true;
},
m_filter
@@ -370,6 +389,17 @@ Sidebar::Sidebar(const std::string& title, const std::string& sub, Side side, fl
m_list->SetScrollBarPos(GetX() + GetW() - 20, m_base_pos.y - 10, pos.h - m_base_pos.y + 48);
}
Sidebar::~Sidebar() {
if (m_on_exit_when_changed) {
for (const auto& item : m_items) {
if (item->IsDirty()) {
m_on_exit_when_changed();
break;
}
}
}
}
auto Sidebar::Update(Controller* controller, TouchInfo* touch) -> void {
Widget::Update(controller, touch);