add support for backup/restore save to usb
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "fs.hpp"
|
||||
#include "location.hpp"
|
||||
#include <switch.h>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
@@ -30,6 +31,17 @@ enum DumpLocationFlag {
|
||||
DumpLocationFlag_All = DumpLocationFlag_SdCard | DumpLocationFlag_UsbS2S | DumpLocationFlag_DevNull | DumpLocationFlag_Stdio | DumpLocationFlag_Network,
|
||||
};
|
||||
|
||||
struct DumpEntry {
|
||||
DumpLocationType type;
|
||||
s32 index;
|
||||
};
|
||||
|
||||
struct DumpLocation {
|
||||
DumpEntry entry{};
|
||||
location::Entries network{};
|
||||
location::StdioEntries stdio{};
|
||||
};
|
||||
|
||||
struct BaseSource {
|
||||
virtual ~BaseSource() = default;
|
||||
virtual Result Read(const std::string& path, void* buf, s64 off, s64 size, u64* bytes_read) = 0;
|
||||
@@ -40,7 +52,13 @@ struct BaseSource {
|
||||
|
||||
// called after dump has finished.
|
||||
using OnExit = std::function<void(Result rc)>;
|
||||
using OnLocation = std::function<void(const DumpLocation& loc)>;
|
||||
|
||||
// prompts the user to select dump location, calls on_loc on success with the selected location.
|
||||
void DumpGetLocation(const std::string& title, u32 location_flags, OnLocation on_loc);
|
||||
// dumps to a fetched location using DumpGetLocation().
|
||||
void Dump(std::shared_ptr<BaseSource> source, const DumpLocation& location, const std::vector<fs::FsPath>& paths, OnExit on_exit);
|
||||
// DumpGetLocation() + Dump() all in one.
|
||||
void Dump(std::shared_ptr<BaseSource> source, const std::vector<fs::FsPath>& paths, OnExit on_exit = [](Result){}, u32 location_flags = DumpLocationFlag_All);
|
||||
|
||||
} // namespace sphaira::dump
|
||||
|
||||
@@ -23,17 +23,12 @@
|
||||
namespace sphaira::dump {
|
||||
namespace {
|
||||
|
||||
struct DumpEntry {
|
||||
DumpLocationType type;
|
||||
s32 index;
|
||||
};
|
||||
|
||||
struct DumpLocation {
|
||||
struct DumpLocationEntry {
|
||||
const DumpLocationType type;
|
||||
const char* name;
|
||||
};
|
||||
|
||||
constexpr DumpLocation DUMP_LOCATIONS[]{
|
||||
constexpr DumpLocationEntry DUMP_LOCATIONS[]{
|
||||
{ DumpLocationType_SdCard, "microSD card (/dumps/)" },
|
||||
{ DumpLocationType_UsbS2S, "USB transfer (Switch 2 Switch)" },
|
||||
{ DumpLocationType_DevNull, "/dev/null (Speed Test)" },
|
||||
@@ -319,23 +314,24 @@ Result DumpToNetwork(ui::ProgressBox* pbox, const location::Entry& loc, BaseSour
|
||||
|
||||
} // namespace
|
||||
|
||||
void Dump(std::shared_ptr<BaseSource> source, const std::vector<fs::FsPath>& paths, OnExit on_exit, u32 location_flags) {
|
||||
void DumpGetLocation(const std::string& title, u32 location_flags, OnLocation on_loc) {
|
||||
DumpLocation out;
|
||||
ui::PopupList::Items items;
|
||||
std::vector<DumpEntry> dump_entries;
|
||||
|
||||
const auto network_locations = location::Load();
|
||||
out.network = location::Load();
|
||||
if (location_flags & (1 << DumpLocationType_Network)) {
|
||||
for (s32 i = 0; i < std::size(network_locations); i++) {
|
||||
for (s32 i = 0; i < std::size(out.network); i++) {
|
||||
dump_entries.emplace_back(DumpLocationType_Network, i);
|
||||
items.emplace_back(network_locations[i].name);
|
||||
items.emplace_back(out.network[i].name);
|
||||
}
|
||||
}
|
||||
|
||||
const auto stdio_locations = location::GetStdio(true);
|
||||
out.stdio = location::GetStdio(true);
|
||||
if (location_flags & (1 << DumpLocationType_Stdio)) {
|
||||
for (s32 i = 0; i < std::size(stdio_locations); i++) {
|
||||
for (s32 i = 0; i < std::size(out.stdio); i++) {
|
||||
dump_entries.emplace_back(DumpLocationType_Stdio, i);
|
||||
items.emplace_back(stdio_locations[i].name);
|
||||
items.emplace_back(out.stdio[i].name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,40 +343,44 @@ void Dump(std::shared_ptr<BaseSource> source, const std::vector<fs::FsPath>& pat
|
||||
}
|
||||
|
||||
App::Push(std::make_shared<ui::PopupList>(
|
||||
"Select dump location"_i18n, items, [source, paths, on_exit, network_locations, stdio_locations, dump_entries](auto op_index){
|
||||
if (!op_index) {
|
||||
on_exit(0xFFFF);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto dump_entry = dump_entries[*op_index];
|
||||
|
||||
App::Push(std::make_shared<ui::ProgressBox>(0, "Dumping"_i18n, "", [source, paths, network_locations, stdio_locations, dump_entry](auto pbox) -> Result {
|
||||
if (dump_entry.type == DumpLocationType_Network) {
|
||||
R_TRY(DumpToNetwork(pbox, network_locations[dump_entry.index], source.get(), paths));
|
||||
} else if (dump_entry.type == DumpLocationType_Stdio) {
|
||||
R_TRY(DumpToStdio(pbox, stdio_locations[dump_entry.index], source.get(), paths));
|
||||
} else if (dump_entry.type == DumpLocationType_SdCard) {
|
||||
R_TRY(DumpToFileNative(pbox, source.get(), paths));
|
||||
} else if (dump_entry.type == DumpLocationType_UsbS2S) {
|
||||
R_TRY(DumpToUsbS2S(pbox, source.get(), paths));
|
||||
} else if (dump_entry.type == DumpLocationType_DevNull) {
|
||||
R_TRY(DumpToDevNull(pbox, source.get(), paths));
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}, [on_exit](Result rc){
|
||||
App::PushErrorBox(rc, "Dump failed!"_i18n);
|
||||
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
App::Notify("Dump successfull!"_i18n);
|
||||
log_write("dump successfull!!!\n");
|
||||
}
|
||||
|
||||
on_exit(rc);
|
||||
}));
|
||||
title, items, [dump_entries, out, on_loc](auto op_index) mutable {
|
||||
out.entry = dump_entries[*op_index];
|
||||
on_loc(out);
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
void Dump(std::shared_ptr<BaseSource> source, const DumpLocation& location, const std::vector<fs::FsPath>& paths, OnExit on_exit) {
|
||||
App::Push(std::make_shared<ui::ProgressBox>(0, "Dumping"_i18n, "", [source, paths, location](auto pbox) -> Result {
|
||||
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) {
|
||||
R_TRY(DumpToStdio(pbox, location.stdio[location.entry.index], source.get(), paths));
|
||||
} else if (location.entry.type == DumpLocationType_SdCard) {
|
||||
R_TRY(DumpToFileNative(pbox, source.get(), paths));
|
||||
} else if (location.entry.type == DumpLocationType_UsbS2S) {
|
||||
R_TRY(DumpToUsbS2S(pbox, source.get(), paths));
|
||||
} else if (location.entry.type == DumpLocationType_DevNull) {
|
||||
R_TRY(DumpToDevNull(pbox, source.get(), paths));
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}, [on_exit](Result rc){
|
||||
App::PushErrorBox(rc, "Dump failed!"_i18n);
|
||||
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
App::Notify("Dump successfull!"_i18n);
|
||||
log_write("dump successfull!!!\n");
|
||||
}
|
||||
|
||||
on_exit(rc);
|
||||
}));
|
||||
}
|
||||
|
||||
void Dump(std::shared_ptr<BaseSource> source, const std::vector<fs::FsPath>& paths, OnExit on_exit, u32 location_flags) {
|
||||
DumpGetLocation("Select dump location"_i18n, location_flags, [source, paths, on_exit](const DumpLocation& loc){
|
||||
Dump(source, loc, paths, on_exit);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace sphaira::dump
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "image.hpp"
|
||||
#include "threaded_file_transfer.hpp"
|
||||
#include "minizip_helper.hpp"
|
||||
#include "dumper.hpp"
|
||||
|
||||
#include "ui/menus/save_menu.hpp"
|
||||
#include "ui/menus/filebrowser.hpp"
|
||||
@@ -504,7 +505,16 @@ Result RestoreSaveInternal(ProgressBox* pbox, const Entry& e, const fs::FsPath&
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result BackupSaveInternal(ProgressBox* pbox, const AccountEntry& acc, const Entry& e, bool compressed, bool is_auto = false) {
|
||||
Result BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& location, const AccountEntry& acc, const Entry& e, bool compressed, bool is_auto = false) {
|
||||
std::unique_ptr<fs::Fs> fs;
|
||||
if (location.entry.type == dump::DumpLocationType_Stdio) {
|
||||
fs = std::make_unique<fs::FsStdio>(true, location.stdio[location.entry.index].mount);
|
||||
} else if (location.entry.type == dump::DumpLocationType_SdCard) {
|
||||
fs = std::make_unique<fs::FsNativeSd>();
|
||||
} else {
|
||||
R_THROW(0x1);
|
||||
}
|
||||
|
||||
pbox->SetTitle(e.GetName());
|
||||
if (e.image) {
|
||||
pbox->SetImage(e.image);
|
||||
@@ -539,10 +549,6 @@ Result BackupSaveInternal(ProgressBox* pbox, const AccountEntry& acc, const Entr
|
||||
// the save file may be empty, this isn't an error, but we exit early.
|
||||
R_UNLESS(!collections.empty(), 0x0);
|
||||
|
||||
// we will actually store this to the dump locations, eventually.
|
||||
fs::FsNativeSd fs;
|
||||
R_TRY(fs.GetFsOpenResult());
|
||||
|
||||
const auto t = std::time(NULL);
|
||||
const auto tm = std::localtime(&t);
|
||||
|
||||
@@ -555,11 +561,11 @@ Result BackupSaveInternal(ProgressBox* pbox, const AccountEntry& acc, const Entr
|
||||
zip_info_default.tmz_date.tm_mon = tm->tm_mon;
|
||||
zip_info_default.tmz_date.tm_year = tm->tm_year;
|
||||
|
||||
const auto path = BuildSavePath(acc, e, is_auto);
|
||||
const auto path = fs::AppendPath(fs->Root(), BuildSavePath(acc, e, is_auto));
|
||||
const auto temp_path = path + ".temp";
|
||||
|
||||
fs.CreateDirectoryRecursivelyWithPath(temp_path);
|
||||
ON_SCOPE_EXIT(fs.DeleteFile(temp_path));
|
||||
fs->CreateDirectoryRecursivelyWithPath(temp_path);
|
||||
ON_SCOPE_EXIT(fs->DeleteFile(temp_path));
|
||||
|
||||
// zip to memory if less than 1GB and not applet mode.
|
||||
// TODO: use my mmz code from ftpsrv to stream zip creation.
|
||||
@@ -606,9 +612,9 @@ Result BackupSaveInternal(ProgressBox* pbox, const AccountEntry& acc, const Entr
|
||||
|
||||
// try and load the actual timestamp of the file.
|
||||
// TODO: not supported for saves...
|
||||
#if 0
|
||||
#if 1
|
||||
FsTimeStampRaw timestamp{};
|
||||
if (R_SUCCEEDED(fs.GetFileTimeStampRaw(file_path, ×tamp)) && timestamp.is_valid) {
|
||||
if (R_SUCCEEDED(save_fs.GetFileTimeStampRaw(file_path, ×tamp)) && timestamp.is_valid) {
|
||||
const auto time = (time_t)timestamp.modified;
|
||||
if (auto tm = localtime(&time)) {
|
||||
zip_info.tmz_date.tm_sec = tm->tm_sec;
|
||||
@@ -660,10 +666,10 @@ Result BackupSaveInternal(ProgressBox* pbox, const AccountEntry& acc, const Entr
|
||||
const auto is_file_based_emummc = App::IsFileBaseEmummc();
|
||||
if (!file_download) {
|
||||
pbox->NewTransfer("Flushing zip to file");
|
||||
R_TRY(fs.CreateFile(temp_path, mz_mem.buf.size(), 0));
|
||||
R_TRY(fs->CreateFile(temp_path, mz_mem.buf.size(), 0));
|
||||
|
||||
fs::File file;
|
||||
R_TRY(fs.OpenFile(temp_path, FsOpenMode_Write, &file));
|
||||
R_TRY(fs->OpenFile(temp_path, FsOpenMode_Write, &file));
|
||||
|
||||
R_TRY(thread::Transfer(pbox, mz_mem.buf.size(),
|
||||
[&](void* data, s64 off, s64 size, u64* bytes_read) -> Result {
|
||||
@@ -682,10 +688,9 @@ Result BackupSaveInternal(ProgressBox* pbox, const AccountEntry& acc, const Entr
|
||||
));
|
||||
}
|
||||
|
||||
fs.DeleteFile(path);
|
||||
R_TRY(fs.RenameFile(temp_path, path));
|
||||
fs->DeleteFile(path);
|
||||
R_TRY(fs->RenameFile(temp_path, path));
|
||||
|
||||
App::Notify("Backed up to "_i18n + path.toString());
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
@@ -1174,101 +1179,98 @@ void Menu::OnLayoutChange() {
|
||||
}
|
||||
|
||||
void Menu::BackupSaves(std::vector<std::reference_wrapper<Entry>>& entries) {
|
||||
int image = 0;
|
||||
if (entries.size() == 1) {
|
||||
image = entries[0].get().image;
|
||||
}
|
||||
|
||||
App::Push(std::make_shared<OptionBox>(
|
||||
"Are you sure you want to backup save(s)?"_i18n,
|
||||
"Back"_i18n, "Backup"_i18n, 0, [this, entries](auto op_index){
|
||||
if (op_index && *op_index) {
|
||||
App::Push(std::make_shared<ProgressBox>(0, "Backup"_i18n, "", [this, entries](auto pbox) -> Result {
|
||||
for (auto& e : entries) {
|
||||
// the entry may not have loaded yet.
|
||||
LoadControlEntry(e);
|
||||
R_TRY(BackupSaveInternal(pbox, m_accounts[m_account_index], e, m_compress_save_backup.Get()));
|
||||
}
|
||||
R_SUCCEED();
|
||||
}, [](Result rc){
|
||||
App::PushErrorBox(rc, "Backup failed!"_i18n);
|
||||
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
App::Notify("Backup successfull!"_i18n);
|
||||
}
|
||||
}));
|
||||
dump::DumpGetLocation("Select backup location"_i18n, dump::DumpLocationFlag_SdCard|dump::DumpLocationFlag_Stdio, [this, entries](const dump::DumpLocation& location){
|
||||
App::Push(std::make_shared<ProgressBox>(0, "Backup"_i18n, "", [this, entries, location](auto pbox) -> Result {
|
||||
for (auto& e : entries) {
|
||||
// the entry may not have loaded yet.
|
||||
LoadControlEntry(e);
|
||||
R_TRY(BackupSaveInternal(pbox, location, m_accounts[m_account_index], e, m_compress_save_backup.Get()));
|
||||
}
|
||||
}, image
|
||||
));
|
||||
R_SUCCEED();
|
||||
}, [](Result rc){
|
||||
App::PushErrorBox(rc, "Backup failed!"_i18n);
|
||||
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
App::Notify("Backup successfull!"_i18n);
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
void Menu::RestoreSave() {
|
||||
const auto save_path = BuildSaveBasePath(m_entries[m_index]);
|
||||
|
||||
fs::FsNativeSd fs;
|
||||
filebrowser::FsDirCollection collection;
|
||||
filebrowser::FsView::get_collection(&fs, save_path, "", collection, true, false, false);
|
||||
|
||||
// reverse as they will be sorted in oldest -> newest.
|
||||
std::ranges::reverse(collection.files);
|
||||
|
||||
std::vector<fs::FsPath> paths;
|
||||
PopupList::Items items;
|
||||
for (const auto&p : collection.files) {
|
||||
const auto view = std::string_view{p.name};
|
||||
if (view.starts_with("BCAT") || !view.ends_with(".zip")) {
|
||||
continue;
|
||||
dump::DumpGetLocation("Select restore location"_i18n, dump::DumpLocationFlag_SdCard|dump::DumpLocationFlag_Stdio, [this](const dump::DumpLocation& location){
|
||||
std::unique_ptr<fs::Fs> fs;
|
||||
if (location.entry.type == dump::DumpLocationType_Stdio) {
|
||||
fs = std::make_unique<fs::FsStdio>(true, location.stdio[location.entry.index].mount);
|
||||
} else if (location.entry.type == dump::DumpLocationType_SdCard) {
|
||||
fs = std::make_unique<fs::FsNativeSd>();
|
||||
}
|
||||
|
||||
items.emplace_back(p.name);
|
||||
paths.emplace_back(fs::AppendPath(collection.path, p.name));
|
||||
}
|
||||
const auto save_path = fs::AppendPath(fs->Root(), BuildSaveBasePath(m_entries[m_index]));
|
||||
filebrowser::FsDirCollection collection;
|
||||
filebrowser::FsView::get_collection(fs.get(), save_path, "", collection, true, false, false);
|
||||
|
||||
if (paths.empty()) {
|
||||
App::Push(std::make_shared<ui::OptionBox>(
|
||||
"No saves found in "_i18n + save_path.toString(),
|
||||
"OK"_i18n
|
||||
));
|
||||
return;
|
||||
}
|
||||
// reverse as they will be sorted in oldest -> newest.
|
||||
std::ranges::reverse(collection.files);
|
||||
|
||||
const auto title = "Restore save for: "_i18n + m_entries[m_index].GetName();
|
||||
App::Push(std::make_shared<PopupList>(
|
||||
title, items, [this, paths, items](auto op_index){
|
||||
if (!op_index) {
|
||||
return;
|
||||
std::vector<fs::FsPath> paths;
|
||||
PopupList::Items items;
|
||||
for (const auto&p : collection.files) {
|
||||
const auto view = std::string_view{p.name};
|
||||
if (view.starts_with("BCAT") || !view.ends_with(".zip")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto file_name = items[*op_index];
|
||||
const auto file_path = paths[*op_index];
|
||||
|
||||
App::Push(std::make_shared<OptionBox>(
|
||||
"Are you sure you want to restore "_i18n + file_name + "?",
|
||||
"Back"_i18n, "Restore"_i18n, 0, [this, file_path](auto op_index){
|
||||
if (op_index && *op_index) {
|
||||
App::Push(std::make_shared<ProgressBox>(0, "Restore"_i18n, "", [this, file_path](auto pbox) -> Result {
|
||||
// the entry may not have loaded yet.
|
||||
LoadControlEntry(m_entries[m_index]);
|
||||
|
||||
if (m_auto_backup_on_restore.Get()) {
|
||||
pbox->SetActionName("Auto backup"_i18n);
|
||||
R_TRY(BackupSaveInternal(pbox, m_accounts[m_account_index], m_entries[m_index], m_compress_save_backup.Get(), true));
|
||||
}
|
||||
|
||||
pbox->SetActionName("Restore"_i18n);
|
||||
return RestoreSaveInternal(pbox, m_entries[m_index], file_path);
|
||||
}, [this](Result rc){
|
||||
App::PushErrorBox(rc, "Restore failed!"_i18n);
|
||||
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
App::Notify("Restore successfull!"_i18n);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}, m_entries[m_index].image
|
||||
));
|
||||
items.emplace_back(p.name);
|
||||
paths.emplace_back(fs::AppendPath(collection.path, p.name));
|
||||
}
|
||||
));
|
||||
|
||||
if (paths.empty()) {
|
||||
App::Push(std::make_shared<ui::OptionBox>(
|
||||
"No saves found in "_i18n + save_path.toString(),
|
||||
"OK"_i18n
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto title = "Restore save for: "_i18n + m_entries[m_index].GetName();
|
||||
App::Push(std::make_shared<PopupList>(
|
||||
title, items, [this, paths, items, location](auto op_index){
|
||||
if (!op_index) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto file_name = items[*op_index];
|
||||
const auto file_path = paths[*op_index];
|
||||
|
||||
App::Push(std::make_shared<OptionBox>(
|
||||
"Are you sure you want to restore "_i18n + file_name + "?",
|
||||
"Back"_i18n, "Restore"_i18n, 0, [this, file_path, location](auto op_index){
|
||||
if (op_index && *op_index) {
|
||||
App::Push(std::make_shared<ProgressBox>(0, "Restore"_i18n, "", [this, file_path, location](auto pbox) -> Result {
|
||||
// the entry may not have loaded yet.
|
||||
LoadControlEntry(m_entries[m_index]);
|
||||
|
||||
if (m_auto_backup_on_restore.Get()) {
|
||||
pbox->SetActionName("Auto backup"_i18n);
|
||||
R_TRY(BackupSaveInternal(pbox, location, m_accounts[m_account_index], m_entries[m_index], m_compress_save_backup.Get(), true));
|
||||
}
|
||||
|
||||
pbox->SetActionName("Restore"_i18n);
|
||||
return RestoreSaveInternal(pbox, m_entries[m_index], file_path);
|
||||
}, [this](Result rc){
|
||||
App::PushErrorBox(rc, "Restore failed!"_i18n);
|
||||
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
App::Notify("Restore successfull!"_i18n);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}, m_entries[m_index].image
|
||||
));
|
||||
}
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui::menu::save
|
||||
|
||||
Reference in New Issue
Block a user