From 9a0dcb28b2d5a8c0b021698d8399cef389e66ee6 Mon Sep 17 00:00:00 2001 From: whyydk <254117058+whyydk@users.noreply.github.com> Date: Mon, 30 Mar 2026 09:03:41 +0200 Subject: [PATCH 1/5] qt: Add folder migration for NAND/SDMC on dir change --- .../configuration/configure_storage.cpp | 109 +++++++++++++++++- .../configuration/configure_storage.h | 7 +- .../configuration/configure_storage.ui | 16 +-- 3 files changed, 115 insertions(+), 17 deletions(-) diff --git a/src/citra_qt/configuration/configure_storage.cpp b/src/citra_qt/configuration/configure_storage.cpp index a8cabd987..52dcc025a 100644 --- a/src/citra_qt/configuration/configure_storage.cpp +++ b/src/citra_qt/configuration/configure_storage.cpp @@ -6,12 +6,17 @@ #include #include #include "citra_qt/configuration/configure_storage.h" + +#include +#include + #include "common/file_util.h" #include "common/settings.h" #include "ui_configure_storage.h" ConfigureStorage::ConfigureStorage(bool is_powered_on_, QWidget* parent) - : QWidget(parent), ui(std::make_unique()), is_powered_on{is_powered_on_} { + : QWidget(parent), ui(std::make_unique()), is_powered_on{is_powered_on_}, + parent{parent} { ui->setupUi(this); SetConfiguration(); @@ -22,12 +27,15 @@ ConfigureStorage::ConfigureStorage(bool is_powered_on_, QWidget* parent) connect(ui->change_nand_dir, &QPushButton::clicked, this, [this]() { ui->change_nand_dir->setEnabled(false); + const QString og_path = + QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir)); const QString dir_path = QFileDialog::getExistingDirectory( this, tr("Select NAND Directory"), QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir)), QFileDialog::ShowDirsOnly); if (!dir_path.isEmpty()) { FileUtil::UpdateUserPath(FileUtil::UserPath::NANDDir, dir_path.toStdString()); + MigrateFolder(og_path, dir_path); SetConfiguration(); } ui->change_nand_dir->setEnabled(true); @@ -40,12 +48,15 @@ ConfigureStorage::ConfigureStorage(bool is_powered_on_, QWidget* parent) connect(ui->change_sdmc_dir, &QPushButton::clicked, this, [this]() { ui->change_sdmc_dir->setEnabled(false); + const QString og_path = + QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir)); const QString dir_path = QFileDialog::getExistingDirectory( this, tr("Select SDMC Directory"), QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir)), QFileDialog::ShowDirsOnly); if (!dir_path.isEmpty()) { FileUtil::UpdateUserPath(FileUtil::UserPath::SDMCDir, dir_path.toStdString()); + MigrateFolder(og_path, dir_path); SetConfiguration(); } ui->change_sdmc_dir->setEnabled(true); @@ -95,6 +106,102 @@ void ConfigureStorage::ApplyConfiguration() { } } +void ConfigureStorage::MigrateFolder(const QString& from, const QString& dest) { + namespace fs = std::filesystem; + + std::error_code ec; + + bool source_exists = fs::exists(from.toStdString(), ec); + bool dest_exists = fs::exists(dest.toStdString(), ec); + + bool dest_has_content = false; + bool source_has_content = false; + + if (source_exists) { + bool source_empty = fs::is_empty(from.toStdString(), ec); + source_has_content = !ec && !source_empty; + } + + if (dest_exists) { + bool dest_empty = fs::is_empty(dest.toStdString(), ec); + dest_has_content = !ec && !dest_empty; + } + + if (!source_has_content) { + QMessageBox::information(this, tr("Migration"), + tr("The source directory is empty. No data to migrate.")); + return; + } + + QString message; + if (dest_has_content) { + message = tr("Data exists in both the old and new locations.\n\n" + "Old: %1\n" + "New: %2\n\n" + "Would you like to migrate the data from the old location?\n" + "WARNING: This will overwrite any data in the new location!") + .arg(from) + .arg(dest); + } else { + message = tr("Would you like to migrate your data to the new location?\n\n" + "From: %1\n" + "To: %2") + .arg(from) + .arg(dest); + } + + QMessageBox::StandardButton reply = + QMessageBox::question(parent, tr("Migrate Save Data"), message, + QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); + + if (reply != QMessageBox::Yes) { + return; + } + + QProgressDialog progress(tr("Migrating save data..."), tr("Cancel"), 0, 0, this); + progress.setWindowModality(Qt::WindowModal); + progress.setMinimumDuration(0); + progress.show(); + + if (!dest_exists) { + if (!fs::create_directories(dest.toStdString(), ec)) { + progress.close(); + QMessageBox::warning(this, tr("Migration Failed"), + tr("Failed to create destination directory:\n%1") + .arg(QString::fromStdString(ec.message()))); + return; + } + } + + fs::copy(from.toStdString(), dest.toStdString(), + fs::copy_options::recursive | fs::copy_options::overwrite_existing, ec); + + progress.close(); + + if (ec) { + QMessageBox::warning( + this, tr("Migration Failed"), + tr("Failed to migrate data:\n%1").arg(QString::fromStdString(ec.message()))); + return; + } + + QMessageBox::StandardButton deleteReply = + QMessageBox::question(this, tr("Migration Complete"), + tr("Data has been migrated successfully.\n\n" + "Would you like to delete the old data?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (deleteReply == QMessageBox::Yes) { + std::error_code delete_ec; + fs::remove_all(from.toStdString(), delete_ec); + if (delete_ec) { + QMessageBox::warning(this, tr("Deletion Failed"), + tr("Failed to delete old data:\n%1") + .arg(QString::fromStdString(delete_ec.message()))); + } + } +} + void ConfigureStorage::RetranslateUI() { ui->retranslateUi(this); } diff --git a/src/citra_qt/configuration/configure_storage.h b/src/citra_qt/configuration/configure_storage.h index 48850fbc2..b0eee97ca 100644 --- a/src/citra_qt/configuration/configure_storage.h +++ b/src/citra_qt/configuration/configure_storage.h @@ -1,10 +1,11 @@ -// Copyright 2021 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #pragma once #include +#include #include namespace Ui { @@ -19,9 +20,13 @@ public: ~ConfigureStorage() override; void ApplyConfiguration(); + + void MigrateFolder(const QString& from, const QString& dest); + void RetranslateUI(); void SetConfiguration(); std::unique_ptr ui; bool is_powered_on; + QWidget* parent; }; diff --git a/src/citra_qt/configuration/configure_storage.ui b/src/citra_qt/configuration/configure_storage.ui index 5cfd0c449..aec77cd56 100644 --- a/src/citra_qt/configuration/configure_storage.ui +++ b/src/citra_qt/configuration/configure_storage.ui @@ -79,13 +79,6 @@ - - - - NOTE: This does not move the contents of the previous directory to the new one. - - - @@ -143,14 +136,7 @@ - - - - - NOTE: This does not move the contents of the previous directory to the new one. - - - + > From 4e6ff317ddd88aaaafaab6540c7223c876bacd57 Mon Sep 17 00:00:00 2001 From: whyydk <254117058+whyydk@users.noreply.github.com> Date: Tue, 31 Mar 2026 12:52:23 +0200 Subject: [PATCH 2/5] Remove the message --- src/citra_qt/configuration/configure_storage.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/citra_qt/configuration/configure_storage.cpp b/src/citra_qt/configuration/configure_storage.cpp index 52dcc025a..70d5e06fe 100644 --- a/src/citra_qt/configuration/configure_storage.cpp +++ b/src/citra_qt/configuration/configure_storage.cpp @@ -128,8 +128,6 @@ void ConfigureStorage::MigrateFolder(const QString& from, const QString& dest) { } if (!source_has_content) { - QMessageBox::information(this, tr("Migration"), - tr("The source directory is empty. No data to migrate.")); return; } From 3a857f633fc367c2b7ae2a820dd6af72b3a6a823 Mon Sep 17 00:00:00 2001 From: whyydk <254117058+whyydk@users.noreply.github.com> Date: Wed, 1 Apr 2026 11:50:26 +0200 Subject: [PATCH 3/5] fileUtil: Add IsEmptyDir function --- src/common/file_util.cpp | 18 ++++++++++++++++++ src/common/file_util.h | 3 +++ 2 files changed, 21 insertions(+) diff --git a/src/common/file_util.cpp b/src/common/file_util.cpp index 52f406226..26bc50c2d 100644 --- a/src/common/file_util.cpp +++ b/src/common/file_util.cpp @@ -155,6 +155,24 @@ static void StripTailDirSlashes(std::string& fname) { fname.resize(i); } +bool IsEmptyDir(const std::string& folder_path) { + if (!IsDirectory(folder_path)) { + return false; + } + + bool has_entries = false; + + ForeachDirectoryEntry(nullptr, folder_path, + [&has_entries]([[maybe_unused]] u64* num_entries_out, + [[maybe_unused]] const std::string& directory, + [[maybe_unused]] const std::string& virtual_name) -> bool { + has_entries = true; + return false; + }); + + return !has_entries; +} + bool Exists(const std::string& filename) { std::string copy(filename); StripTailDirSlashes(copy); diff --git a/src/common/file_util.h b/src/common/file_util.h index 56dbeea93..2fd81265e 100644 --- a/src/common/file_util.h +++ b/src/common/file_util.h @@ -121,6 +121,9 @@ private: friend class boost::serialization::access; }; +// Check if a folder is empty +[[nodiscard]] bool IsEmptyDir(const std::string& folder_path); + // Returns true if file filename exists [[nodiscard]] bool Exists(const std::string& filename); From ffcc15d946b1101c65101de5d7027a1d352bb092 Mon Sep 17 00:00:00 2001 From: whyydk <254117058+whyydk@users.noreply.github.com> Date: Wed, 1 Apr 2026 11:52:11 +0200 Subject: [PATCH 4/5] Use FileUtil for folder migration --- .../configuration/configure_storage.cpp | 36 +++++-------------- 1 file changed, 9 insertions(+), 27 deletions(-) diff --git a/src/citra_qt/configuration/configure_storage.cpp b/src/citra_qt/configuration/configure_storage.cpp index 70d5e06fe..faa0ca6c7 100644 --- a/src/citra_qt/configuration/configure_storage.cpp +++ b/src/citra_qt/configuration/configure_storage.cpp @@ -107,24 +107,18 @@ void ConfigureStorage::ApplyConfiguration() { } void ConfigureStorage::MigrateFolder(const QString& from, const QString& dest) { - namespace fs = std::filesystem; - - std::error_code ec; - - bool source_exists = fs::exists(from.toStdString(), ec); - bool dest_exists = fs::exists(dest.toStdString(), ec); + bool source_exists = FileUtil::Exists(from.toStdString()); + bool dest_exists = FileUtil::Exists(dest.toStdString()); bool dest_has_content = false; bool source_has_content = false; if (source_exists) { - bool source_empty = fs::is_empty(from.toStdString(), ec); - source_has_content = !ec && !source_empty; + source_has_content = !FileUtil::IsEmptyDir(from.toStdString()); } if (dest_exists) { - bool dest_empty = fs::is_empty(dest.toStdString(), ec); - dest_has_content = !ec && !dest_empty; + dest_has_content = !FileUtil::IsEmptyDir(dest.toStdString()); } if (!source_has_content) { @@ -162,27 +156,18 @@ void ConfigureStorage::MigrateFolder(const QString& from, const QString& dest) { progress.show(); if (!dest_exists) { - if (!fs::create_directories(dest.toStdString(), ec)) { + if (!FileUtil::CreateFullPath(dest.toStdString())) { progress.close(); QMessageBox::warning(this, tr("Migration Failed"), - tr("Failed to create destination directory:\n%1") - .arg(QString::fromStdString(ec.message()))); + tr("Failed to create destination directory.")); return; } } - fs::copy(from.toStdString(), dest.toStdString(), - fs::copy_options::recursive | fs::copy_options::overwrite_existing, ec); + FileUtil::CopyDir(from.toStdString() + "/", dest.toStdString() + "/"); progress.close(); - if (ec) { - QMessageBox::warning( - this, tr("Migration Failed"), - tr("Failed to migrate data:\n%1").arg(QString::fromStdString(ec.message()))); - return; - } - QMessageBox::StandardButton deleteReply = QMessageBox::question(this, tr("Migration Complete"), tr("Data has been migrated successfully.\n\n" @@ -190,12 +175,9 @@ void ConfigureStorage::MigrateFolder(const QString& from, const QString& dest) { QMessageBox::Yes | QMessageBox::No, QMessageBox::No); if (deleteReply == QMessageBox::Yes) { - std::error_code delete_ec; - fs::remove_all(from.toStdString(), delete_ec); - if (delete_ec) { + if (!FileUtil::DeleteDirRecursively(from.toStdString())) { QMessageBox::warning(this, tr("Deletion Failed"), - tr("Failed to delete old data:\n%1") - .arg(QString::fromStdString(delete_ec.message()))); + tr("Failed to delete old data.")); } } } From 6d42572c74660f28d116b48bf92aba828954b69d Mon Sep 17 00:00:00 2001 From: whyydk <254117058+whyydk@users.noreply.github.com> Date: Wed, 1 Apr 2026 12:50:33 +0200 Subject: [PATCH 5/5] clang format --- src/citra_qt/configuration/configure_storage.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/citra_qt/configuration/configure_storage.cpp b/src/citra_qt/configuration/configure_storage.cpp index faa0ca6c7..2bf1c85ca 100644 --- a/src/citra_qt/configuration/configure_storage.cpp +++ b/src/citra_qt/configuration/configure_storage.cpp @@ -176,8 +176,7 @@ void ConfigureStorage::MigrateFolder(const QString& from, const QString& dest) { if (deleteReply == QMessageBox::Yes) { if (!FileUtil::DeleteDirRecursively(from.toStdString())) { - QMessageBox::warning(this, tr("Deletion Failed"), - tr("Failed to delete old data.")); + QMessageBox::warning(this, tr("Deletion Failed"), tr("Failed to delete old data.")); } } }