Migrate only user files

This commit is contained in:
Luke Street
2026-05-13 00:47:29 -06:00
parent 8830760d34
commit 9ce1ab7d5a
7 changed files with 253 additions and 92 deletions
+188 -79
View File
@@ -6,6 +6,7 @@
#include "dusk/main.h"
#include <array>
#include <chrono>
#include <filesystem>
#include <optional>
#include <ranges>
@@ -30,6 +31,21 @@ constexpr auto kLocationDescriptorName = "data_location.json";
constexpr auto kPipelineCacheName = "pipeline_cache.db";
constexpr auto kInitialPipelineCacheName = "initial_pipeline_cache.db";
constexpr std::array<std::string_view, 4> kUserDataDirectories = {
"texture_replacements",
"USA",
"EUR",
"JAP",
};
constexpr std::array<std::string_view, 6> kUserDataFiles = {
"achievements.json",
"config.json",
"controller_ports.dat",
"imgui.ini",
"keyboard_bindings.dat",
"states.json",
};
enum class LocationMode {
Default,
Portable,
@@ -249,6 +265,62 @@ std::filesystem::path absolute_path(const std::filesystem::path& path) {
return absolute.lexically_normal();
}
void rename_legacy_pref_path(
const std::filesystem::path& legacyPath, const std::filesystem::path& prefPath) {
if (legacyPath.empty() || prefPath.empty() ||
normalized_path(legacyPath) == normalized_path(prefPath))
{
return;
}
std::error_code ec;
if (!std::filesystem::exists(legacyPath, ec)) {
if (ec) {
Log.warn("Failed to inspect legacy data directory '{}': {}",
io::fs_path_to_string(legacyPath), ec.message());
}
return;
}
const bool prefExists = std::filesystem::exists(prefPath, ec);
if (ec) {
Log.warn("Failed to inspect data directory '{}': {}", io::fs_path_to_string(prefPath),
ec.message());
return;
}
if (prefExists) {
if (!std::filesystem::is_directory(prefPath, ec) ||
!std::filesystem::is_empty(prefPath, ec))
{
if (ec) {
Log.warn("Failed to inspect data directory '{}': {}",
io::fs_path_to_string(prefPath), ec.message());
} else {
Log.info("Skipping legacy data directory rename because '{}' is not empty",
io::fs_path_to_string(prefPath));
}
return;
}
std::filesystem::remove(prefPath, ec);
if (ec) {
Log.warn("Failed to remove empty data directory '{}' before legacy rename: {}",
io::fs_path_to_string(prefPath), ec.message());
return;
}
}
std::filesystem::rename(legacyPath, prefPath, ec);
if (ec) {
Log.warn("Failed to rename legacy data directory '{}' to '{}': {}",
io::fs_path_to_string(legacyPath), io::fs_path_to_string(prefPath), ec.message());
return;
}
Log.info("Renamed legacy data directory '{}' to '{}'", io::fs_path_to_string(legacyPath),
io::fs_path_to_string(prefPath));
}
bool is_same_or_inside(const std::filesystem::path& root, const std::filesystem::path& path) {
const auto normalizedRoot = normalized_path(root);
const auto normalizedPath = normalized_path(path);
@@ -283,78 +355,39 @@ bool should_skip_migration_path(const std::filesystem::path& path,
return false;
}
bool has_location_descriptor(const std::filesystem::path& path) {
std::error_code ec;
return std::filesystem::exists(path / kLocationDescriptorName, ec);
bool matches_name(std::string_view name, const auto& names) {
return std::ranges::find(names, name) != names.end();
}
bool remove_empty_destination_for_rename(const std::filesystem::path& path) {
std::error_code ec;
const bool exists = std::filesystem::exists(path, ec);
if (ec) {
Log.debug("Could not inspect migration destination '{}': {}", io::fs_path_to_string(path),
ec.message());
bool should_migrate_user_data_path(
const std::filesystem::path& sourcePath, const std::filesystem::path& from) {
const auto relativePath = sourcePath.lexically_relative(from);
if (relativePath.empty() || relativePath.is_absolute()) {
return false;
}
if (!exists) {
auto it = relativePath.begin();
if (it == relativePath.end() || *it == "..") {
return false;
}
const auto first = io::fs_path_to_string(*it);
if (matches_name(first, kUserDataDirectories)) {
return true;
}
const bool canRemove = std::filesystem::is_directory(path, ec) &&
std::filesystem::is_empty(path, ec) && !has_location_descriptor(path);
if (ec || !canRemove) {
if (ec) {
Log.debug("Could not inspect migration destination '{}': {}",
io::fs_path_to_string(path), ec.message());
}
++it;
if (it != relativePath.end()) {
return false;
}
std::filesystem::remove(path, ec);
if (ec) {
Log.debug("Could not remove empty migration destination '{}': {}",
io::fs_path_to_string(path), ec.message());
return false;
const auto filename = io::fs_path_to_string(relativePath.filename());
if (matches_name(filename, kUserDataFiles)) {
return true;
}
return true;
}
bool try_rename_directory_migration(
const std::filesystem::path& from, const std::filesystem::path& to) {
std::error_code ec;
if (!std::filesystem::is_directory(from, ec)) {
return false;
}
if (ec) {
Log.debug("Could not inspect migration source '{}': {}", io::fs_path_to_string(from),
ec.message());
return false;
}
if (has_location_descriptor(from)) {
return false;
}
if (!remove_empty_destination_for_rename(to)) {
return false;
}
std::filesystem::create_directories(to.parent_path(), ec);
if (ec) {
Log.debug("Could not create migration destination parent '{}': {}",
io::fs_path_to_string(to.parent_path()), ec.message());
return false;
}
std::filesystem::rename(from, to, ec);
if (ec) {
Log.debug("Could not rename data directory '{}' to '{}': {}", io::fs_path_to_string(from),
io::fs_path_to_string(to), ec.message());
return false;
}
Log.info("Renamed data directory '{}' to '{}'", io::fs_path_to_string(from),
io::fs_path_to_string(to));
return true;
return relativePath.extension() == ".controller" || relativePath.extension() == ".gci" ||
(filename.starts_with("MemoryCard") && filename.ends_with(".raw"));
}
std::filesystem::path current_data_path() {
@@ -439,6 +472,61 @@ bool write_location_descriptor(LocationMode mode, const std::filesystem::path& t
return false;
}
void set_error(std::string* errorOut, std::string error) {
if (errorOut != nullptr) {
*errorOut = std::move(error);
}
}
bool validate_writable_data_path(const std::filesystem::path& path, std::string* errorOut) {
if (path.empty()) {
set_error(errorOut, "Choose a folder.");
return false;
}
std::error_code ec;
std::filesystem::create_directories(path, ec);
if (ec) {
set_error(errorOut, fmt::format("{} could not create the selected folder.", AppName));
Log.warn("Failed to create custom data folder '{}': {}", io::fs_path_to_string(path),
ec.message());
return false;
}
if (!std::filesystem::is_directory(path, ec)) {
set_error(errorOut, "The selected path is not a folder.");
if (ec) {
Log.warn("Failed to inspect custom data folder '{}': {}", io::fs_path_to_string(path),
ec.message());
}
return false;
}
const auto probePath = path / fmt::format(".write-probe-{}.tmp",
std::chrono::steady_clock::now().time_since_epoch().count());
try {
io::FileStream::WriteAllText(probePath, "dusk");
} catch (const std::exception& e) {
set_error(errorOut, fmt::format("{} could not write to the selected folder.", AppName));
Log.warn("Failed write probe for custom data folder '{}': {}", io::fs_path_to_string(path),
e.what());
return false;
}
std::filesystem::remove(probePath, ec);
if (ec) {
set_error(
errorOut, fmt::format("{} could write to the selected folder, but could not remove "
"the test file it created.",
AppName));
Log.warn("Failed to remove custom data folder write probe '{}': {}",
io::fs_path_to_string(probePath), ec.message());
return false;
}
return true;
}
std::uintmax_t remove_empty_directories(const std::filesystem::path& root, bool includeRoot) {
std::error_code ec;
std::vector<std::filesystem::path> directories;
@@ -647,14 +735,6 @@ void migrate_directory(const std::filesystem::path& from, const std::filesystem:
return;
}
if (try_rename_directory_migration(from, to)) {
return;
}
if (try_rename_directory_migration(from, to)) {
return;
}
std::filesystem::create_directories(to, ec);
if (ec) {
++stats.failures;
@@ -700,6 +780,16 @@ void migrate_directory(const std::filesystem::path& from, const std::filesystem:
continue;
}
if (!should_migrate_user_data_path(sourcePath, from)) {
++stats.skippedUnsupportedEntries;
if (std::filesystem::is_directory(status)) {
it.disable_recursion_pending();
}
ec.clear();
it.increment(ec);
continue;
}
const auto relativePath = sourcePath.lexically_relative(from);
if (relativePath.empty() || relativePath.is_absolute()) {
++stats.failures;
@@ -761,8 +851,6 @@ void migrate_data(const std::filesystem::path& prefPath, const std::filesystem::
const LocationDescriptor* descriptor) {
if (descriptor && !descriptor->previousPath.empty()) {
migrate_directory(descriptor->previousPath, dataPath, prefPath);
} else if (const auto legacyPath = get_legacy_path(); !legacyPath.empty()) {
migrate_directory(legacyPath, dataPath, prefPath);
}
}
@@ -899,17 +987,25 @@ bool open_data_path() {
#endif
}
bool set_custom_data_path(const std::filesystem::path& path) {
if (path.empty()) {
Log.warn("Ignoring empty custom data path");
bool set_custom_data_path(const std::filesystem::path& path, std::string* errorOut) {
if (!validate_writable_data_path(path, errorOut)) {
return false;
}
return write_location_descriptor(LocationMode::Custom, path);
if (!write_location_descriptor(LocationMode::Custom, path)) {
set_error(errorOut, fmt::format("{} could not save the data folder setting.", AppName));
return false;
}
return true;
}
bool set_custom_data_path(const char* path) {
return set_custom_data_path(path_from_utf8(path));
bool set_custom_data_path(const char* path, std::string* errorOut) {
if (path == nullptr) {
set_error(errorOut, "Choose a folder.");
return false;
}
return set_custom_data_path(path_from_utf8(path), errorOut);
}
bool set_portable_data_path() {
@@ -941,6 +1037,13 @@ std::filesystem::path configured_data_path() {
return *sConfiguredDataPath;
}
std::filesystem::path cache_path() {
if (!CachePath.empty()) {
return CachePath;
}
return get_pref_path();
}
bool is_data_path_restart_pending() {
if (ConfigPath.empty()) {
return false;
@@ -949,8 +1052,10 @@ bool is_data_path_restart_pending() {
return normalized_path(ConfigPath) != normalized_path(configured_data_path());
}
std::filesystem::path initialize_data() {
Paths initialize_data() {
const auto prefPath = get_pref_path();
rename_legacy_pref_path(get_legacy_path(), prefPath);
const auto descriptor = read_location_descriptor(prefPath);
if (descriptor) {
sActiveDescriptorPath = descriptor->path;
@@ -963,9 +1068,13 @@ std::filesystem::path initialize_data() {
migrate_data(prefPath, dataPath, descriptor ? &descriptor->descriptor : nullptr);
ensure_data_directory(dataPath);
ensure_initial_pipeline_cache(dataPath);
ensure_data_directory(prefPath);
ensure_initial_pipeline_cache(prefPath);
return dataPath;
return Paths{
.userPath = dataPath,
.cachePath = prefPath,
};
}
} // namespace dusk::data