mirror of
https://github.com/hrydgard/ppsspp.git
synced 2024-11-23 13:30:02 +00:00
301 lines
10 KiB
C++
301 lines
10 KiB
C++
#include <algorithm>
|
|
#include <vector>
|
|
|
|
#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<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, "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<std::mutex> 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<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.
|
|
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 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 };
|
|
}
|