Files
Henrik RydgårdandClaude Opus 5.5 1b60f76e58 Translate error messages once, from keys the source names
GameManager translated install errors when storing them, and the install
screens then looked the translated text up again as a key. It now stores the
category and key, and translates when asked. The raw ISO install error gets
the existing "Installation failed" string.

Some other errors went around as strings that were then used as keys:
- A missing savestate file now has its own error code, ERROR_FILE_MISSING,
  instead of a key in the failure reason.
- The PKG reader's reasons are technical English, so they're now shown
  untranslated under the translated "This PKG file isn't a PSP game update".
- The netconf dialog looked up the channel number. Only "Auto" is translated
  now, using the WLAN Channel setting's string.
- Adds the missing "Failed to read old directory" for memstick moves.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-26 17:04:20 +01:00

305 lines
11 KiB
C++

#include <algorithm>
#include <vector>
#include "Common/File/Path.h"
#include "Common/File/FileUtil.h"
#include "Common/File/DirListing.h"
#include "Common/System/System.h"
#include "Common/Log.h"
#include "Common/TimeUtil.h"
#include "Common/StringUtils.h"
#include "Common/Data/Text/I18n.h"
#include "Common/Data/Text/Parsers.h"
#include "Core/Util/MemStick.h"
#include "Core/Config.h"
#include "Core/System.h"
#include "Core/Reporting.h"
bool FolderSeemsToBeUsed(const Path &newMemstickFolder) {
// Inspect the potential new folder, quickly.
if (File::Exists(newMemstickFolder / "PSP/SAVEDATA") || File::Exists(newMemstickFolder / "SAVEDATA")) {
// Does seem likely. We could add more criteria like checking for actual savegames or something.
return true;
} else {
return false;
}
}
bool SwitchMemstickFolderTo(Path newMemstickFolder) {
// Doesn't already exist, create.
// Should this ever happen?
if (newMemstickFolder.Type() == PathType::NATIVE) {
if (!File::Exists(newMemstickFolder)) {
File::CreateFullPath(newMemstickFolder);
}
Path testWriteFile = newMemstickFolder / ".write_verify_file";
if (!File::WriteDataToFile(true, "1", 1, testWriteFile)) {
return false;
}
File::Delete(testWriteFile);
} else {
// TODO: Do the same but with scoped storage? Not really necessary, right? If it came from a browse
// for folder, we can assume it exists and is writable, barring wacky race conditions like the user
// being connected by USB and deleting it.
}
Path memStickDirFile = g_Config.internalDataDirectory / "memstick_dir.txt";
#if PPSSPP_PLATFORM(UWP)
File::Delete(memStickDirFile);
if (newMemstickFolder != g_Config.internalDataDirectory) {
#endif
std::string str = newMemstickFolder.ToString();
if (!File::WriteDataToFile(true, str.c_str(), str.size(), memStickDirFile)) {
ERROR_LOG(Log::System, "Failed to write memstick path '%s' to '%s'", newMemstickFolder.c_str(), memStickDirFile.c_str());
// Not sure what to do if this file can't be written. Disk full?
}
#if PPSSPP_PLATFORM(UWP)
}
#endif
// Save so the settings, at least, are transferred.
g_Config.memStickDirectory = newMemstickFolder;
g_Config.SetSearchPath(GetSysDirectory(DIRECTORY_SYSTEM));
g_Config.UpdateIniLocation();
return true;
}
// Keep the size with the file, so we can skip overly large ones in the move.
// The user will have to take care of them afterwards, it'll just take too long probably.
struct FileSuffix {
std::string suffix;
u64 fileSize;
};
static bool ListFileSuffixesRecursively(const Path &root, const Path &folder, std::vector<std::string> &dirSuffixes, std::vector<FileSuffix> &fileSuffixes, MoveProgressReporter &progressReporter) {
std::vector<File::FileInfo> files;
if (!File::GetFilesInDir(folder, &files)) {
return false;
}
for (auto &file : files) {
if (file.isDirectory) {
std::string dirSuffix;
if (root.ComputePathTo(file.fullName, dirSuffix)) {
if (!dirSuffix.empty()) {
dirSuffixes.push_back(dirSuffix);
ListFileSuffixesRecursively(root, folder / file.name, dirSuffixes, fileSuffixes, progressReporter);
progressReporter.SetProgress(file.name, fileSuffixes.size(), (size_t)-1);
}
} else {
ERROR_LOG_REPORT(Log::System, "Failed to compute PathTo from '%s' to '%s'", root.c_str(), folder.c_str());
}
} else {
std::string fileSuffix;
if (root.ComputePathTo(file.fullName, fileSuffix)) {
if (!fileSuffix.empty()) {
fileSuffixes.push_back(FileSuffix{ fileSuffix, file.size });
}
}
}
}
return true;
}
bool MoveChildrenFast(const Path &moveSrc, const Path &moveDest, MoveProgressReporter &progressReporter) {
std::vector<File::FileInfo> files;
if (!File::GetFilesInDir(moveSrc, &files)) {
return false;
}
for (size_t i = 0; i < files.size(); i++) {
auto &file = files[i];
// Construct destination path
Path fileSrc = file.fullName;
Path fileDest = moveDest / file.name;
progressReporter.SetProgress(file.name, i, files.size());
INFO_LOG(Log::System, "Fast-moving PSP data from '%s' to '%s' (%d/%d)", fileSrc.c_str(), fileDest.c_str(), (int)i + 1, (int)files.size());
bool result = File::MoveIfFast(fileSrc, fileDest);
if (!result) {
// TODO: Should we try to move back anything that succeeded before this one?
INFO_LOG(Log::System, "Failed to fast-move '%s' to '%s'", fileSrc.c_str(), fileDest.c_str());
return false;
}
}
INFO_LOG(Log::System, "Done with fast-move.");
return true;
}
std::string MoveProgressReporter::Format() {
std::string str;
{
std::lock_guard<std::mutex> guard(mutex_);
if (max_ > 0) {
str = StringFromFormat("(%d/%d) ", count_ + 1, max_);
} else if (max_ < 0) {
str = StringFromFormat("(%d/?) ", count_ + 1);
}
str += progress_;
}
return str;
}
MoveResult *MoveDirectoryContentsSafe(Path moveSrc, Path moveDest, MoveProgressReporter &progressReporter) {
auto ms = GetI18NCategory(I18NCat::MEMSTICK);
if (moveSrc.GetFilename() != "PSP") {
moveSrc = moveSrc / "PSP";
}
if (moveDest.GetFilename() != "PSP") {
moveDest = moveDest / "PSP";
File::CreateDir(moveDest);
}
INFO_LOG(Log::System, "About to move PSP data from '%s' to '%s'", moveSrc.c_str(), moveDest.c_str());
Instant moveStart = Instant::Now();
// First, we try the cheapest and safest way to move: Can we move files directly within the same device?
// We loop through the files/dirs in the source directory and just try to move them, it should work.
if (MoveChildrenFast(moveSrc, moveDest, progressReporter)) {
INFO_LOG(Log::System, "Quick-move succeeded after %0.1f ms", moveStart.ElapsedMs());
progressReporter.SetProgress(StringFromFormat("%s (%0.3f)", T_cstr(I18NCat::DIALOG, "Done!"), moveStart.ElapsedSeconds()));
return new MoveResult{ true, "" };
} else {
INFO_LOG(Log::System, "Quick move denied after %0.1f ms, falling back to slow move.", moveStart.ElapsedMs());
}
// If this doesn't work, we'll fall back on a recursive *copy* (disk space is less of a concern when
// moving from device to device, other than that everything fits on the destination).
// Then we verify the results before we delete the originals.
// Search through recursively, listing the files to move and also summing their sizes.
std::vector<FileSuffix> fileSuffixesToMove;
std::vector<std::string> directorySuffixesToCreate;
// NOTE: It's correct to pass moveSrc twice here, it's to keep the root in the recursion.
if (!ListFileSuffixesRecursively(moveSrc, moveSrc, directorySuffixesToCreate, fileSuffixesToMove, progressReporter)) {
// TODO: Handle failure listing files.
INFO_LOG(Log::System, "Failed to read old directory");
progressReporter.SetProgress(ms->T("Failed to read old directory"));
return new MoveResult{ false, "Failed to read old directory" };
}
bool dryRun = false; // Useful for debugging. Probably want to remove the MoveChildrenFast path above to test it.
size_t failedFileCount = 0;
// We're not moving huge files like ISOs during this process, unless
// they can be directly moved, without rewriting the file.
const uint64_t BIG_FILE_THRESHOLD = 24 * 1024 * 1024;
if (moveSrc.empty()) {
// Shouldn't happen.
return new MoveResult{ true, "", };
}
// Better not interrupt the app while this is happening!
// Create all the necessary directories.
for (size_t i = 0; i < directorySuffixesToCreate.size(); i++) {
const auto &dirSuffix = directorySuffixesToCreate[i];
Path dir = moveDest / dirSuffix;
if (dryRun) {
INFO_LOG(Log::System, "dry run: Would have created dir '%s'", dir.c_str());
} else {
INFO_LOG(Log::System, "Creating dir '%s'", dir.c_str());
progressReporter.SetProgress(dirSuffix);
// Just ignore already-exists errors.
File::CreateDir(dir);
}
}
for (size_t i = 0; i < fileSuffixesToMove.size(); i++) {
const auto &fileSuffix = fileSuffixesToMove[i];
progressReporter.SetProgress(StringFromFormat("%s (%s)", fileSuffix.suffix.c_str(), NiceSizeFormat(fileSuffix.fileSize).c_str()),
i, fileSuffixesToMove.size());
Path from = moveSrc / fileSuffix.suffix;
Path to = moveDest / fileSuffix.suffix;
if (dryRun) {
INFO_LOG(Log::System, "dry run: Would have moved '%s' to '%s' (%d bytes)", from.c_str(), to.c_str(), (int)fileSuffix.fileSize);
} else {
// Remove the "from" prefix from the path.
// We have to drop down to string operations for this.
if (File::Exists(to)) {
WARN_LOG(Log::System, "Target file '%s' already exists. Will be overwritten", to.c_str());
}
if (!File::Copy(from, to)) {
ERROR_LOG(Log::System, "Failed to copy file '%s' to '%s'", from.c_str(), to.c_str());
failedFileCount++;
// Should probably just bail?
} else {
INFO_LOG(Log::System, "Copied file '%s' to '%s' (size: %d)", from.c_str(), to.c_str(), (int)fileSuffix.fileSize);
}
}
}
if (failedFileCount) {
ERROR_LOG(Log::System, "Copy failed (%d files failed)", (int)failedFileCount);
return new MoveResult{ false, "", failedFileCount };
}
INFO_LOG(Log::System, "Done copying. Moving to verification.");
// After the whole move, verify that all the files arrived correctly.
// If there's a single error, we do not delete the source data.
bool ok = true;
for (size_t i = 0; i < fileSuffixesToMove.size(); i++) {
const auto &fileSuffix = fileSuffixesToMove[i];
progressReporter.SetProgress(ms->T("Checking..."), i, fileSuffixesToMove.size());
Path to = moveDest / fileSuffix.suffix;
File::FileInfo info;
if (!File::GetFileInfo(to, &info)) {
ok = false;
break;
}
if (!info.exists) {
ERROR_LOG(Log::System, "File '%s' missing at target location '%s'.", fileSuffix.suffix.c_str(), to.c_str());
ok = false;
failedFileCount++;
break;
}
// More lenient handling for .nomedia files.
if (fileSuffix.fileSize != info.size && !endsWithNoCase(fileSuffix.suffix, ".nomedia")) {
ERROR_LOG(Log::System, "Mismatched size in target file %s (expected %d, was %d bytes). Verification failed.", fileSuffix.suffix.c_str(), (int)fileSuffix.fileSize, (int)info.size);
ok = false;
failedFileCount++;
break;
}
}
if (!ok) {
return new MoveResult{ false, "", failedFileCount };
}
INFO_LOG(Log::System, "Verification complete");
// Delete all the old, now hopefully empty, directories.
// Hopefully DeleteDir actually fails if it contains a file...
// Reverse the order to delete subfolders before parents.
std::reverse(directorySuffixesToCreate.begin(), directorySuffixesToCreate.end());
for (size_t i = 0; i < directorySuffixesToCreate.size(); i++) {
const auto &dirSuffix = directorySuffixesToCreate[i];
Path dir = moveSrc / dirSuffix;
if (dryRun) {
INFO_LOG(Log::System, "dry run: Would have deleted dir '%s'", dir.c_str());
} else {
INFO_LOG(Log::System, "Deleting dir '%s'", dir.c_str());
progressReporter.SetProgress(dirSuffix, i, directorySuffixesToCreate.size());
if (File::Exists(dir)) {
File::DeleteDir(dir);
}
}
}
return new MoveResult{ true, "", 0 };
}