mirror of
https://git.citron-emu.org/citron/emulator
synced 2026-01-19 01:13:47 +00:00
feat(fs): Cross-Compatible Emulator Save Pathing w/ Custom Save Paths
Signed-off-by: Collecting <collecting@noreply.localhost>
This commit is contained in:
@@ -3,7 +3,9 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include "common/assert.h"
|
||||
#include "common/common_types.h"
|
||||
#include "common/logging/log.h"
|
||||
@@ -15,56 +17,70 @@
|
||||
#include "core/file_sys/savedata_extra_data_accessor.h"
|
||||
#include "core/file_sys/savedata_factory.h"
|
||||
#include "core/file_sys/vfs/vfs.h"
|
||||
#include "core/file_sys/vfs/vfs_real.h"
|
||||
#include "core/hle/service/acc/profile_manager.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
namespace {
|
||||
|
||||
// Using a leaked raw pointer for the RealVfsFilesystem singleton.
|
||||
// This prevents SIGSEGV during shutdown by ensuring the VFS bridge
|
||||
// outlives all threads that might still be flushing save data.
|
||||
RealVfsFilesystem* GetPersistentVfs() {
|
||||
static RealVfsFilesystem* instance = new RealVfsFilesystem();
|
||||
return instance;
|
||||
}
|
||||
|
||||
bool ShouldSaveDataBeAutomaticallyCreated(SaveDataSpaceId space, const SaveDataAttribute& attr) {
|
||||
return attr.type == SaveDataType::Cache || attr.type == SaveDataType::Temporary ||
|
||||
(space == SaveDataSpaceId::User && ///< Normal Save Data -- Current Title & User
|
||||
(space == SaveDataSpaceId::User &&
|
||||
(attr.type == SaveDataType::Account || attr.type == SaveDataType::Device) &&
|
||||
attr.program_id == 0 && attr.system_save_data_id == 0);
|
||||
}
|
||||
|
||||
std::string GetFutureSaveDataPath(SaveDataSpaceId space_id, SaveDataType type, u64 title_id,
|
||||
u128 user_id) {
|
||||
// Only detect nand user saves.
|
||||
const auto space_id_path = [space_id]() -> std::string_view {
|
||||
switch (space_id) {
|
||||
case SaveDataSpaceId::User:
|
||||
return "/user/save";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}();
|
||||
|
||||
if (space_id_path.empty()) {
|
||||
if (space_id != SaveDataSpaceId::User) {
|
||||
return "";
|
||||
}
|
||||
|
||||
Common::UUID uuid;
|
||||
std::memcpy(uuid.uuid.data(), user_id.data(), sizeof(Common::UUID));
|
||||
|
||||
// Only detect account/device saves from the future location.
|
||||
switch (type) {
|
||||
case SaveDataType::Account:
|
||||
return fmt::format("{}/account/{}/{:016X}/0", space_id_path, uuid.RawString(), title_id);
|
||||
return fmt::format("/user/save/account/{}/{:016X}/0", uuid.RawString(), title_id);
|
||||
case SaveDataType::Device:
|
||||
return fmt::format("{}/device/{:016X}/0", space_id_path, title_id);
|
||||
return fmt::format("/user/save/device/{:016X}/0", title_id);
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
void BufferedVfsCopy(VirtualFile source, VirtualFile dest) {
|
||||
if (!source || !dest) return;
|
||||
try {
|
||||
std::vector<u8> buffer(0x100000); // 1MB buffer
|
||||
dest->Resize(0);
|
||||
size_t offset = 0;
|
||||
while (offset < source->GetSize()) {
|
||||
const size_t to_read = std::min(buffer.size(), source->GetSize() - offset);
|
||||
source->Read(buffer.data(), to_read, offset);
|
||||
dest->Write(buffer.data(), to_read, offset);
|
||||
offset += to_read;
|
||||
}
|
||||
} catch (...) {
|
||||
LOG_ERROR(Service_FS, "Critical error during VFS mirror operation.");
|
||||
}
|
||||
}
|
||||
|
||||
} // Anonymous namespace
|
||||
|
||||
SaveDataFactory::SaveDataFactory(Core::System& system_, ProgramId program_id_,
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -79,19 +95,14 @@ VirtualDir SaveDataFactory::Create(SaveDataSpaceId space, const SaveDataAttribut
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Initialize ExtraData for new save
|
||||
SaveDataExtraDataAccessor accessor(save_dir);
|
||||
if (accessor.Initialize(true) != ResultSuccess) {
|
||||
LOG_WARNING(Service_FS, "Failed to initialize ExtraData for new save at {}", save_directory);
|
||||
// Continue anyway - save is still usable
|
||||
} else {
|
||||
// Write initial extra data
|
||||
if (accessor.Initialize(true) == ResultSuccess) {
|
||||
SaveDataExtraData initial_data{};
|
||||
initial_data.attr = meta;
|
||||
initial_data.owner_id = meta.program_id;
|
||||
initial_data.timestamp = std::chrono::system_clock::now().time_since_epoch().count();
|
||||
initial_data.flags = static_cast<u32>(SaveDataFlags::None);
|
||||
initial_data.available_size = 0; // Will be updated on commit
|
||||
initial_data.available_size = 0;
|
||||
initial_data.journal_size = 0;
|
||||
initial_data.commit_id = 1;
|
||||
|
||||
@@ -122,6 +133,8 @@ VirtualDir SaveDataFactory::GetSaveDataSpaceDirectory(SaveDataSpaceId space) con
|
||||
std::string SaveDataFactory::GetSaveDataSpaceIdPath(SaveDataSpaceId space) {
|
||||
switch (space) {
|
||||
case SaveDataSpaceId::System:
|
||||
case SaveDataSpaceId::ProperSystem:
|
||||
case SaveDataSpaceId::SafeMode:
|
||||
return "/system/";
|
||||
case SaveDataSpaceId::User:
|
||||
return "/user/";
|
||||
@@ -130,54 +143,37 @@ std::string SaveDataFactory::GetSaveDataSpaceIdPath(SaveDataSpaceId space) {
|
||||
case SaveDataSpaceId::SdSystem:
|
||||
case SaveDataSpaceId::SdUser:
|
||||
return "/sd/";
|
||||
case SaveDataSpaceId::ProperSystem:
|
||||
return "/system/";
|
||||
case SaveDataSpaceId::SafeMode:
|
||||
return "/system/";
|
||||
default:
|
||||
ASSERT_MSG(false, "Unrecognized SaveDataSpaceId: {:02X}", static_cast<u8>(space));
|
||||
return "/unrecognized/"; ///< To prevent corruption when ignoring asserts.
|
||||
return "/unrecognized/";
|
||||
}
|
||||
}
|
||||
|
||||
std::string SaveDataFactory::GetFullPath(ProgramId program_id, VirtualDir dir,
|
||||
SaveDataSpaceId space, SaveDataType type, u64 title_id,
|
||||
u128 user_id, u64 save_id) {
|
||||
// According to switchbrew, if a save is of type SaveData and the title id field is 0, it should
|
||||
// be interpreted as the title id of the current process.
|
||||
if (type == SaveDataType::Account || type == SaveDataType::Device) {
|
||||
if (title_id == 0) {
|
||||
title_id = program_id;
|
||||
}
|
||||
if ((type == SaveDataType::Account || type == SaveDataType::Device) && title_id == 0) {
|
||||
title_id = program_id;
|
||||
}
|
||||
|
||||
// For compat with a future impl.
|
||||
if (std::string future_path =
|
||||
GetFutureSaveDataPath(space, type, title_id & ~(0xFFULL), user_id);
|
||||
if (std::string future_path = GetFutureSaveDataPath(space, type, title_id & ~(0xFFULL), user_id);
|
||||
!future_path.empty()) {
|
||||
// Check if this location exists, and prefer it over the old.
|
||||
if (const auto future_dir = dir->GetDirectoryRelative(future_path); future_dir != nullptr) {
|
||||
LOG_INFO(Service_FS, "Using save at new location: {}", future_path);
|
||||
if (dir->GetDirectoryRelative(future_path) != nullptr) {
|
||||
return future_path;
|
||||
}
|
||||
}
|
||||
|
||||
std::string out = GetSaveDataSpaceIdPath(space);
|
||||
|
||||
switch (type) {
|
||||
case SaveDataType::System:
|
||||
return fmt::format("{}save/{:016X}/{:016X}{:016X}", out, save_id, user_id[1], user_id[0]);
|
||||
case SaveDataType::Account:
|
||||
case SaveDataType::Device:
|
||||
return fmt::format("{}save/{:016X}/{:016X}{:016X}/{:016X}", out, 0, user_id[1], user_id[0],
|
||||
title_id);
|
||||
return fmt::format("{}save/{:016X}/{:016X}{:016X}/{:016X}", out, 0, user_id[1], user_id[0], title_id);
|
||||
case SaveDataType::Temporary:
|
||||
return fmt::format("{}{:016X}/{:016X}{:016X}/{:016X}", out, 0, user_id[1], user_id[0],
|
||||
title_id);
|
||||
return fmt::format("{}{:016X}/{:016X}{:016X}/{:016X}", out, 0, user_id[1], user_id[0], title_id);
|
||||
case SaveDataType::Cache:
|
||||
return fmt::format("{}save/cache/{:016X}", out, title_id);
|
||||
default:
|
||||
ASSERT_MSG(false, "Unrecognized SaveDataType: {:02X}", static_cast<u8>(type));
|
||||
return fmt::format("{}save/unknown_{:X}/{:016X}", out, static_cast<u8>(type), title_id);
|
||||
}
|
||||
}
|
||||
@@ -191,36 +187,21 @@ std::string SaveDataFactory::GetUserGameSaveDataRoot(u128 user_id, bool future)
|
||||
return fmt::format("/user/save/{:016X}/{:016X}{:016X}", 0, user_id[1], user_id[0]);
|
||||
}
|
||||
|
||||
SaveDataSize SaveDataFactory::ReadSaveDataSize(SaveDataType type, u64 title_id,
|
||||
u128 user_id) const {
|
||||
const auto path =
|
||||
GetFullPath(program_id, dir, SaveDataSpaceId::User, type, title_id, user_id, 0);
|
||||
SaveDataSize SaveDataFactory::ReadSaveDataSize(SaveDataType type, u64 title_id, u128 user_id) const {
|
||||
const auto path = GetFullPath(program_id, dir, SaveDataSpaceId::User, type, title_id, user_id, 0);
|
||||
const auto relative_dir = GetOrCreateDirectoryRelative(dir, path);
|
||||
|
||||
const auto size_file = relative_dir->GetFile(GetSaveDataSizeFileName());
|
||||
if (size_file == nullptr || size_file->GetSize() < sizeof(SaveDataSize)) {
|
||||
return {0, 0};
|
||||
}
|
||||
|
||||
if (size_file == nullptr || size_file->GetSize() < sizeof(SaveDataSize)) return {0, 0};
|
||||
SaveDataSize out;
|
||||
if (size_file->ReadObject(&out) != sizeof(SaveDataSize)) {
|
||||
return {0, 0};
|
||||
}
|
||||
|
||||
if (size_file->ReadObject(&out) != sizeof(SaveDataSize)) return {0, 0};
|
||||
return out;
|
||||
}
|
||||
|
||||
void SaveDataFactory::WriteSaveDataSize(SaveDataType type, u64 title_id, u128 user_id,
|
||||
SaveDataSize new_value) const {
|
||||
const auto path =
|
||||
GetFullPath(program_id, dir, SaveDataSpaceId::User, type, title_id, user_id, 0);
|
||||
void SaveDataFactory::WriteSaveDataSize(SaveDataType type, u64 title_id, u128 user_id, SaveDataSize new_value) const {
|
||||
const auto path = GetFullPath(program_id, dir, SaveDataSpaceId::User, type, title_id, user_id, 0);
|
||||
const auto relative_dir = GetOrCreateDirectoryRelative(dir, path);
|
||||
|
||||
const auto size_file = relative_dir->CreateFile(GetSaveDataSizeFileName());
|
||||
if (size_file == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (size_file == nullptr) return;
|
||||
size_file->Resize(sizeof(SaveDataSize));
|
||||
size_file->WriteObject(new_value);
|
||||
}
|
||||
@@ -229,137 +210,173 @@ void SaveDataFactory::SetAutoCreate(bool state) {
|
||||
auto_create = state;
|
||||
}
|
||||
|
||||
Result SaveDataFactory::ReadSaveDataExtraData(SaveDataExtraData* out_extra_data,
|
||||
SaveDataSpaceId space,
|
||||
const SaveDataAttribute& attribute) const {
|
||||
const auto save_directory =
|
||||
GetFullPath(program_id, dir, space, attribute.type, attribute.program_id, attribute.user_id,
|
||||
attribute.system_save_data_id);
|
||||
|
||||
Result SaveDataFactory::ReadSaveDataExtraData(SaveDataExtraData* out_extra_data, SaveDataSpaceId space, const SaveDataAttribute& attribute) const {
|
||||
const auto save_directory = GetFullPath(program_id, dir, space, attribute.type, attribute.program_id, attribute.user_id, attribute.system_save_data_id);
|
||||
auto save_dir = dir->GetDirectoryRelative(save_directory);
|
||||
if (save_dir == nullptr) {
|
||||
return ResultPathNotFound;
|
||||
}
|
||||
|
||||
if (save_dir == nullptr) return ResultPathNotFound;
|
||||
SaveDataExtraDataAccessor accessor(save_dir);
|
||||
|
||||
// Try to initialize (but don't create if missing)
|
||||
if (Result result = accessor.Initialize(false); result != ResultSuccess) {
|
||||
// ExtraData doesn't exist - return default values
|
||||
LOG_DEBUG(Service_FS, "ExtraData not found for save at {}, returning defaults",
|
||||
save_directory);
|
||||
|
||||
// Return zeroed data
|
||||
*out_extra_data = {}; // Or: *out_extra_data = SaveDataExtraData{};
|
||||
if (accessor.Initialize(false) != ResultSuccess) {
|
||||
*out_extra_data = {};
|
||||
out_extra_data->attr = attribute;
|
||||
return ResultSuccess;
|
||||
}
|
||||
|
||||
return accessor.ReadExtraData(out_extra_data);
|
||||
}
|
||||
|
||||
Result SaveDataFactory::WriteSaveDataExtraData(const SaveDataExtraData& extra_data,
|
||||
SaveDataSpaceId space,
|
||||
const SaveDataAttribute& attribute) const {
|
||||
const auto save_directory =
|
||||
GetFullPath(program_id, dir, space, attribute.type, attribute.program_id, attribute.user_id,
|
||||
attribute.system_save_data_id);
|
||||
|
||||
Result SaveDataFactory::WriteSaveDataExtraData(const SaveDataExtraData& extra_data, SaveDataSpaceId space, const SaveDataAttribute& attribute) const {
|
||||
const auto save_directory = GetFullPath(program_id, dir, space, attribute.type, attribute.program_id, attribute.user_id, attribute.system_save_data_id);
|
||||
auto save_dir = dir->GetDirectoryRelative(save_directory);
|
||||
if (save_dir == nullptr) {
|
||||
return ResultPathNotFound;
|
||||
}
|
||||
|
||||
if (save_dir == nullptr) return ResultPathNotFound;
|
||||
SaveDataExtraDataAccessor accessor(save_dir);
|
||||
|
||||
// Initialize and create if missing
|
||||
R_TRY(accessor.Initialize(true));
|
||||
|
||||
// Write the data
|
||||
R_TRY(accessor.WriteExtraData(extra_data));
|
||||
|
||||
// Commit immediately for transactional writes
|
||||
R_TRY(accessor.CommitExtraData());
|
||||
|
||||
return ResultSuccess;
|
||||
return accessor.CommitExtraData();
|
||||
}
|
||||
|
||||
Result SaveDataFactory::WriteSaveDataExtraDataWithMask(const SaveDataExtraData& extra_data,
|
||||
const SaveDataExtraData& mask,
|
||||
SaveDataSpaceId space,
|
||||
const SaveDataAttribute& attribute) const {
|
||||
const auto save_directory =
|
||||
GetFullPath(program_id, dir, space, attribute.type, attribute.program_id, attribute.user_id,
|
||||
attribute.system_save_data_id);
|
||||
|
||||
Result SaveDataFactory::WriteSaveDataExtraDataWithMask(const SaveDataExtraData& extra_data, const SaveDataExtraData& mask, SaveDataSpaceId space, const SaveDataAttribute& attribute) const {
|
||||
const auto save_directory = GetFullPath(program_id, dir, space, attribute.type, attribute.program_id, attribute.user_id, attribute.system_save_data_id);
|
||||
auto save_dir = dir->GetDirectoryRelative(save_directory);
|
||||
if (save_dir == nullptr) {
|
||||
return ResultPathNotFound;
|
||||
}
|
||||
|
||||
if (save_dir == nullptr) return ResultPathNotFound;
|
||||
SaveDataExtraDataAccessor accessor(save_dir);
|
||||
|
||||
// Initialize and create if missing
|
||||
R_TRY(accessor.Initialize(true));
|
||||
|
||||
// Read existing data
|
||||
SaveDataExtraData current_data{};
|
||||
R_TRY(accessor.ReadExtraData(¤t_data));
|
||||
|
||||
// Apply mask: copy only the bytes where mask is non-zero
|
||||
const u8* extra_data_bytes = reinterpret_cast<const u8*>(&extra_data);
|
||||
const u8* mask_bytes = reinterpret_cast<const u8*>(&mask);
|
||||
u8* current_data_bytes = reinterpret_cast<u8*>(¤t_data);
|
||||
|
||||
for (size_t i = 0; i < sizeof(SaveDataExtraData); ++i) {
|
||||
if (mask_bytes[i] != 0) {
|
||||
current_data_bytes[i] = extra_data_bytes[i];
|
||||
if (mask_bytes[i] != 0) current_data_bytes[i] = extra_data_bytes[i];
|
||||
}
|
||||
R_TRY(accessor.WriteExtraData(current_data));
|
||||
return accessor.CommitExtraData();
|
||||
}
|
||||
|
||||
// --- MIRRORING TOOLS ---
|
||||
|
||||
VirtualDir SaveDataFactory::GetMirrorDirectory(u64 title_id) const {
|
||||
auto it = Settings::values.mirrored_save_paths.find(title_id);
|
||||
if (it == Settings::values.mirrored_save_paths.end() || it->second.empty()) return nullptr;
|
||||
|
||||
std::filesystem::path host_path(it->second);
|
||||
if (!std::filesystem::exists(host_path)) return nullptr;
|
||||
|
||||
// Get the persistent VFS bridge
|
||||
auto* vfs = GetPersistentVfs();
|
||||
return vfs->OpenDirectory(it->second, OpenMode::ReadWrite);
|
||||
}
|
||||
|
||||
void SaveDataFactory::SmartSyncFromSource(VirtualDir source, VirtualDir dest) const {
|
||||
// Citron: Shutdown and null safety
|
||||
if (!source || !dest || system.IsShuttingDown()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Sync files from Source to Destination
|
||||
for (const auto& s_file : source->GetFiles()) {
|
||||
if (!s_file) continue;
|
||||
std::string name = s_file->GetName();
|
||||
|
||||
// Skip metadata and lock files
|
||||
if (name == ".lock" || name == ".citron_save_size" || name.find("mirror_backup") != std::string::npos) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto d_file = dest->CreateFile(name);
|
||||
if (d_file) {
|
||||
BufferedVfsCopy(s_file, d_file);
|
||||
}
|
||||
}
|
||||
|
||||
// Write back the masked data
|
||||
R_TRY(accessor.WriteExtraData(current_data));
|
||||
// Recurse into subdirectories
|
||||
for (const auto& s_subdir : source->GetSubdirectories()) {
|
||||
if (!s_subdir) continue;
|
||||
|
||||
// Commit the changes
|
||||
R_TRY(accessor.CommitExtraData());
|
||||
// Prevent recursion into title-id-named folders to avoid infinite loops
|
||||
if (s_subdir->GetName().find("0100") != std::string::npos) continue;
|
||||
|
||||
return ResultSuccess;
|
||||
auto d_subdir = dest->GetDirectoryRelative(s_subdir->GetName());
|
||||
if (!d_subdir) {
|
||||
d_subdir = dest->CreateDirectoryRelative(s_subdir->GetName());
|
||||
}
|
||||
|
||||
if (d_subdir) {
|
||||
SmartSyncFromSource(s_subdir, d_subdir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SaveDataFactory::PerformStartupMirrorSync() const {
|
||||
// If settings are empty or system is shutting down/uninitialized
|
||||
if (Settings::values.mirrored_save_paths.empty() || system.IsShuttingDown()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure our NAND directory is actually valid
|
||||
if (!dir) {
|
||||
LOG_ERROR(Service_FS, "Mirroring: Startup Sync aborted. NAND directory is null.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Attempt to locate the save root with null checks at every step
|
||||
VirtualDir user_save_root = nullptr;
|
||||
try {
|
||||
user_save_root = dir->GetDirectoryRelative("user/save/0000000000000000");
|
||||
if (!user_save_root) {
|
||||
user_save_root = dir->GetDirectoryRelative("user/save");
|
||||
}
|
||||
} catch (...) {
|
||||
LOG_ERROR(Service_FS, "Mirroring: Critical failure accessing VFS. Filesystem may be stale.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!user_save_root) {
|
||||
LOG_WARNING(Service_FS, "Mirroring: Could not find user save root in NAND.");
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_INFO(Service_FS, "Mirroring: Startup Sync initiated.");
|
||||
|
||||
for (const auto& [title_id, host_path] : Settings::values.mirrored_save_paths) {
|
||||
if (host_path.empty()) continue;
|
||||
|
||||
auto mirror_source = GetMirrorDirectory(title_id);
|
||||
if (!mirror_source) continue;
|
||||
|
||||
std::string title_id_str = fmt::format("{:016X}", title_id);
|
||||
|
||||
for (const auto& profile_dir : user_save_root->GetSubdirectories()) {
|
||||
if (!profile_dir) continue;
|
||||
|
||||
auto nand_dest = profile_dir->GetDirectoryRelative(title_id_str);
|
||||
|
||||
if (!nand_dest) {
|
||||
for (const auto& sub : profile_dir->GetSubdirectories()) {
|
||||
if (!sub) continue;
|
||||
nand_dest = sub->GetDirectoryRelative(title_id_str);
|
||||
if (nand_dest) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (nand_dest) {
|
||||
LOG_INFO(Service_FS, "Mirroring: Pulling external data for {}", title_id_str);
|
||||
SmartSyncFromSource(mirror_source, nand_dest);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
u64 title_id = (meta.program_id != 0 ? meta.program_id : static_cast<u64>(program_id));
|
||||
if (Settings::values.mirrored_save_paths.count(title_id)) return;
|
||||
|
||||
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);
|
||||
if (!Settings::values.backup_saves_to_nand.GetValue() || backup_dir == nullptr || custom_dir == nullptr) 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
|
||||
if (nand_out) {
|
||||
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!");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user