phase 5: transfer service
This commit is contained in:
@@ -0,0 +1,333 @@
|
||||
/*
|
||||
* This file is part of Checkpoint
|
||||
* Copyright (C) 2017-2021 Bernardo Giordano, FlagBrew
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* Additional Terms 7.b and 7.c of GPLv3 apply to this file:
|
||||
* * Requiring preservation of specified reasonable legal notices or
|
||||
* author attributions in that material or in the Appropriate Legal
|
||||
* Notices displayed by works containing it.
|
||||
* * Prohibiting misrepresentation of the origin of that material,
|
||||
* or requiring that modified versions of such material be marked in
|
||||
* reasonable ways as different from the original version.
|
||||
*/
|
||||
|
||||
#include <nxst/infra/fs/io.hpp>
|
||||
#include <nxst/app/main.hpp>
|
||||
#include <nxst/infra/sys/logger.hpp>
|
||||
|
||||
bool io::fileExists(const std::string& path)
|
||||
{
|
||||
struct stat buffer;
|
||||
return (stat(path.c_str(), &buffer) == 0);
|
||||
}
|
||||
|
||||
void io::copyFile(const std::string& srcPath, const std::string& dstPath)
|
||||
{
|
||||
g_isTransferringFile = true;
|
||||
|
||||
FILE* src = fopen(srcPath.c_str(), "rb");
|
||||
if (src == NULL) {
|
||||
Logger::getInstance().log(Logger::ERROR, "Failed to open source file " + srcPath + " during copy with errno %d. Skipping...", errno);
|
||||
return;
|
||||
}
|
||||
FILE* dst = fopen(dstPath.c_str(), "wb");
|
||||
if (dst == NULL) {
|
||||
Logger::getInstance().log(Logger::ERROR, "Failed to open destination file " + dstPath + " during copy with errno " + std::to_string(errno) + ". Skipping...");
|
||||
fclose(src);
|
||||
return;
|
||||
}
|
||||
|
||||
fseek(src, 0, SEEK_END);
|
||||
u64 sz = ftell(src);
|
||||
rewind(src);
|
||||
|
||||
u8* buf = new u8[BUFFER_SIZE];
|
||||
u64 offset = 0;
|
||||
|
||||
size_t slashpos = srcPath.rfind("/");
|
||||
g_currentFile = srcPath.substr(slashpos + 1, srcPath.length() - slashpos - 1);
|
||||
|
||||
while (offset < sz) {
|
||||
u32 count = fread((char*)buf, 1, BUFFER_SIZE, src);
|
||||
if (count == 0) {
|
||||
Logger::getInstance().log(Logger::ERROR, "fread returned 0 for file {} at offset {}/{} with errno {}. Aborting copy.", srcPath, offset, sz, errno);
|
||||
break;
|
||||
}
|
||||
fwrite((char*)buf, 1, count, dst);
|
||||
offset += count;
|
||||
}
|
||||
|
||||
delete[] buf;
|
||||
fclose(src);
|
||||
fclose(dst);
|
||||
|
||||
if (dstPath.rfind("save:/", 0) == 0) {
|
||||
fsdevCommitDevice("save");
|
||||
}
|
||||
|
||||
g_isTransferringFile = false;
|
||||
}
|
||||
|
||||
Result io::copyDirectory(const std::string& srcPath, const std::string& dstPath)
|
||||
{
|
||||
Result res = 0;
|
||||
bool quit = false;
|
||||
Directory items(srcPath);
|
||||
|
||||
if (!items.good()) {
|
||||
return items.error();
|
||||
}
|
||||
|
||||
for (size_t i = 0, sz = items.size(); i < sz && !quit; i++) {
|
||||
std::string newsrc = srcPath + items.entry(i);
|
||||
std::string newdst = dstPath + items.entry(i);
|
||||
|
||||
if (items.folder(i)) {
|
||||
res = io::createDirectory(newdst);
|
||||
if (R_SUCCEEDED(res)) {
|
||||
newsrc += "/";
|
||||
newdst += "/";
|
||||
res = io::copyDirectory(newsrc, newdst);
|
||||
}
|
||||
else {
|
||||
quit = true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
io::copyFile(newsrc, newdst);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
Result io::createDirectory(const std::string& path)
|
||||
{
|
||||
mkdir(path.c_str(), 0777);
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool io::directoryExists(const std::string& path)
|
||||
{
|
||||
struct stat sb;
|
||||
return (stat(path.c_str(), &sb) == 0 && S_ISDIR(sb.st_mode));
|
||||
}
|
||||
|
||||
Result io::deleteFolderRecursively(const std::string& path)
|
||||
{
|
||||
Directory dir(path);
|
||||
if (!dir.good()) {
|
||||
return dir.error();
|
||||
}
|
||||
|
||||
for (size_t i = 0, sz = dir.size(); i < sz; i++) {
|
||||
if (dir.folder(i)) {
|
||||
std::string newpath = path + "/" + dir.entry(i) + "/";
|
||||
deleteFolderRecursively(newpath);
|
||||
newpath = path + dir.entry(i);
|
||||
rmdir(newpath.c_str());
|
||||
}
|
||||
else {
|
||||
std::string newpath = path + dir.entry(i);
|
||||
std::remove(newpath.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
rmdir(path.c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::tuple<bool, Result, std::string> io::backup(size_t index, AccountUid uid)
|
||||
{
|
||||
Result res = 0;
|
||||
std::tuple<bool, Result, std::string> ret = std::make_tuple(false, -1, "");
|
||||
Title title;
|
||||
getTitle(title, uid, index);
|
||||
|
||||
Logger::getInstance().log(Logger::INFO, "Started backup of %s. Title id: 0x%016lX; User id: 0x%lX%lX.", title.name().c_str(), title.id(),
|
||||
title.userId().uid[1], title.userId().uid[0]);
|
||||
|
||||
FsFileSystem fileSystem;
|
||||
res = FileSystem::mount(&fileSystem, title.id(), title.userId());
|
||||
if (R_SUCCEEDED(res)) {
|
||||
int rc = FileSystem::mount(fileSystem);
|
||||
if (rc == -1) {
|
||||
fsFsClose(&fileSystem);
|
||||
FileSystem::unmount();
|
||||
Logger::getInstance().log(Logger::ERROR, "Failed to mount filesystem during backup. Title id: 0x%016lX; User id: 0x%lX%lX.", title.id(),
|
||||
title.userId().uid[1], title.userId().uid[0]);
|
||||
return std::make_tuple(false, -2, "Failed to mount save.");
|
||||
}
|
||||
}
|
||||
else {
|
||||
Logger::getInstance().log(Logger::ERROR,
|
||||
"Failed to mount filesystem during backup with result 0x%08lX. Title id: 0x%016lX; User id: 0x%lX%lX.", res, title.id(),
|
||||
title.userId().uid[1], title.userId().uid[0]);
|
||||
return std::make_tuple(false, res, "Failed to mount save.");
|
||||
}
|
||||
|
||||
std::string suggestion = StringUtils::removeNotAscii(StringUtils::removeAccents(Account::username(title.userId())));
|
||||
|
||||
io::createDirectory(title.path());
|
||||
std::string dstPath = title.path() + "/" + suggestion;
|
||||
|
||||
// Write to a temp dir first; rename on success so the existing backup
|
||||
// is never destroyed if the copy is interrupted mid-way.
|
||||
std::string tmpPath = dstPath + ".tmp";
|
||||
if (io::directoryExists(tmpPath)) {
|
||||
io::deleteFolderRecursively((tmpPath + "/").c_str());
|
||||
}
|
||||
io::createDirectory(tmpPath);
|
||||
res = io::copyDirectory("save:/", tmpPath + "/");
|
||||
if (R_FAILED(res)) {
|
||||
FileSystem::unmount();
|
||||
io::deleteFolderRecursively((tmpPath + "/").c_str());
|
||||
Logger::getInstance().log(Logger::ERROR, "Failed to copy directory to " + tmpPath + " with result 0x%08lX.", res);
|
||||
return std::make_tuple(false, res, "Failed to backup save.");
|
||||
}
|
||||
|
||||
// Swap: delete old backup only after new one is fully written.
|
||||
if (io::directoryExists(dstPath)) {
|
||||
io::deleteFolderRecursively((dstPath + "/").c_str());
|
||||
}
|
||||
if (rename(tmpPath.c_str(), dstPath.c_str()) != 0) {
|
||||
FileSystem::unmount();
|
||||
Logger::getInstance().log(Logger::ERROR, "Failed to rename temp backup to " + dstPath);
|
||||
return std::make_tuple(false, (Result)-1, "Failed to finalise backup.");
|
||||
}
|
||||
|
||||
refreshDirectories(title.id());
|
||||
|
||||
FileSystem::unmount();
|
||||
|
||||
ret = std::make_tuple(true, 0, dstPath);
|
||||
Logger::getInstance().log(Logger::INFO, "Backup succeeded.");
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::tuple<bool, Result, std::string> io::restore(size_t index, AccountUid uid, size_t cellIndex, const std::string& nameFromCell)
|
||||
{
|
||||
Result res = 0;
|
||||
std::tuple<bool, Result, std::string> ret = std::make_tuple(false, -1, "");
|
||||
Title title;
|
||||
getTitle(title, uid, index);
|
||||
|
||||
Logger::getInstance().log(Logger::INFO, "Started restore of %s. Title id: 0x%016lX; User id: 0x%lX%lX.", title.name().c_str(), title.id(),
|
||||
title.userId().uid[1], title.userId().uid[0]);
|
||||
|
||||
// If save data does not yet exist (game was never launched), create it via NACP.
|
||||
// fsCreateSaveDataFileSystem returns an error if the save already exists — this is expected.
|
||||
{
|
||||
NsApplicationControlData* nsacd = (NsApplicationControlData*)malloc(sizeof(NsApplicationControlData));
|
||||
if (nsacd != NULL) {
|
||||
memset(nsacd, 0, sizeof(NsApplicationControlData));
|
||||
size_t outsize = 0;
|
||||
if (R_SUCCEEDED(nsGetApplicationControlData(NsApplicationControlSource_Storage, title.id(), nsacd, sizeof(NsApplicationControlData), &outsize))) {
|
||||
static const FsSaveDataMetaInfo meta = {.size = 0x40060, .type = FsSaveDataMetaType_Thumbnail};
|
||||
|
||||
FsSaveDataAttribute attr = {};
|
||||
attr.application_id = title.id();
|
||||
attr.uid = uid;
|
||||
attr.save_data_type = FsSaveDataType_Account;
|
||||
attr.save_data_rank = FsSaveDataRank_Primary;
|
||||
|
||||
FsSaveDataCreationInfo createInfo = {};
|
||||
createInfo.save_data_size = (s64)nsacd->nacp.user_account_save_data_size;
|
||||
createInfo.journal_size = (s64)nsacd->nacp.user_account_save_data_journal_size;
|
||||
createInfo.available_size = 0x4000;
|
||||
createInfo.owner_id = nsacd->nacp.save_data_owner_id;
|
||||
createInfo.save_data_space_id = FsSaveDataSpaceId_User;
|
||||
|
||||
fsCreateSaveDataFileSystem(&attr, &createInfo, &meta);
|
||||
}
|
||||
free(nsacd);
|
||||
}
|
||||
}
|
||||
|
||||
FsFileSystem fileSystem;
|
||||
res = FileSystem::mount(&fileSystem, title.id(), uid);
|
||||
if (R_SUCCEEDED(res)) {
|
||||
int rc = FileSystem::mount(fileSystem);
|
||||
if (rc == -1) {
|
||||
fsFsClose(&fileSystem);
|
||||
FileSystem::unmount();
|
||||
Logger::getInstance().log(Logger::ERROR, "Failed to mount filesystem during restore. Title id: 0x%016lX; User id: 0x%lX%lX.", title.id(),
|
||||
uid.uid[1], uid.uid[0]);
|
||||
return std::make_tuple(false, -2, "Failed to mount save.");
|
||||
}
|
||||
}
|
||||
else {
|
||||
Logger::getInstance().log(Logger::ERROR,
|
||||
"Failed to mount filesystem during restore with result 0x%08lX. Title id: 0x%016lX; User id: 0x%lX%lX.", res, title.id(),
|
||||
uid.uid[1], uid.uid[0]);
|
||||
return std::make_tuple(false, res, "Failed to mount save.");
|
||||
}
|
||||
|
||||
std::string suggestion = StringUtils::removeNotAscii(StringUtils::removeAccents(Account::username(uid)));
|
||||
std::string srcPath = title.path() + "/" + suggestion + "/";
|
||||
std::string dstPath = "save:/";
|
||||
|
||||
// Validate source exists and is non-empty before touching live save data.
|
||||
{
|
||||
Directory srcCheck(srcPath);
|
||||
if (!srcCheck.good() || srcCheck.size() == 0) {
|
||||
FileSystem::unmount();
|
||||
Logger::getInstance().log(Logger::ERROR, "Restore source is empty or missing: " + srcPath);
|
||||
return std::make_tuple(false, (Result)-1, "Restore source is empty or missing.");
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
Directory saveRoot(dstPath);
|
||||
for (size_t i = 0, sz = saveRoot.size(); i < sz; i++) {
|
||||
if (saveRoot.folder(i)) {
|
||||
io::deleteFolderRecursively((dstPath + saveRoot.entry(i) + "/").c_str());
|
||||
rmdir((dstPath + saveRoot.entry(i)).c_str());
|
||||
} else {
|
||||
std::remove((dstPath + saveRoot.entry(i)).c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res = fsdevCommitDevice("save");
|
||||
if (R_FAILED(res)) {
|
||||
FileSystem::unmount();
|
||||
Logger::getInstance().log(Logger::ERROR, "Failed to commit save after clearing with result 0x%08lX.", res);
|
||||
return std::make_tuple(false, res, "Failed to commit save after delete.");
|
||||
}
|
||||
|
||||
res = io::copyDirectory(srcPath, dstPath);
|
||||
if (R_FAILED(res)) {
|
||||
FileSystem::unmount();
|
||||
Logger::getInstance().log(Logger::ERROR, "Failed to copy directory " + srcPath + " to " + dstPath + " with result 0x%08lX. Skipping...", res);
|
||||
return std::make_tuple(false, res, "Failed to restore save.");
|
||||
}
|
||||
|
||||
res = fsdevCommitDevice("save");
|
||||
if (R_FAILED(res)) {
|
||||
Logger::getInstance().log(Logger::ERROR, "Failed to commit save with result 0x%08lX.", res);
|
||||
return std::make_tuple(false, res, "Failed to commit to save device.");
|
||||
}
|
||||
else {
|
||||
blinkLed(4);
|
||||
ret = std::make_tuple(true, 0, nameFromCell + "\nhas been restored successfully.");
|
||||
}
|
||||
|
||||
FileSystem::unmount();
|
||||
|
||||
Logger::getInstance().log(Logger::INFO, "Restore succeeded.");
|
||||
return ret;
|
||||
}
|
||||
Reference in New Issue
Block a user