From 93c38da7424e7627dbce902c426313aa8c3d69ec Mon Sep 17 00:00:00 2001 From: ITotalJustice <47043333+ITotalJustice@users.noreply.github.com> Date: Fri, 23 May 2025 12:23:28 +0100 Subject: [PATCH] add support for mounting stdio (hdd) in the file browser. --- sphaira/include/fs.hpp | 90 ++++++++- sphaira/include/ui/menus/filebrowser.hpp | 39 +++- sphaira/include/ui/progress_box.hpp | 1 + sphaira/source/fs.cpp | 235 +++++++++++++++++++++++ sphaira/source/ui/menus/filebrowser.cpp | 195 ++++++++++--------- sphaira/source/ui/progress_box.cpp | 37 ++-- 6 files changed, 485 insertions(+), 112 deletions(-) diff --git a/sphaira/include/fs.hpp b/sphaira/include/fs.hpp index 8f81c01..987a47f 100644 --- a/sphaira/include/fs.hpp +++ b/sphaira/include/fs.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -169,6 +170,26 @@ static_assert(FsPath::TestFrom(std::string_view{"abc"})); static_assert(FsPath::TestFrom(std::string{"abc"})); static_assert(FsPath::TestFrom(FsPath{"abc"})); +// fwd +struct Fs; + +struct File { + fs::Fs* m_fs; + FsFile m_native; + std::FILE* m_stdio; + s64 m_stdio_off; + // sadly, fatfs doesn't support fstat, so we have to manually + // stat the file to get it's size. + FsPath m_path; +}; + +struct Dir { + fs::Fs* m_fs; + FsDir m_native; + DIR* m_stdio; + u32 m_mode; +}; + FsPath AppendPath(const fs::FsPath& root_path, const fs::FsPath& file_path); Result CreateFile(FsFileSystem* fs, const FsPath& path, u64 size = 0, u32 option = 0, bool ignore_read_only = true); @@ -205,6 +226,24 @@ Result read_entire_file(const FsPath& path, std::vector& out); Result write_entire_file(const FsPath& path, const std::vector& in, bool ignore_read_only = true); Result copy_entire_file(const FsPath& dst, const FsPath& src, bool ignore_read_only = true); +Result OpenFile(fs::Fs* fs, const fs::FsPath& path, u32 mode, File* f); +Result FileRead(File* f, s64 off, void* buf, u64 read_size, u32 option, u64* bytes_read); +Result FileWrite(File* f, s64 off, const void* buf, u64 write_size, u32 option); +Result FileSetSize(File* f, s64 sz); +Result FileGetSize(File* f, s64* out); +void FileClose(File* f); + +Result OpenDirectory(fs::Fs* fs, const fs::FsPath& path, u32 mode, Dir* d); +Result DirReadAll(Dir* d, std::vector& buf); +void DirClose(Dir* d); + +// optimised for stdio calls as stat returns size and timestamp in a single call. +// whereas for native, this is 2 function calls. +// however if you need both, you will need 2 calls for native anyway, +// but can avoid the second (expensive) stat call. +Result FileGetSizeAndTimestamp(fs::Fs* fs, const FsPath& path, FsTimeStampRaw* ts, s64* size); +Result IsDirEmpty(fs::Fs* m_fs, const fs::FsPath& path, bool* out); + struct Fs { static constexpr inline u32 FsModule = 505; static constexpr inline Result ResultTooManyEntries = MAKERESULT(FsModule, 1); @@ -238,10 +277,48 @@ struct Fs { virtual Result GetFileTimeStampRaw(const FsPath& path, FsTimeStampRaw *out) = 0; virtual bool FileExists(const FsPath& path) = 0; virtual bool DirExists(const FsPath& path) = 0; + virtual bool IsNative() const = 0; + virtual FsPath Root() const { return "/"; } virtual Result read_entire_file(const FsPath& path, std::vector& out) = 0; virtual Result write_entire_file(const FsPath& path, const std::vector& in) = 0; virtual Result copy_entire_file(const FsPath& dst, const FsPath& src) = 0; + Result OpenFile(const fs::FsPath& path, u32 mode, File* f) { + return fs::OpenFile(this, path, mode, f); + } + Result FileRead(File* f, s64 off, void* buf, u64 read_size, u32 option, u64* bytes_read) { + return fs::FileRead(f, off, buf, read_size, option, bytes_read); + } + Result FileWrite(File* f, s64 off, const void* buf, u64 write_size, u32 option) { + return fs::FileWrite(f, off, buf, write_size, option); + } + Result FileSetSize(File* f, s64 sz) { + return fs::FileSetSize(f, sz); + } + Result FileGetSize(File* f, s64* out) { + return fs::FileGetSize(f, out); + } + void FileClose(File* f) { + fs::FileClose(f); + } + + Result OpenDirectory(const fs::FsPath& path, u32 mode, Dir* d) { + return fs::OpenDirectory(this, path, mode, d); + } + Result DirReadAll(Dir* d, std::vector& buf) { + return fs::DirReadAll(d, buf); + } + void DirClose(Dir* d) { + fs::DirClose(d); + } + + Result FileGetSizeAndTimestamp(const FsPath& path, FsTimeStampRaw* ts, s64* size) { + return fs::FileGetSizeAndTimestamp(this, path, ts, size); + } + Result IsDirEmpty(const fs::FsPath& path, bool* out) { + return fs::IsDirEmpty(this, path, out); + } + void SetIgnoreReadOnly(bool enable) { m_ignore_read_only = enable; } @@ -251,7 +328,7 @@ protected: }; struct FsStdio : Fs { - FsStdio(bool ignore_read_only = true) : Fs{ignore_read_only} {} + FsStdio(bool ignore_read_only = true, const FsPath& root = "/") : Fs{ignore_read_only}, m_root{root} {} virtual ~FsStdio() = default; Result CreateFile(const FsPath& path, u64 size = 0, u32 option = 0) override { @@ -293,6 +370,12 @@ struct FsStdio : Fs { bool DirExists(const FsPath& path) override { return fs::DirExists(path); } + bool IsNative() const override { + return false; + } + FsPath Root() const override { + return m_root; + } Result read_entire_file(const FsPath& path, std::vector& out) override { return fs::read_entire_file(path, out); } @@ -302,6 +385,8 @@ struct FsStdio : Fs { Result copy_entire_file(const FsPath& dst, const FsPath& src) override { return fs::copy_entire_file(dst, src, m_ignore_read_only); } + + const FsPath m_root; }; struct FsNative : Fs { @@ -407,6 +492,9 @@ struct FsNative : Fs { bool DirExists(const FsPath& path) override { return fs::DirExists(&m_fs, path); } + bool IsNative() const override { + return true; + } Result read_entire_file(const FsPath& path, std::vector& out) override { return fs::read_entire_file(&m_fs, path, out); } diff --git a/sphaira/include/ui/menus/filebrowser.hpp b/sphaira/include/ui/menus/filebrowser.hpp index 2c92588..42797ec 100644 --- a/sphaira/include/ui/menus/filebrowser.hpp +++ b/sphaira/include/ui/menus/filebrowser.hpp @@ -10,10 +10,19 @@ namespace sphaira::ui::menu::filebrowser { +enum FsEntryFlag { + FsEntryFlag_None, + // write protected. + FsEntryFlag_ReadOnly = 1 << 0, + // supports file assoc. + FsEntryFlag_Assoc = 1 << 1, +}; + enum class FsType { Sd, ImageNand, ImageSd, + Stdio, }; enum class SelectedType { @@ -33,6 +42,21 @@ enum OrderType { OrderType_Ascending, }; +struct FsEntry { + fs::FsPath name{}; + fs::FsPath root{}; + FsType type{}; + u32 flags{FsEntryFlag_None}; + + auto IsReadOnly() const -> bool { + return flags & FsEntryFlag_ReadOnly; + } + + auto IsAssoc() const -> bool { + return flags & FsEntryFlag_Assoc; + } +}; + // roughly 1kib in size per entry struct FileEntry : FsDirectoryEntry { std::string extension{}; // if any @@ -225,6 +249,10 @@ private: return GetEntry(m_index); } + auto IsSd() const -> bool { + return m_fs_entry.type == FsType::Sd; + } + void Sort(); void SortAndFindLastFile(); void SetIndexFromLastFile(const LastFile& last_file); @@ -238,14 +266,18 @@ private: auto get_collection(const fs::FsPath& path, const fs::FsPath& parent_name, FsDirCollection& out, bool inc_file, bool inc_dir, bool inc_size) -> Result; auto get_collections(const fs::FsPath& path, const fs::FsPath& parent_name, FsDirCollections& out, bool inc_size = false) -> Result; - void SetFs(const fs::FsPath& new_path, u32 new_type); + void SetFs(const fs::FsPath& new_path, const FsEntry& new_entry); + + auto GetNative() -> fs::FsNative* { + return (fs::FsNative*)m_fs.get(); + } private: static constexpr inline const char* INI_SECTION = "filebrowser"; const std::vector& m_nro_entries; - std::unique_ptr m_fs{}; - FsType m_fs_type{}; + std::unique_ptr m_fs{}; + FsEntry m_fs_entry{}; fs::FsPath m_path{}; std::vector m_entries{}; std::vector m_entries_index{}; // files not including hidden @@ -280,7 +312,6 @@ private: option::OptionBool m_folders_first{INI_SECTION, "folders_first", true}; option::OptionBool m_hidden_last{INI_SECTION, "hidden_last", false}; option::OptionBool m_ignore_read_only{INI_SECTION, "ignore_read_only", false}; - option::OptionLong m_mount{INI_SECTION, "mount", 0}; bool m_loaded_assoc_entries{}; bool m_is_update_folder{}; diff --git a/sphaira/include/ui/progress_box.hpp b/sphaira/include/ui/progress_box.hpp index 273f31c..cf7c69e 100644 --- a/sphaira/include/ui/progress_box.hpp +++ b/sphaira/include/ui/progress_box.hpp @@ -36,6 +36,7 @@ struct ProgressBox final : Widget { auto ShouldExitResult() -> Result; // helper functions + auto CopyFile(fs::Fs* fs, const fs::FsPath& src, const fs::FsPath& dst) -> Result; auto CopyFile(const fs::FsPath& src, const fs::FsPath& dst) -> Result; void Yield(); diff --git a/sphaira/source/fs.cpp b/sphaira/source/fs.cpp index 21f81f4..cab0558 100644 --- a/sphaira/source/fs.cpp +++ b/sphaira/source/fs.cpp @@ -121,12 +121,18 @@ Result CreateDirectoryRecursively(FsFileSystem* fs, const FsPath& _path, bool ig auto path_view = std::string_view{_path}; // todo: fix this for sdmc: and ums0: FsPath path{"/"}; + if (auto s = std::strchr(_path.s, ':')) { + const int len = (s - _path.s) + 1; + std::snprintf(path, sizeof(path), "%.*s/", len, _path.s); + path_view = path_view.substr(len); + } for (const auto dir : std::views::split(path_view, '/')) { if (dir.empty()) { continue; } std::strncat(path, dir.data(), dir.size()); + log_write("[FS] dir creation path is now: %s\n", path.s); Result rc; if (fs) { @@ -447,4 +453,233 @@ Result copy_entire_file(const FsPath& dst, const FsPath& src, bool ignore_read_o return write_entire_file(dst, data, ignore_read_only); } +Result OpenFile(fs::Fs* fs, const fs::FsPath& path, u32 mode, File* f) { + *f = {}; + f->m_fs = fs; + + if (f->m_fs->IsNative()) { + auto fs = (fs::FsNative*)f->m_fs; + R_TRY(fs->OpenFile(path, mode, &f->m_native)); + } else { + if ((mode & FsOpenMode_Read) && (mode & FsOpenMode_Write)) { + // todo: + R_THROW(0x1); + } else if (mode & FsOpenMode_Read) { + f->m_stdio = fopen(path, "rb"); + } else if (mode & FsOpenMode_Write) { + f->m_stdio = fopen(path, "wb"); + } + + R_UNLESS(f->m_stdio, 0x1); + } + + std::strcpy(f->m_path, path); + R_SUCCEED(); +} + +Result FileRead(File* f, s64 off, void* buf, u64 read_size, u32 option, u64* bytes_read) { + if (f->m_fs->IsNative()) { + R_TRY(fsFileRead(&f->m_native, off, buf, read_size, option, bytes_read)); + } else { + if (f->m_stdio_off != off) { + f->m_stdio_off = off; + std::fseek(f->m_stdio, off, SEEK_SET); + } + + *bytes_read = std::fread(buf, 1, read_size, f->m_stdio); + f->m_stdio_off += *bytes_read; + } + + R_SUCCEED(); +} + +Result FileWrite(File* f, s64 off, const void* buf, u64 write_size, u32 option) { + if (f->m_fs->IsNative()) { + R_TRY(fsFileWrite(&f->m_native, off, buf, write_size, option)); + } else { + if (f->m_stdio_off != off) { + log_write("[FS] diff seek\n"); + f->m_stdio_off = off; + std::fseek(f->m_stdio, off, SEEK_SET); + } + + const auto result = std::fwrite(buf, 1, write_size, f->m_stdio); + // log_write("[FS] fwrite res: %zu vs %zu\n", result, write_size); + R_UNLESS(result == write_size, 0x1); + + f->m_stdio_off += write_size; + } + + R_SUCCEED(); +} + +Result FileSetSize(File* f, s64 sz) { + if (f->m_fs->IsNative()) { + R_TRY(fsFileSetSize(&f->m_native, sz)); + } else { + R_SUCCEED(); + // const auto fd = fileno(f->m_stdio); + // R_UNLESS(fd > 0, 0x1); + // R_UNLESS(!ftruncate(fd, sz), 0x1); + } + + R_SUCCEED(); +} + +Result FileGetSize(File* f, s64* out) { + if (f->m_fs->IsNative()) { + R_TRY(fsFileGetSize(&f->m_native, out)); + } else { + struct stat st; + const auto fd = fileno(f->m_stdio); + bool did_stat{}; + + if (fd && !fstat(fd, &st)) { + did_stat = true; + } + + if (!did_stat) { + R_UNLESS(!lstat(f->m_path, &st), 0x1); + } + + *out = st.st_size; + } + + R_SUCCEED(); +} + +void FileClose(File* f) { + if (f->m_fs->IsNative()) { + fsFileClose(&f->m_native); + } else { + std::fclose(f->m_stdio); + } + + *f = {}; +} + +Result OpenDirectory(fs::Fs* fs, const fs::FsPath& path, u32 mode, Dir* d) { + *d = {}; + d->m_fs = fs; + d->m_mode = mode; + + if (d->m_fs->IsNative()) { + auto fs = (fs::FsNative*)d->m_fs; + R_TRY(fs->OpenDirectory(path, mode, &d->m_native)); + } else { + d->m_stdio = opendir(path); + R_UNLESS(d->m_stdio, 0x1); + } + + R_SUCCEED(); +} + +Result DirReadAll(Dir* d, std::vector& buf) { + buf.clear(); + + if (d->m_fs->IsNative()) { + auto fs = (fs::FsNative*)d->m_fs; + + s64 count; + R_TRY(fs->DirGetEntryCount(&d->m_native, &count)); + + buf.resize(count); + R_TRY(fs->DirRead(&d->m_native, &count, buf.size(), buf.data())); + buf.resize(count); + } else { + buf.reserve(1000); + + struct dirent* dirent; + while ((dirent = readdir(d->m_stdio))) { + if (!std::strcmp(dirent->d_name, ".") || !std::strcmp(dirent->d_name, "..")) { + continue; + } + + FsDirectoryEntry entry{}; + + if (dirent->d_type == DT_DIR) { + if (!(d->m_mode & FsDirOpenMode_ReadDirs)) { + continue; + } + entry.type = FsDirEntryType_Dir; + } else if (dirent->d_type == DT_REG) { + if (!(d->m_mode & FsDirOpenMode_ReadFiles)) { + continue; + } + entry.type = FsDirEntryType_File; + } + + std::strcpy(entry.name, dirent->d_name); + buf.emplace_back(entry); + } + } + + R_SUCCEED(); +} + +void DirClose(Dir* d) { + if (d->m_fs->IsNative()) { + fsDirClose(&d->m_native); + } else { + closedir(d->m_stdio); + } + + *d = {}; +} + +Result FileGetSizeAndTimestamp(fs::Fs* m_fs, const FsPath& path, FsTimeStampRaw* ts, s64* size) { + *ts = {}; + *size = {}; + + if (m_fs->IsNative()) { + auto fs = (fs::FsNative*)m_fs; + R_TRY(fs->GetFileTimeStampRaw(path, ts)); + + File f; + R_TRY(m_fs->OpenFile(path, FsOpenMode_Read, &f)); + ON_SCOPE_EXIT(fs->FileClose(&f)); + + R_TRY(m_fs->FileGetSize(&f, size)); + } else { + struct stat st; + R_UNLESS(!lstat(path, &st), 0x1); + + ts->is_valid = true; + ts->created = st.st_ctim.tv_sec; + ts->modified = st.st_mtim.tv_sec; + ts->accessed = st.st_atim.tv_sec; + *size = st.st_size; + } + + R_SUCCEED(); +} + +Result IsDirEmpty(fs::Fs* m_fs, const fs::FsPath& path, bool* out) { + *out = true; + + if (m_fs->IsNative()) { + auto fs = (fs::FsNative*)m_fs; + + s64 count; + R_TRY(fs->DirGetEntryCount(path, FsDirOpenMode_ReadDirs | FsDirOpenMode_ReadFiles | FsDirOpenMode_NoFileSize, &count)); + *out = !count; + } else { + auto dir = opendir(path); + R_UNLESS(dir, 0x1); + ON_SCOPE_EXIT(closedir(dir)); + + struct dirent* dirent; + while ((dirent = readdir(dir))) { + if (!std::strcmp(dirent->d_name, ".") || !std::strcmp(dirent->d_name, "..")) { + continue; + } + + *out = false; + break; + } + } + + R_SUCCEED(); +} + } // namespace fs diff --git a/sphaira/source/ui/menus/filebrowser.cpp b/sphaira/source/ui/menus/filebrowser.cpp index 9d4d5b4..b05159a 100644 --- a/sphaira/source/ui/menus/filebrowser.cpp +++ b/sphaira/source/ui/menus/filebrowser.cpp @@ -42,6 +42,16 @@ namespace sphaira::ui::menu::filebrowser { namespace { +constexpr FsEntry FS_ENTRY_DEFAULT{ + "Sd", "/", FsType::Sd, FsEntryFlag_Assoc, +}; + +constexpr FsEntry FS_ENTRIES[]{ + FS_ENTRY_DEFAULT, + { "Image System memory", "/", FsType::ImageNand }, + { "Image microSD card", "/", FsType::ImageSd}, +}; + struct ExtDbEntry { std::string_view db_name; std::span ext; @@ -198,7 +208,7 @@ auto GetRomDatabaseFromPath(std::string_view path) -> RomDatabaseIndexs { } // -auto GetRomIcon(fs::FsNative* fs, ProgressBox* pbox, std::string filename, const RomDatabaseIndexs& db_indexs, const NroEntry& nro) { +auto GetRomIcon(fs::Fs* fs, ProgressBox* pbox, std::string filename, const RomDatabaseIndexs& db_indexs, const NroEntry& nro) { // if no db entries, use nro icon if (db_indexs.empty()) { log_write("using nro image\n"); @@ -315,7 +325,7 @@ Menu::Menu(const std::vector& nro_entries) : MenuBase{"FileBrowser"_i1 return; } - if (m_fs_type == FsType::Sd && m_is_update_folder && m_daybreak_path.has_value()) { + if (IsSd() && m_is_update_folder && m_daybreak_path.has_value()) { App::Push(std::make_shared("Open with DayBreak?"_i18n, "No"_i18n, "Yes"_i18n, 1, [this](auto op_index){ if (op_index && *op_index) { // daybreak uses native fs so do not use nro_add_arg_file @@ -330,9 +340,9 @@ Menu::Menu(const std::vector& nro_entries) : MenuBase{"FileBrowser"_i1 if (entry.type == FsDirEntryType_Dir) { Scan(GetNewPathCurrent()); - } else if (m_fs_type == FsType::Sd) { + } else { // special case for nro - if (entry.GetExtension() == "nro") { + if (IsSd() && entry.GetExtension() == "nro") { App::Push(std::make_shared("Launch "_i18n + entry.GetName() + '?', "No"_i18n, "Launch"_i18n, 1, [this](auto op_index){ if (op_index && *op_index) { @@ -341,7 +351,7 @@ Menu::Menu(const std::vector& nro_entries) : MenuBase{"FileBrowser"_i1 })); } else if (App::GetInstallEnable() && IsExtension(entry.GetExtension(), INSTALL_EXTENSIONS)) { InstallFiles(); - } else { + } else if (IsSd()) { const auto assoc_list = FindFileAssocFor(); if (!assoc_list.empty()) { // for (auto&e : assoc_list) { @@ -374,12 +384,12 @@ Menu::Menu(const std::vector& nro_entries) : MenuBase{"FileBrowser"_i1 std::make_pair(Button::B, Action{"Back"_i18n, [this](){ std::string_view view{m_path}; - if (view != "/") { + if (view != m_fs->Root()) { const auto end = view.find_last_of('/'); assert(end != view.npos); if (end == 0) { - Scan("/", true); + Scan(m_fs->Root(), true); } else { Scan(view.substr(0, end), true); } @@ -519,7 +529,7 @@ Menu::Menu(const std::vector& nro_entries) : MenuBase{"FileBrowser"_i1 } } - if (m_fs_type == FsType::Sd && m_entries_current.size()) { + if (IsSd() && m_entries_current.size()) { if (App::GetInstallEnable() && HasTypeInSelectedEntries(FsDirEntryType_File) && !m_selected_count && (GetEntry().GetExtension() == "nro" || !FindFileAssocFor().empty())) { options->Add(std::make_shared("Install Forwarder"_i18n, [this](){; if (App::GetInstallPrompt()) { @@ -552,7 +562,7 @@ Menu::Menu(const std::vector& nro_entries) : MenuBase{"FileBrowser"_i1 App::Push(std::make_shared("Are you sure you want to extract to root?"_i18n, "No"_i18n, "Yes"_i18n, 0, [this](auto op_index){ if (op_index && *op_index) { - UnzipFiles("/"); + UnzipFiles(m_fs->Root()); } })); })); @@ -591,11 +601,11 @@ Menu::Menu(const std::vector& nro_entries) : MenuBase{"FileBrowser"_i1 options->Add(std::make_shared("Create File"_i18n, [this](){ std::string out; - if (R_SUCCEEDED(swkbd::ShowText(out, "Set File Name"_i18n.c_str())) && !out.empty()) { + if (R_SUCCEEDED(swkbd::ShowText(out, "Set File Name"_i18n.c_str(), fs::AppendPath(m_path, ""))) && !out.empty()) { App::PopToMenu(); fs::FsPath full_path; - if (out[0] == '/') { + if (out.starts_with(m_fs_entry.root.s)) { full_path = out; } else { full_path = fs::AppendPath(m_path, out); @@ -617,7 +627,7 @@ Menu::Menu(const std::vector& nro_entries) : MenuBase{"FileBrowser"_i1 App::PopToMenu(); fs::FsPath full_path; - if (out[0] == '/') { + if (out.starts_with(m_fs_entry.root.s)) { full_path = out; } else { full_path = fs::AppendPath(m_path, out); @@ -632,13 +642,13 @@ Menu::Menu(const std::vector& nro_entries) : MenuBase{"FileBrowser"_i1 } })); - if (m_fs_type == FsType::Sd && m_entries_current.size() && !m_selected_count && GetEntry().IsFile() && GetEntry().file_size < 1024*64) { + if (IsSd() && m_entries_current.size() && !m_selected_count && GetEntry().IsFile() && GetEntry().file_size < 1024*64) { options->Add(std::make_shared("View as text (unfinished)"_i18n, [this](){ App::Push(std::make_shared(GetNewPathCurrent())); })); } - if (m_fs_type == FsType::Sd && m_entries_current.size()) { + if (m_entries_current.size()) { options->Add(std::make_shared("Upload"_i18n, [this](){ UploadFiles(); })); @@ -650,15 +660,27 @@ Menu::Menu(const std::vector& nro_entries) : MenuBase{"FileBrowser"_i1 })); SidebarEntryArray::Items mount_items; - mount_items.push_back("Sd"_i18n); - mount_items.push_back("Image System memory"_i18n); - mount_items.push_back("Image microSD card"_i18n); + std::vector fs_entries; + for (const auto& e: FS_ENTRIES) { + fs_entries.emplace_back(e); + mount_items.push_back(i18n::get(e.name)); + } - options->Add(std::make_shared("Mount"_i18n, mount_items, [this](s64& index_out){ + const auto stdio_locations = location::GetStdio(false); + for (const auto& e: stdio_locations) { + u32 flags{}; + if (e.write_protect) { + flags |= FsEntryFlag_ReadOnly; + } + + fs_entries.emplace_back(e.name, e.mount, FsType::Stdio, flags); + mount_items.push_back(e.name); + } + + options->Add(std::make_shared("Mount"_i18n, mount_items, [this, fs_entries](s64& index_out){ App::PopToMenu(); - m_mount.Set(index_out); - SetFs("/", index_out); - }, m_mount.Get())); + SetFs(fs_entries[index_out].root, fs_entries[index_out]); + }, m_fs_entry.name)); })); }}) ); @@ -668,11 +690,14 @@ Menu::Menu(const std::vector& nro_entries) : MenuBase{"FileBrowser"_i1 fs::FsPath buf; ini_gets("paths", "last_path", "/", buf, sizeof(buf), App::CONFIG_PATH); - SetFs(buf, m_mount.Get()); + SetFs(buf, FS_ENTRY_DEFAULT); } Menu::~Menu() { - ini_puts("paths", "last_path", m_path, App::CONFIG_PATH); + // don't store mount points for non-sd card paths. + if (IsSd()) { + ini_puts("paths", "last_path", m_path, App::CONFIG_PATH); + } } void Menu::Update(Controller* controller, TouchInfo* touch) { @@ -704,10 +729,10 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) { auto& e = GetEntry(i); if (e.IsDir()) { - if (e.file_count == -1 && e.dir_count == -1) { + if (m_fs->IsNative() && e.file_count == -1 && e.dir_count == -1) { const auto full_path = GetNewPath(e); - m_fs->DirGetEntryCount(full_path, FsDirOpenMode_ReadFiles | FsDirOpenMode_NoFileSize, &e.file_count); - m_fs->DirGetEntryCount(full_path, FsDirOpenMode_ReadDirs | FsDirOpenMode_NoFileSize, &e.dir_count); + GetNative()->DirGetEntryCount(full_path, FsDirOpenMode_ReadFiles | FsDirOpenMode_NoFileSize, &e.file_count); + GetNative()->DirGetEntryCount(full_path, FsDirOpenMode_ReadDirs | FsDirOpenMode_NoFileSize, &e.dir_count); } } else if (!e.checked_extension) { e.checked_extension = true; @@ -759,13 +784,17 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) { gfx::drawText(vg, x + text_xoffset+65, y + (h / 2.f), 20.f, e.name, NULL, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(text_id)); nvgRestore(vg); - if (e.IsDir()) { + if (m_fs->IsNative() && e.IsDir()) { gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) - 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_BOTTOM, theme->GetColour(text_id), "%zd files"_i18n.c_str(), e.file_count); gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) + 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_TOP, theme->GetColour(text_id), "%zd dirs"_i18n.c_str(), e.dir_count); - } else { + } else if (e.IsFile()) { if (!e.time_stamp.is_valid) { const auto path = GetNewPath(e); - m_fs->GetFileTimeStampRaw(path, &e.time_stamp); + if (m_fs->IsNative()) { + m_fs->GetFileTimeStampRaw(path, &e.time_stamp); + } else { + m_fs->FileGetSizeAndTimestamp(path, &e.time_stamp, &e.file_size); + } } const auto t = (time_t)(e.time_stamp.modified); struct tm tm{}; @@ -784,7 +813,7 @@ void Menu::OnFocusGained() { MenuBase::OnFocusGained(); if (m_entries.empty()) { if (m_path.empty()) { - Scan("/"); + Scan(m_fs->Root()); } else { Scan(m_path); } @@ -899,7 +928,11 @@ void Menu::InstallFiles() { App::Push(std::make_shared(0, "Installing "_i18n, "", [this, targets](auto pbox) -> Result { for (auto& e : targets) { - R_TRY(yati::InstallFromFile(pbox, &m_fs->m_fs, GetNewPath(e))); + if (m_fs->IsNative()) { + R_TRY(yati::InstallFromFile(pbox, &GetNative()->m_fs, GetNewPath(e))); + } else { + R_TRY(yati::InstallFromStdioFile(pbox, GetNewPath(e))); + } App::Notify("Installed " + e.GetName()); } @@ -919,7 +952,6 @@ void Menu::UnzipFiles(fs::FsPath dir_path) { App::Push(std::make_shared(0, "Extracting "_i18n, "", [this, dir_path, targets](auto pbox) -> Result { constexpr auto chunk_size = 1024 * 512; // 512KiB - auto& fs = *m_fs.get(); for (auto& e : targets) { pbox->SetTitle(e.GetName()); @@ -959,19 +991,19 @@ void Menu::UnzipFiles(fs::FsPath dir_path) { pbox->NewTransfer(name); // create directories - fs.CreateDirectoryRecursivelyWithPath(file_path); + m_fs->CreateDirectoryRecursivelyWithPath(file_path); Result rc; - if (R_FAILED(rc = fs.CreateFile(file_path, info.uncompressed_size, 0)) && rc != FsError_PathAlreadyExists) { + if (R_FAILED(rc = m_fs->CreateFile(file_path, info.uncompressed_size, 0)) && rc != FsError_PathAlreadyExists) { log_write("failed to create file: %s 0x%04X\n", file_path.s, rc); R_THROW(rc); } - FsFile f; - R_TRY(fs.OpenFile(file_path, FsOpenMode_Write, &f)); - ON_SCOPE_EXIT(fsFileClose(&f)); + fs::File f; + R_TRY(m_fs->OpenFile(file_path, FsOpenMode_Write, &f)); + ON_SCOPE_EXIT(m_fs->FileClose(&f)); - R_TRY(fsFileSetSize(&f, info.uncompressed_size)); + R_TRY(m_fs->FileSetSize(&f, info.uncompressed_size)); std::vector buf(chunk_size); s64 offset{}; @@ -984,7 +1016,7 @@ void Menu::UnzipFiles(fs::FsPath dir_path) { R_THROW(0x1); } - R_TRY(fsFileWrite(&f, offset, buf.data(), bytes_read, FsWriteOption_None)); + R_TRY(m_fs->FileWrite(&f, offset, buf.data(), bytes_read, FsWriteOption_None)); pbox->UpdateTransfer(offset, info.uncompressed_size); offset += bytes_read; @@ -1030,7 +1062,7 @@ void Menu::ZipFiles(fs::FsPath zip_out) { } zip_out = fs::AppendPath(m_path, file_path); - if (!fs::FileExists(&m_fs->m_fs, zip_out)) { + if (!m_fs->FileExists(zip_out)) { break; } } @@ -1043,7 +1075,6 @@ void Menu::ZipFiles(fs::FsPath zip_out) { App::Push(std::make_shared(0, "Compressing "_i18n, "", [this, zip_out, targets](auto pbox) -> Result { constexpr auto chunk_size = 1024 * 512; // 512KiB - auto& fs = *m_fs.get(); const auto t = std::time(NULL); const auto tm = std::localtime(&t); @@ -1080,12 +1111,12 @@ void Menu::ZipFiles(fs::FsPath zip_out) { } ON_SCOPE_EXIT(zipCloseFileInZip(zfile)); - FsFile f; - R_TRY(fs.OpenFile(file_path, FsOpenMode_Read, &f)); - ON_SCOPE_EXIT(fsFileClose(&f)); + fs::File f; + R_TRY(m_fs->OpenFile(file_path, FsOpenMode_Read, &f)); + ON_SCOPE_EXIT(m_fs->FileClose(&f)); s64 file_size; - R_TRY(fsFileGetSize(&f, &file_size)); + R_TRY(m_fs->FileGetSize(&f, &file_size)); std::vector buf(chunk_size); s64 offset{}; @@ -1093,7 +1124,7 @@ void Menu::ZipFiles(fs::FsPath zip_out) { R_TRY(pbox->ShouldExitResult()); u64 bytes_read; - R_TRY(fsFileRead(&f, offset, buf.data(), buf.size(), FsReadOption_None, &bytes_read)); + R_TRY(m_fs->FileRead(&f, offset, buf.data(), buf.size(), FsReadOption_None, &bytes_read)); if (ZIP_OK != zipWriteInFileInZip(zfile, buf.data(), bytes_read)) { log_write("failed to write zip file: %s\n", file_path.s); @@ -1168,13 +1199,13 @@ void Menu::UploadFiles() { pbox->SetTitle(name); pbox->NewTransfer(relative_file_name); - FsFile file; - R_TRY(m_fs->OpenFile(file_path, FsOpenMode_Read, &file)); - ON_SCOPE_EXIT(fsFileClose(&file)); + fs::File f; + R_TRY(m_fs->OpenFile(file_path, FsOpenMode_Read, &f)); + ON_SCOPE_EXIT(m_fs->FileClose(&f)); return thread::TransferPull(pbox, file_size, [&](void* data, s64 off, s64 size, u64* bytes_read) -> Result { - return fsFileRead(&file, off, data, size, FsReadOption_None, bytes_read); + return m_fs->FileRead(&f, off, data, size, FsReadOption_None, bytes_read); }, [&](thread::PullCallback pull) -> Result { s64 offset{}; @@ -1256,20 +1287,16 @@ auto Menu::Scan(const fs::FsPath& new_path, bool is_walk_up) -> Result { ResetSelection(); } - FsDir d; + fs::Dir d; R_TRY(m_fs->OpenDirectory(new_path, FsDirOpenMode_ReadDirs | FsDirOpenMode_ReadFiles, &d)); - ON_SCOPE_EXIT(fsDirClose(&d)); - - s64 count; - R_TRY(m_fs->DirGetEntryCount(&d, &count)); + ON_SCOPE_EXIT(m_fs->DirClose(&d)); // we won't run out of memory here (tm) - std::vector dir_entries(count); + std::vector dir_entries; - R_TRY(m_fs->DirRead(&d, &count, dir_entries.size(), dir_entries.data())); + R_TRY(m_fs->DirReadAll(&d, dir_entries)); - // size may of changed - dir_entries.resize(count); + const auto count = dir_entries.size(); m_entries.reserve(count); m_entries_index.clear(); @@ -1581,9 +1608,9 @@ void Menu::OnDeleteCallback() { const auto full_path = GetNewPath(m_selected_path, entry.name); if (entry.IsDir()) { - s64 count{}; - m_fs->DirGetEntryCount(full_path, FsDirOpenMode_ReadDirs | FsDirOpenMode_ReadFiles | FsDirOpenMode_NoFileSize, &count); - if (!count) { + bool empty{}; + m_fs->IsDirEmpty(full_path, &empty); + if (empty) { m_fs->DeleteDirectory(full_path); use_progress_box = false; } @@ -1726,7 +1753,7 @@ void Menu::OnPasteCallback() { m_fs->CreateDirectory(dst_path); } else { pbox->NewTransfer("Copying "_i18n + src_path); - R_TRY(pbox->CopyFile(src_path, dst_path)); + R_TRY(pbox->CopyFile(m_fs.get(), src_path, dst_path)); } } @@ -1755,7 +1782,7 @@ void Menu::OnPasteCallback() { pbox->NewTransfer("Copying "_i18n + src_path); log_write("copying: %s to %s\n", src_path.s, dst_path.s); - R_TRY(pbox->CopyFile(src_path, dst_path)); + R_TRY(pbox->CopyFile(m_fs.get(), src_path, dst_path)); } } } @@ -1776,7 +1803,8 @@ void Menu::OnRenameCallback() { } auto Menu::CheckIfUpdateFolder() -> Result { - R_UNLESS(m_fs_type == FsType::Sd, FsError_InvalidMountName); + R_UNLESS(IsSd(), FsError_InvalidMountName); + R_UNLESS(m_fs->IsNative(), 0x1); // check if we have already tried to find daybreak if (m_daybreak_path.has_value() && m_daybreak_path.value().empty()) { @@ -1799,12 +1827,8 @@ auto Menu::CheckIfUpdateFolder() -> Result { log_write("found daybreak in: %s\n", m_daybreak_path.value().s); } - FsDir d; - R_TRY(m_fs->OpenDirectory(m_path, FsDirOpenMode_ReadDirs, &d)); - ON_SCOPE_EXIT(m_fs->DirClose(&d)); - s64 count; - R_TRY(m_fs->DirGetEntryCount(&d, &count)); + R_TRY(GetNative()->DirGetEntryCount(m_path, FsDirOpenMode_ReadDirs, &count)); // check that we are at the bottom level R_UNLESS(count == 0, 0x1); @@ -1826,15 +1850,10 @@ auto Menu::get_collection(const fs::FsPath& path, const fs::FsPath& parent_name, out.parent_name = parent_name; const auto fetch = [this, &path](std::vector& out, u32 flags) -> Result { - FsDir d; + fs::Dir d; R_TRY(m_fs->OpenDirectory(path, flags, &d)); ON_SCOPE_EXIT(m_fs->DirClose(&d)); - - s64 count; - R_TRY(m_fs->DirGetEntryCount(&d, &count)); - - out.resize(count); - return m_fs->DirRead(&d, &count, out.size(), out.data()); + return m_fs->DirReadAll(&d, out); }; if (inc_file) { @@ -1871,9 +1890,9 @@ auto Menu::get_collections(const fs::FsPath& path, const fs::FsPath& parent_name R_SUCCEED(); } -void Menu::SetFs(const fs::FsPath& new_path, u32 _new_type) { - const auto new_type = static_cast(_new_type); - if (m_fs && new_type == m_fs_type) { +void Menu::SetFs(const fs::FsPath& new_path, const FsEntry& new_entry) { + if (m_fs && m_fs_entry.root == new_entry.root && m_fs_entry.type == new_entry.type) { + log_write("same fs, ignoring\n"); return; } @@ -1888,28 +1907,26 @@ void Menu::SetFs(const fs::FsPath& new_path, u32 _new_type) { m_selected_path.clear(); m_selected_count = 0; m_selected_type = SelectedType::None; + m_fs_entry = new_entry; - switch (new_type) { - default: case FsType::Sd: + switch (new_entry.type) { + case FsType::Sd: m_fs = std::make_unique(m_ignore_read_only.Get()); - m_fs_type = FsType::Sd; - log_write("doing fs: %u\n", _new_type); break; case FsType::ImageNand: m_fs = std::make_unique(FsImageDirectoryId_Nand); - m_fs_type = FsType::ImageNand; - log_write("doing image nand\n"); break; case FsType::ImageSd: m_fs = std::make_unique(FsImageDirectoryId_Sd); - m_fs_type = FsType::ImageSd; - log_write("doing image sd\n"); + break; + case FsType::Stdio: + m_fs = std::make_unique(true, new_entry.root); break; } if (HasFocus()) { if (m_path.empty()) { - Scan("/"); + Scan(m_fs->Root()); } else { Scan(m_path); } diff --git a/sphaira/source/ui/progress_box.cpp b/sphaira/source/ui/progress_box.cpp index 0e875d4..74afb71 100644 --- a/sphaira/source/ui/progress_box.cpp +++ b/sphaira/source/ui/progress_box.cpp @@ -219,41 +219,36 @@ auto ProgressBox::ShouldExitResult() -> Result { R_SUCCEED(); } -auto ProgressBox::CopyFile(const fs::FsPath& src_path, const fs::FsPath& dst_path) -> Result { - fs::FsNativeSd fs; - R_TRY(fs.GetFsOpenResult()); - - FsFile src_file; - R_TRY(fs.OpenFile(src_path, FsOpenMode_Read, &src_file)); - ON_SCOPE_EXIT(fsFileClose(&src_file)); +auto ProgressBox::CopyFile(fs::Fs* fs, const fs::FsPath& src_path, const fs::FsPath& dst_path) -> Result { + fs::File src_file; + R_TRY(fs->OpenFile(src_path, FsOpenMode_Read, &src_file)); + ON_SCOPE_EXIT(fs->FileClose(&src_file)); s64 src_size; - R_TRY(fsFileGetSize(&src_file, &src_size)); + R_TRY(fs->FileGetSize(&src_file, &src_size)); // this can fail if it already exists so we ignore the result. // if the file actually failed to be created, the result is implicitly // handled when we try and open it for writing. - fs.CreateFile(dst_path, src_size, 0); + fs->CreateFile(dst_path, src_size, 0); - FsFile dst_file; - R_TRY(fs.OpenFile(dst_path, FsOpenMode_Write, &dst_file)); - ON_SCOPE_EXIT(fsFileClose(&dst_file)); + fs::File dst_file; + R_TRY(fs->OpenFile(dst_path, FsOpenMode_Write, &dst_file)); + ON_SCOPE_EXIT(fs->FileClose(&dst_file)); - R_TRY(fsFileSetSize(&dst_file, src_size)); + R_TRY(fs->FileSetSize(&dst_file, src_size)); s64 offset{}; std::vector buf(1024*1024*4); // 4MiB while (offset < src_size) { - if (ShouldExit()) { - R_THROW(0xFFFF); - } + R_TRY(ShouldExitResult()); u64 bytes_read; - R_TRY(fsFileRead(&src_file, offset, buf.data(), buf.size(), 0, &bytes_read)); + R_TRY(fs->FileRead(&src_file, offset, buf.data(), buf.size(), 0, &bytes_read)); Yield(); - R_TRY(fsFileWrite(&dst_file, offset, buf.data(), bytes_read, FsWriteOption_None)); + R_TRY(fs->FileWrite(&dst_file, offset, buf.data(), bytes_read, FsWriteOption_None)); Yield(); UpdateTransfer(offset, src_size); @@ -263,6 +258,12 @@ auto ProgressBox::CopyFile(const fs::FsPath& src_path, const fs::FsPath& dst_pat R_SUCCEED(); } +auto ProgressBox::CopyFile(const fs::FsPath& src_path, const fs::FsPath& dst_path) -> Result { + fs::FsNativeSd fs; + R_TRY(fs.GetFsOpenResult()); + return CopyFile(&fs, src_path, dst_path); +} + void ProgressBox::Yield() { svcSleepThread(YieldType_WithoutCoreMigration); }