#include "Common/File/Path.h" #include "Common/File/FileUtil.h" #include "Common/File/DirListing.h" #include "Common/Log.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 &dirSuffixes, std::vector &fileSuffixes, MoveProgressReporter &progressReporter) { std::vector 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 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, "About to move PSP data from '%s' to '%s'", fileSrc.c_str(), fileDest.c_str()); bool result = File::MoveIfFast(fileSrc, fileDest); if (!result) { // TODO: Should we try to move back anything that succeeded before this one? return false; } } return true; } std::string MoveProgressReporter::Format() { std::string str; { std::lock_guard guard(mutex_); if (max_ > 0) { str = StringFromFormat("(%d/%d) ", count_, max_); } else if (max_ < 0) { str = StringFromFormat("(%d) ", count_); } 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()); // 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"); progressReporter.SetProgress(ms->T("Done!")); return new MoveResult{ true, "" }; } // 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 fileSuffixesToMove; std::vector 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. std::string error = "Failed to read old directory"; INFO_LOG(Log::System, "%s", error.c_str()); progressReporter.SetProgress(ms->T(error.c_str())); return new MoveResult{ false, error }; } bool dryRun = false; // Useful for debugging. size_t failedFiles = 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::Copy(from, to)) { ERROR_LOG(Log::System, "Failed to copy file '%s' to '%s'", from.c_str(), to.c_str()); failedFiles++; // Should probably just bail? } else { INFO_LOG(Log::System, "Copied file '%s' to '%s'", from.c_str(), to.c_str()); } } } if (failedFiles) { return new MoveResult{ false, "", failedFiles }; } // 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 (fileSuffix.fileSize != info.size) { ERROR_LOG(Log::System, "Mismatched size in target file %s. Verification failed.", fileSuffix.suffix.c_str()); ok = false; failedFiles++; break; } } if (!ok) { return new MoveResult{ false, "", failedFiles }; } INFO_LOG(Log::System, "Verification complete"); // Delete all the old, now hopefully empty, directories. // Hopefully DeleteDir actually fails if it contains a file... 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 }; }