mirror of
https://git.eden-emu.dev/archive/citron
synced 2026-03-23 09:59:39 -04:00
Merge pull request 'fs(feat): Add Backup Saves for Custom Save Paths' (#73) from fs/custom-save-path-backup into main
Reviewed-on: https://git.citron-emu.org/Citron/Emulator/pulls/73
This commit is contained in:
@@ -71,6 +71,7 @@ void ConfigureFilesystem::SetConfiguration() {
|
||||
ui->dump_nso->setChecked(Settings::values.dump_nso.GetValue());
|
||||
ui->cache_game_list->setChecked(UISettings::values.cache_game_list.GetValue());
|
||||
ui->prompt_for_autoloader->setChecked(UISettings::values.prompt_for_autoloader.GetValue());
|
||||
ui->backup_saves_to_nand->setChecked(Settings::values.backup_saves_to_nand.GetValue());
|
||||
|
||||
// NCA Scanning Toggle
|
||||
ui->scan_nca->setChecked(UISettings::values.scan_nca.GetValue());
|
||||
@@ -102,6 +103,7 @@ void ConfigureFilesystem::ApplyConfiguration() {
|
||||
Settings::values.dump_nso = ui->dump_nso->isChecked();
|
||||
UISettings::values.cache_game_list = ui->cache_game_list->isChecked();
|
||||
UISettings::values.prompt_for_autoloader = ui->prompt_for_autoloader->isChecked();
|
||||
Settings::values.backup_saves_to_nand.SetValue(ui->backup_saves_to_nand->isChecked());
|
||||
|
||||
// NCA Scanning Toggle
|
||||
UISettings::values.scan_nca = ui->scan_nca->isChecked();
|
||||
|
||||
@@ -87,6 +87,7 @@
|
||||
<item row="0" column="0"><widget class="QCheckBox" name="cache_game_list"><property name="text"><string>Cache Game List Metadata</string></property></widget></item>
|
||||
<item row="0" column="1"><widget class="QPushButton" name="reset_game_list_cache"><property name="text"><string>Reset Metadata Cache</string></property></widget></item>
|
||||
<item row="1" column="0" colspan="2"><widget class="QCheckBox" name="scan_nca"><property name="text"><string>Scan for .nca files (Advanced: Significantly slows down scanning)</string></property></widget></item>
|
||||
<item row="2" column="0" colspan="2"><widget class="QCheckBox" name="backup_saves_to_nand"><property name="text"><string>Allow Backup Saves to NAND if using Custom Save Path</string></property></widget></item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
|
||||
@@ -684,8 +684,9 @@ struct Values {
|
||||
// Key: build_id (hex string), Value: set of disabled cheat names
|
||||
std::map<std::string, std::set<std::string>> disabled_cheats;
|
||||
|
||||
// Custom Save Paths
|
||||
// Custom Save Paths (with backups)
|
||||
std::map<u64, std::string> custom_save_paths;
|
||||
Setting<bool> backup_saves_to_nand{linkage, false, "backup_saves_to_nand", Category::DataStorage};
|
||||
};
|
||||
|
||||
extern Values values;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#include "common/logging/log.h"
|
||||
#include "common/settings.h"
|
||||
#include "core/file_sys/errors.h"
|
||||
#include "core/file_sys/directory_save_data_filesystem.h"
|
||||
|
||||
@@ -16,8 +17,9 @@ constexpr int RetryWaitTimeMs = 100;
|
||||
|
||||
} // Anonymous namespace
|
||||
|
||||
DirectorySaveDataFileSystem::DirectorySaveDataFileSystem(VirtualDir base_filesystem)
|
||||
: base_fs(std::move(base_filesystem)), extra_data_accessor(base_fs), journaling_enabled(true),
|
||||
DirectorySaveDataFileSystem::DirectorySaveDataFileSystem(VirtualDir base_filesystem, VirtualDir backup_filesystem)
|
||||
: base_fs(std::move(base_filesystem)), backup_fs(std::move(backup_filesystem)),
|
||||
extra_data_accessor(base_fs), journaling_enabled(true),
|
||||
open_writable_files(0) {}
|
||||
|
||||
DirectorySaveDataFileSystem::~DirectorySaveDataFileSystem() = default;
|
||||
@@ -126,6 +128,25 @@ Result DirectorySaveDataFileSystem::Commit() {
|
||||
// Update cached committed_dir reference
|
||||
committed_dir = base_fs->GetSubdirectory(CommittedDirectoryName);
|
||||
|
||||
if (Settings::values.backup_saves_to_nand.GetValue() && backup_fs != nullptr) {
|
||||
LOG_INFO(Service_FS, "Dual-Save: Backing up custom save to NAND...");
|
||||
|
||||
// 1. Find or Create the '0' (Committed) folder in the NAND
|
||||
auto nand_committed = backup_fs->GetSubdirectory(CommittedDirectoryName);
|
||||
if (nand_committed == nullptr) {
|
||||
nand_committed = backup_fs->CreateSubdirectory(CommittedDirectoryName);
|
||||
}
|
||||
|
||||
if (nand_committed != nullptr) {
|
||||
// 2. Wipe whatever old backup was there
|
||||
backup_fs->DeleteSubdirectoryRecursive(CommittedDirectoryName);
|
||||
nand_committed = backup_fs->CreateSubdirectory(CommittedDirectoryName);
|
||||
|
||||
// 3. Copy the fresh data from our 'working' area to the NAND '0' folder
|
||||
CopyDirectoryRecursively(nand_committed, working_dir);
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO(Service_FS, "Save data committed successfully");
|
||||
return ResultSuccess;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,8 @@ namespace FileSys {
|
||||
/// Uses /0 (committed) and /1 (working) directories for journaling
|
||||
class DirectorySaveDataFileSystem {
|
||||
public:
|
||||
explicit DirectorySaveDataFileSystem(VirtualDir base_filesystem);
|
||||
// optional directory here for backup
|
||||
explicit DirectorySaveDataFileSystem(VirtualDir base_filesystem, VirtualDir backup_filesystem = nullptr);
|
||||
~DirectorySaveDataFileSystem();
|
||||
|
||||
/// Initialize the journaling filesystem
|
||||
@@ -54,6 +55,7 @@ private:
|
||||
Result RetryFinitelyForTargetLocked(std::function<Result()> operation);
|
||||
|
||||
VirtualDir base_fs;
|
||||
VirtualDir backup_fs; // This will store the NAND path
|
||||
VirtualDir working_dir;
|
||||
VirtualDir committed_dir;
|
||||
SaveDataExtraDataAccessor extra_data_accessor;
|
||||
|
||||
@@ -7,8 +7,10 @@
|
||||
#include "common/assert.h"
|
||||
#include "common/common_types.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/settings.h"
|
||||
#include "common/uuid.h"
|
||||
#include "core/core.h"
|
||||
#include "core/file_sys/directory_save_data_filesystem.h"
|
||||
#include "core/file_sys/errors.h"
|
||||
#include "core/file_sys/savedata_extra_data_accessor.h"
|
||||
#include "core/file_sys/savedata_factory.h"
|
||||
@@ -58,8 +60,9 @@ std::string GetFutureSaveDataPath(SaveDataSpaceId space_id, SaveDataType type, u
|
||||
} // Anonymous namespace
|
||||
|
||||
SaveDataFactory::SaveDataFactory(Core::System& system_, ProgramId program_id_,
|
||||
VirtualDir save_directory_)
|
||||
: system{system_}, program_id{program_id_}, dir{std::move(save_directory_)} {
|
||||
VirtualDir save_directory_, VirtualDir backup_directory_)
|
||||
: system{system_}, program_id{program_id_}, dir{std::move(save_directory_)},
|
||||
backup_dir{std::move(backup_directory_)} {
|
||||
// Delete all temporary storages
|
||||
// On hardware, it is expected that temporary storage be empty at first use.
|
||||
dir->DeleteSubdirectoryRecursive("temp");
|
||||
@@ -100,7 +103,6 @@ VirtualDir SaveDataFactory::Create(SaveDataSpaceId space, const SaveDataAttribut
|
||||
}
|
||||
|
||||
VirtualDir SaveDataFactory::Open(SaveDataSpaceId space, const SaveDataAttribute& meta) const {
|
||||
|
||||
const auto save_directory = GetFullPath(program_id, dir, space, meta.type, meta.program_id,
|
||||
meta.user_id, meta.system_save_data_id);
|
||||
|
||||
@@ -324,4 +326,41 @@ Result SaveDataFactory::WriteSaveDataExtraDataWithMask(const SaveDataExtraData&
|
||||
return ResultSuccess;
|
||||
}
|
||||
|
||||
void SaveDataFactory::DoNandBackup(SaveDataSpaceId space, const SaveDataAttribute& meta, VirtualDir custom_dir) const {
|
||||
LOG_INFO(Common, "Dual-Save: Backup process initiated for Program ID: {:016X}", program_id);
|
||||
|
||||
if (!Settings::values.backup_saves_to_nand.GetValue()) {
|
||||
LOG_INFO(Common, "Dual-Save: Backup skipped (Setting is OFF)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (backup_dir == nullptr) {
|
||||
LOG_ERROR(Common, "Dual-Save: Backup failed (NAND directory is NULL)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (custom_dir == nullptr) {
|
||||
LOG_ERROR(Common, "Dual-Save: Backup failed (Source Custom directory is NULL)");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto nand_path = GetFullPath(program_id, backup_dir, space, meta.type, meta.program_id,
|
||||
meta.user_id, meta.system_save_data_id);
|
||||
|
||||
auto nand_out = backup_dir->CreateDirectoryRelative(nand_path);
|
||||
if (nand_out != nullptr) {
|
||||
LOG_INFO(Common, "Dual-Save: Mirroring files to NAND: {}", nand_path);
|
||||
|
||||
// Clear the old backup
|
||||
nand_out->CleanSubdirectoryRecursive(".");
|
||||
|
||||
// Perform the copy
|
||||
VfsRawCopyD(custom_dir, nand_out);
|
||||
|
||||
LOG_INFO(Common, "Dual-Save: NAND Backup successful.");
|
||||
} else {
|
||||
LOG_ERROR(Common, "Dual-Save: Could not create/access NAND backup path!");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace FileSys
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||
// SPDX-FileCopyrightText: 2025 citron Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
@@ -27,7 +28,7 @@ using ProgramId = u64;
|
||||
class SaveDataFactory {
|
||||
public:
|
||||
explicit SaveDataFactory(Core::System& system_, ProgramId program_id_,
|
||||
VirtualDir save_directory_);
|
||||
VirtualDir save_directory_, VirtualDir backup_directory_ = nullptr);
|
||||
~SaveDataFactory();
|
||||
|
||||
VirtualDir Create(SaveDataSpaceId space, const SaveDataAttribute& meta) const;
|
||||
@@ -54,11 +55,13 @@ public:
|
||||
const SaveDataAttribute& attribute) const;
|
||||
|
||||
void SetAutoCreate(bool state);
|
||||
void DoNandBackup(SaveDataSpaceId space, const SaveDataAttribute& meta, VirtualDir custom_dir) const;
|
||||
|
||||
private:
|
||||
Core::System& system;
|
||||
ProgramId program_id;
|
||||
VirtualDir dir;
|
||||
VirtualDir backup_dir; // This will hold the NAND path
|
||||
bool auto_create{true};
|
||||
};
|
||||
|
||||
|
||||
@@ -431,7 +431,12 @@ std::shared_ptr<FileSys::SaveDataFactory> FileSystemController::CreateSaveDataFa
|
||||
if (!custom_path_str.empty() && Common::FS::IsDir(custom_path)) {
|
||||
LOG_INFO(Service_FS, "Using custom save path for program_id={:016X}: {}", program_id, custom_path_str);
|
||||
auto custom_save_directory = vfs->OpenDirectory(custom_path_str, rw_mode);
|
||||
return std::make_shared<FileSys::SaveDataFactory>(system, program_id, std::move(custom_save_directory));
|
||||
|
||||
// Fetch the default NAND directory to act as the backup location
|
||||
auto nand_directory = vfs->OpenDirectory(Common::FS::GetCitronPathString(CitronPath::NANDDir), rw_mode);
|
||||
|
||||
return std::make_shared<FileSys::SaveDataFactory>(
|
||||
system, program_id, std::move(custom_save_directory), std::move(nand_directory));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||
// SPDX-FileCopyrightText: 2025 citron Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "common/string_util.h"
|
||||
#include "common/settings.h"
|
||||
#include "core/file_sys/fssrv/fssrv_sf_path.h"
|
||||
#include "core/hle/service/cmif_serialization.h"
|
||||
#include "core/hle/service/filesystem/fsp/fs_i_directory.h"
|
||||
@@ -10,10 +12,16 @@
|
||||
|
||||
namespace Service::FileSystem {
|
||||
|
||||
IFileSystem::IFileSystem(Core::System& system_, FileSys::VirtualDir dir_, SizeGetter size_getter_)
|
||||
: ServiceFramework{system_, "IFileSystem"}, backend{std::make_unique<FileSys::Fsa::IFileSystem>(
|
||||
dir_)},
|
||||
size_getter{std::move(size_getter_)} {
|
||||
IFileSystem::IFileSystem(Core::System& system_, FileSys::VirtualDir dir_, SizeGetter size_getter_,
|
||||
std::shared_ptr<FileSys::SaveDataFactory> factory_,
|
||||
FileSys::SaveDataSpaceId space_id_, FileSys::SaveDataAttribute attribute_)
|
||||
: ServiceFramework{system_, "IFileSystem"},
|
||||
backend{std::make_unique<FileSys::Fsa::IFileSystem>(dir_)},
|
||||
size_getter{std::move(size_getter_)},
|
||||
content_dir{std::move(dir_)},
|
||||
save_factory{std::move(factory_)},
|
||||
save_space{space_id_},
|
||||
save_attr{attribute_} {
|
||||
static const FunctionInfo functions[] = {
|
||||
{0, D<&IFileSystem::CreateFile>, "CreateFile"},
|
||||
{1, D<&IFileSystem::DeleteFile>, "DeleteFile"},
|
||||
@@ -124,11 +132,15 @@ Result IFileSystem::GetEntryType(
|
||||
}
|
||||
|
||||
Result IFileSystem::Commit() {
|
||||
LOG_DEBUG(Service_FS, "called");
|
||||
Result res = backend->Commit();
|
||||
if (res != ResultSuccess) return res;
|
||||
|
||||
// Based on LibHac DirectorySaveDataFileSystem::DoCommit
|
||||
// The backend FSA layer should handle the actual commit logic
|
||||
R_RETURN(backend->Commit());
|
||||
if (save_factory && Settings::values.backup_saves_to_nand.GetValue()) {
|
||||
LOG_INFO(Common, "IFileSystem: Commit detected, triggering NAND backup...");
|
||||
save_factory->DoNandBackup(save_space, save_attr, content_dir);
|
||||
}
|
||||
|
||||
R_RETURN(res);
|
||||
}
|
||||
|
||||
Result IFileSystem::GetFreeSpaceSize(
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||
// SPDX-FileCopyrightText: 2025 citron Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "common/common_funcs.h"
|
||||
#include "core/file_sys/fs_filesystem.h"
|
||||
#include "core/file_sys/fs_save_data_types.h"
|
||||
#include "core/file_sys/fsa/fs_i_filesystem.h"
|
||||
#include "core/file_sys/vfs/vfs.h"
|
||||
#include "core/hle/service/cmif_types.h"
|
||||
@@ -23,7 +25,10 @@ class IDirectory;
|
||||
|
||||
class IFileSystem final : public ServiceFramework<IFileSystem> {
|
||||
public:
|
||||
explicit IFileSystem(Core::System& system_, FileSys::VirtualDir dir_, SizeGetter size_getter_);
|
||||
explicit IFileSystem(Core::System& system_, FileSys::VirtualDir dir_, SizeGetter size_getter_,
|
||||
std::shared_ptr<FileSys::SaveDataFactory> factory_ = nullptr,
|
||||
FileSys::SaveDataSpaceId space_id_ = {},
|
||||
FileSys::SaveDataAttribute attribute_ = {});
|
||||
|
||||
Result CreateFile(const InLargeData<FileSys::Sf::Path, BufferAttr_HipcPointer> path, s32 option,
|
||||
s64 size);
|
||||
@@ -55,6 +60,10 @@ public:
|
||||
private:
|
||||
std::unique_ptr<FileSys::Fsa::IFileSystem> backend;
|
||||
SizeGetter size_getter;
|
||||
FileSys::VirtualDir content_dir;
|
||||
std::shared_ptr<FileSys::SaveDataFactory> save_factory;
|
||||
FileSys::SaveDataSpaceId save_space;
|
||||
FileSys::SaveDataAttribute save_attr;
|
||||
};
|
||||
|
||||
} // namespace Service::FileSystem
|
||||
|
||||
@@ -280,8 +280,9 @@ Result FSP_SRV::OpenSaveDataFileSystem(OutInterface<IFileSystem> out_interface,
|
||||
break;
|
||||
}
|
||||
|
||||
*out_interface =
|
||||
std::make_shared<IFileSystem>(system, std::move(dir), SizeGetter::FromStorageId(fsc, id));
|
||||
*out_interface = std::make_shared<IFileSystem>(
|
||||
system, std::move(dir), SizeGetter::FromStorageId(fsc, id),
|
||||
save_data_controller->GetFactory(), space_id, attribute);
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
|
||||
// SPDX-FileCopyrightText: 2025 citron Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
@@ -38,6 +39,10 @@ public:
|
||||
FileSys::SaveDataSpaceId space,
|
||||
const FileSys::SaveDataAttribute& attribute);
|
||||
|
||||
std::shared_ptr<FileSys::SaveDataFactory> GetFactory() const {
|
||||
return factory;
|
||||
}
|
||||
|
||||
void SetAutoCreate(bool state);
|
||||
|
||||
private:
|
||||
|
||||
@@ -283,6 +283,7 @@ void Config::ReadDataStorageValues() {
|
||||
FS::SetCitronPath(FS::CitronPath::TASDir, ReadStringSetting(std::string("tas_directory")));
|
||||
|
||||
ReadCategory(Settings::Category::DataStorage);
|
||||
Settings::values.backup_saves_to_nand = ReadBooleanSetting(std::string("backup_saves_to_nand"), false);
|
||||
|
||||
EndGroup();
|
||||
}
|
||||
@@ -636,6 +637,7 @@ void Config::SaveDataStorageValues() {
|
||||
std::make_optional(FS::GetCitronPathString(FS::CitronPath::TASDir)));
|
||||
|
||||
WriteCategory(Settings::Category::DataStorage);
|
||||
WriteBooleanSetting(std::string("backup_saves_to_nand"), Settings::values.backup_saves_to_nand.GetValue());
|
||||
|
||||
EndGroup();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user