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:
Zephyron
2025-12-22 08:43:48 +00:00
13 changed files with 123 additions and 20 deletions

View File

@@ -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();

View File

@@ -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>

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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

View File

@@ -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};
};

View File

@@ -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));
}
}

View File

@@ -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(

View File

@@ -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

View File

@@ -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();
}

View File

@@ -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:

View File

@@ -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();
}