From 7515e0334b65a6a5148229ae738865e34c2e3732 Mon Sep 17 00:00:00 2001 From: Nikolai Fedorov Date: Sat, 25 Apr 2026 09:55:34 +0300 Subject: [PATCH] arch codereview --- CLAUDE.md | 10 + Makefile | 4 +- include/TransferOverlay.hpp | 51 ++++ include/TransferState.hpp | 38 +++ include/client.hpp | 6 + include/net/Socket.hpp | 18 ++ include/protocol.hpp | 17 ++ include/server.hpp | 8 +- source/TitlesLayout.cpp | 39 ++- source/client.cpp | 332 ++++++++++-------------- source/io.cpp | 41 ++- source/server.cpp | 502 +++++++++++++++++------------------- 12 files changed, 579 insertions(+), 487 deletions(-) create mode 100644 include/TransferOverlay.hpp create mode 100644 include/TransferState.hpp create mode 100644 include/net/Socket.hpp create mode 100644 include/protocol.hpp diff --git a/CLAUDE.md b/CLAUDE.md index daced9b..6297c6e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -62,4 +62,14 @@ Strong success criteria let you loop independently. Weak criteria ("make it work --- +## Project: NXST — Nintendo Switch Save Transfer + +**Terminology (inverted from typical client/server):** +- **server** (`source/server.cpp`) — the Switch that **receives** files (listens on TCP, accepts incoming connection) +- **client** (`source/client.cpp`) — the Switch that **sends** files (discovers server via multicast, connects, transmits) + +UI buttons map as: "Transfer" → runs client (sends), "Receive" → runs server (receives). + +--- + **These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes. diff --git a/Makefile b/Makefile index 730fabb..41f8452 100644 --- a/Makefile +++ b/Makefile @@ -32,8 +32,8 @@ include $(DEVKITPRO)/libnx/switch_rules #--------------------------------------------------------------------------------- TARGET := NXST BUILD := build -SOURCES := source source/fs lib/Plutonium/source -INCLUDES := include include/fs lib/Plutonium/include +SOURCES := source lib/Plutonium/source +INCLUDES := include include/net lib/Plutonium/include EXEFS_SRC := exefs_src APP_TITLE := NXST APP_AUTHOR := DragonSpirit diff --git a/include/TransferOverlay.hpp b/include/TransferOverlay.hpp new file mode 100644 index 0000000..ea0c6fa --- /dev/null +++ b/include/TransferOverlay.hpp @@ -0,0 +1,51 @@ +#pragma once +#include + +namespace ui { + + class TransferOverlay : public pu::ui::Overlay { + private: + pu::ui::elm::TextBlock::Ref titleText; + pu::ui::elm::TextBlock::Ref statusText; + pu::ui::elm::ProgressBar::Ref progressBar; + pu::ui::elm::TextBlock::Ref hintText; + + public: + static constexpr int OvlX = 200; + static constexpr int OvlY = 240; + static constexpr int OvlW = 880; + static constexpr int OvlH = 240; + + TransferOverlay(const std::string &title) + : Overlay(OvlX, OvlY, OvlW, OvlH, pu::ui::Color(30, 30, 30, 220)) + { + titleText = pu::ui::elm::TextBlock::New(OvlX + 40, OvlY + 30, title); + titleText->SetColor(pu::ui::Color(255, 255, 255, 255)); + + statusText = pu::ui::elm::TextBlock::New(OvlX + 40, OvlY + 90, ""); + statusText->SetColor(pu::ui::Color(180, 180, 180, 255)); + + progressBar = pu::ui::elm::ProgressBar::New(OvlX + 40, OvlY + 140, OvlW - 80, 20, 100.0); + progressBar->SetProgressColor(pu::ui::Color(100, 180, 255, 255)); + progressBar->SetBackgroundColor(pu::ui::Color(70, 70, 70, 255)); + + hintText = pu::ui::elm::TextBlock::New(OvlX + 40, OvlY + 195, "Press B to cancel"); + hintText->SetColor(pu::ui::Color(130, 130, 130, 255)); + + this->Add(titleText); + this->Add(statusText); + this->Add(progressBar); + this->Add(hintText); + } + PU_SMART_CTOR(TransferOverlay) + + void SetStatus(const std::string &status) { + statusText->SetText(status); + } + + void SetProgress(double val) { + progressBar->SetProgress(val); + } + }; + +} diff --git a/include/TransferState.hpp b/include/TransferState.hpp new file mode 100644 index 0000000..36a71bc --- /dev/null +++ b/include/TransferState.hpp @@ -0,0 +1,38 @@ +#pragma once +#include +#include +#include + +struct TransferState { + std::atomic done{false}; + std::atomic cancelled{false}; + std::atomic bytes_done{0}; + std::atomic bytes_total{0}; + + std::string status; + mutable std::mutex status_mutex; + + void reset() { + done = false; + cancelled = false; + bytes_done = 0; + bytes_total = 0; + std::lock_guard lock(status_mutex); + status.clear(); + } + + double progress() const { + uint64_t t = bytes_total.load(); + return t ? (double)bytes_done.load() / (double)t * 100.0 : 0.0; + } + + std::string getStatus() const { + std::lock_guard lock(status_mutex); + return status; + } + + void setStatus(const std::string& s) { + std::lock_guard lock(status_mutex); + status = s; + } +}; diff --git a/include/client.hpp b/include/client.hpp index 1377aad..e0b83bf 100644 --- a/include/client.hpp +++ b/include/client.hpp @@ -1,4 +1,10 @@ +#include +#include namespace fs = std::filesystem; using path = fs::path; int transfer_files(path directory); +bool isClientTransferDone(); +void cancelClientTransfer(); +double getClientProgress(); +std::string getClientStatusText(); diff --git a/include/net/Socket.hpp b/include/net/Socket.hpp new file mode 100644 index 0000000..0dc2371 --- /dev/null +++ b/include/net/Socket.hpp @@ -0,0 +1,18 @@ +#pragma once +#include + +struct Socket { + int fd = -1; + + Socket() = default; + explicit Socket(int fd) : fd(fd) {} + ~Socket() { if (fd >= 0) close(fd); } + + Socket(const Socket&) = delete; + Socket& operator=(const Socket&) = delete; + Socket(Socket&& o) : fd(o.fd) { o.fd = -1; } + + operator int() const { return fd; } + bool valid() const { return fd >= 0; } + void release() { fd = -1; } +}; diff --git a/include/protocol.hpp b/include/protocol.hpp new file mode 100644 index 0000000..48cd021 --- /dev/null +++ b/include/protocol.hpp @@ -0,0 +1,17 @@ +#pragma once +#include + +namespace proto { + constexpr uint16_t TCP_PORT = 8080; + constexpr uint16_t MULTICAST_PORT = 8081; + constexpr char MULTICAST_GROUP[] = "239.0.0.1"; + constexpr size_t BUF_SIZE = 65536; + constexpr uint32_t MAX_FILENAME = 4096; + constexpr uint32_t EOF_SENTINEL = 0; + + // Wire layout per file: + // [filename_len : uint32_t LE] — 0 == end-of-stream + // [filename : filename_len bytes] + // [file_size : uint64_t LE] + // [file_data : file_size bytes] +} diff --git a/include/server.hpp b/include/server.hpp index 24e3e88..96e2ee3 100644 --- a/include/server.hpp +++ b/include/server.hpp @@ -1 +1,7 @@ -int startSendingThread(); \ No newline at end of file +#include +int startSendingThread(); +bool isServerTransferDone(); +bool isServerTransferCancelled(); +void cancelServerTransfer(); +double getServerProgress(); +std::string getServerStatusText(); \ No newline at end of file diff --git a/source/TitlesLayout.cpp b/source/TitlesLayout.cpp index 26a2ed7..19f6795 100644 --- a/source/TitlesLayout.cpp +++ b/source/TitlesLayout.cpp @@ -4,6 +4,7 @@ #include #include #include +#include static std::vector accSids, devSids, bcatSids, cacheSids; @@ -44,9 +45,21 @@ namespace ui { auto result = io::backup(index, g_currentUId); if (std::get<0>(result)) { printf("path is %s\n", std::get<2>(result).c_str()); - std::string path = std::get<2>(result); + std::string path = std::get<2>(result); auto directory = std::filesystem::path(path); transfer_files(directory); + auto ovl = TransferOverlay::New("Transferring save data..."); + mainApp->StartOverlay(ovl); + while (!isClientTransferDone()) { + ovl->SetStatus(getClientStatusText()); + ovl->SetProgress(getClientProgress()); + mainApp->CallForRender(); + if (mainApp->GetButtonsDown() & HidNpadButton_B) { + cancelClientTransfer(); + } + svcSleepThread(16666666LL); + } + mainApp->EndOverlay(); } break; } @@ -56,11 +69,25 @@ namespace ui { int result = startSendingThread(); printf("result is %i\n", result); if (result == 0) { - auto restoreResult = io::restore(index, g_currentUId, 0, title.name()); - if (std::get<0>(restoreResult)) { - printf("%s\n", std::get<2>(restoreResult).c_str()); - } else { - printf("Failed to restore with error %s\n", std::get<2>(restoreResult).c_str()); + auto ovl = TransferOverlay::New("Receiving save data..."); + mainApp->StartOverlay(ovl); + while (!isServerTransferDone()) { + ovl->SetStatus(getServerStatusText()); + ovl->SetProgress(getServerProgress()); + mainApp->CallForRender(); + if (mainApp->GetButtonsDown() & HidNpadButton_B) { + cancelServerTransfer(); + } + svcSleepThread(16666666LL); + } + mainApp->EndOverlay(); + if (!isServerTransferCancelled()) { + auto restoreResult = io::restore(index, g_currentUId, 0, title.name()); + if (std::get<0>(restoreResult)) { + printf("%s\n", std::get<2>(restoreResult).c_str()); + } else { + printf("Failed to restore with error %s\n", std::get<2>(restoreResult).c_str()); + } } } diff --git a/source/client.cpp b/source/client.cpp index 9b5b8ba..d737685 100644 --- a/source/client.cpp +++ b/source/client.cpp @@ -1,247 +1,187 @@ -#include #include #include -#include #include #include #include #include -#include #include #include #include #include +#include #ifdef __SWITCH__ #include #include #endif -#define PORT 8080 -#define BUFFER_SIZE 65536 -#define MULTICAST_PORT 8081 -#define MULTICAST_GROUP "239.0.0.1" // Multicast group IP + +#include +#include +#include namespace fs = std::filesystem; using path = fs::path; -class Logger { -public: - inline static const std::string INFO = "[INFO]"; - inline static const std::string DEBUG = "[DEBUG]"; - inline static const std::string ERROR = "[ERROR]"; - inline static const std::string WARN = "[WARN]"; -}; +static TransferState g_client_state; + +bool isClientTransferDone() { return g_client_state.done.load(); } +void cancelClientTransfer() { g_client_state.cancelled.store(true); } +double getClientProgress() { return g_client_state.progress(); } +std::string getClientStatusText() { return g_client_state.getStatus(); } struct ThreadArgs { - int sock; - fs::path directory; + int sock; + fs::path directory; }; static bool send_all(int sock, const void* buf, size_t len) { - size_t sent = 0; - while (sent < len) { - ssize_t n = send(sock, static_cast(buf) + sent, len - sent, 0); - if (n <= 0) return false; - sent += n; - } - return true; -} - -// Отправка строки с учетом её длины -void send_string(int sock, const std::string &str) { - size_t length = str.size(); - send(sock, &length, sizeof(length), 0); - send(sock, str.c_str(), length, 0); -} - -void sendFile(int sock, const fs::path &base_path, const fs::path &filepath) { - std::ifstream infile(filepath, std::ios::binary | std::ios::ate); - if (!infile.is_open()) { - std::cerr << "File not found: " << filepath << std::endl; - return; - } - // std::string relative_path = fs::relative(filepath, base_path).string(); - uint32_t filename_len = filepath.string().size(); - - std::cout << "send filepath length: " << filename_len << std::endl; - - if (send(sock, &filename_len, sizeof(filename_len), 0) == -1) { - std::cerr << "Failed to send filename length" << std::endl; - } - std::cout << "send filepath: " << filepath.string() << std::endl; - - // Send the filename - if (send(sock, filepath.c_str(), filename_len, 0) == - -1) { // Send the filename length - std::cerr << "Failed to send filename" << std::endl; - } - - // Get the size of the file - std::streamsize file_size = infile.tellg(); - infile.seekg(0, std::ios::beg); - char* buffer = new char[BUFFER_SIZE]; - - std::cout << "send filesize: " << file_size << std::endl; - - // Send the file size - if (send(sock, &file_size, sizeof(file_size), 0) == -1) { - std::cerr << "Failed to send file size" << std::endl; - } - - while (file_size > 0) { - infile.read(buffer, BUFFER_SIZE); - std::streamsize count = infile.gcount(); - if (!send_all(sock, buffer, count)) { - std::cerr << "Failed to send file data" << std::endl; - delete[] buffer; - pthread_exit(nullptr); + size_t sent = 0; + while (sent < len) { + ssize_t n = send(sock, static_cast(buf) + sent, len - sent, 0); + if (n <= 0) return false; + sent += n; } - file_size -= count; - } - - std::cout << "File sent successfully: " << filepath << std::endl; - - delete[] buffer; - infile.close(); + return true; } -void *send_files_thread(void *args) { - ThreadArgs *thread_args = static_cast(args); - int sock = thread_args->sock; - fs::path cwd = thread_args->directory; - delete thread_args; - std::cout << "cwd is: " << cwd << std::endl; - - char buffer[BUFFER_SIZE]; - - for (const auto &entry : fs::recursive_directory_iterator(cwd)) { - path path = entry.path(); - std::cout << "path is " << path << std::endl; - if (fs::is_regular_file(path)) { - std::cout << "regular file | path is: " << path << std::endl; - sendFile(sock, path.parent_path(), path); +static bool sendFile(int sock, const fs::path& filepath) { + std::ifstream infile(filepath, std::ios::binary | std::ios::ate); + if (!infile.is_open()) { + std::cerr << "File not found: " << filepath << std::endl; + return false; } - } - close(sock); - pthread_exit(nullptr); + uint32_t filename_len = (uint32_t)filepath.string().size(); + uint64_t file_size = (uint64_t)infile.tellg(); + infile.seekg(0, std::ios::beg); + + std::cout << "Sending: " << filepath << " (" << file_size << " bytes)" << std::endl; + + if (!send_all(sock, &filename_len, sizeof(filename_len))) return false; + if (!send_all(sock, filepath.c_str(), filename_len)) return false; + if (!send_all(sock, &file_size, sizeof(file_size))) return false; + + std::vector buffer(proto::BUF_SIZE); + uint64_t remaining = file_size; + while (remaining > 0) { + size_t to_read = (size_t)std::min(remaining, (uint64_t)proto::BUF_SIZE); + infile.read(buffer.data(), (std::streamsize)to_read); + std::streamsize count = infile.gcount(); + if (count <= 0) break; + if (!send_all(sock, buffer.data(), (size_t)count)) { + std::cerr << "Failed to send data for: " << filepath << std::endl; + return false; + } + g_client_state.bytes_done.fetch_add((uint64_t)count); + remaining -= (uint64_t)count; + } + + return true; } -int find_server(char *server_ip) { - std::cout << Logger::INFO << "Init find_server" << std::endl; - int sockfd; - struct sockaddr_in multicast_addr; +static void* send_files_thread(void* arg) { + ThreadArgs* targs = static_cast(arg); + int sock = targs->sock; + fs::path cwd = targs->directory; + delete targs; - std::cout << Logger::INFO << "Create socket" << std::endl; + for (const auto& entry : fs::recursive_directory_iterator(cwd)) { + if (g_client_state.cancelled.load()) break; + const path& p = entry.path(); + if (fs::is_regular_file(p)) { + g_client_state.setStatus(p.filename().string()); + if (!sendFile(sock, p)) break; + } + } - if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) { - std::cout << Logger::ERROR << "Socket creation error" << std::endl; - return -1; - } + // EOF sentinel — server reads this and exits its receive loop cleanly + uint32_t sentinel = proto::EOF_SENTINEL; + send_all(sock, &sentinel, sizeof(sentinel)); - memset(&multicast_addr, 0, sizeof(multicast_addr)); + g_client_state.setStatus(""); + close(sock); + g_client_state.done.store(true); + return nullptr; +} - multicast_addr.sin_family = AF_INET; - multicast_addr.sin_port = htons(MULTICAST_PORT); - multicast_addr.sin_addr.s_addr = inet_addr(MULTICAST_GROUP); +static int find_server(char* server_ip) { + Socket udp(socket(AF_INET, SOCK_DGRAM, 0)); + if (!udp.valid()) { + std::cerr << "find_server: socket failed" << std::endl; + return -1; + } - std::cout << Logger::INFO << "Send DISCOVER_SERVER" << std::endl; + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_port = htons(proto::MULTICAST_PORT); + addr.sin_addr.s_addr = inet_addr(proto::MULTICAST_GROUP); - const char *multicast_message = "DISCOVER_SERVER"; - if (sendto(sockfd, multicast_message, strlen(multicast_message), 0, - (struct sockaddr *)&multicast_addr, sizeof(multicast_addr)) < 0) { - std::cout << Logger::ERROR << "sendto failed" << std::endl; - close(sockfd); - return -1; - } else { - std::cout << Logger::INFO << "send multicast message success" << std::endl; - } + const char* msg = "DISCOVER_SERVER"; + if (sendto(udp, msg, strlen(msg), 0, (sockaddr*)&addr, sizeof(addr)) < 0) { + std::cerr << "find_server: sendto failed" << std::endl; + return -1; + } - struct sockaddr_in cliaddr; - socklen_t len = sizeof(cliaddr); - char buffer[BUFFER_SIZE]; - - ssize_t n = recvfrom(sockfd, buffer, BUFFER_SIZE, 0, - (struct sockaddr *)&cliaddr, &len); - std::cout << Logger::INFO << "recvfrom n: %i" << n << std::endl; - if (n < 0) { - std::cout << Logger::ERROR << "recvfrom failed" << std::endl; - close(sockfd); - return -1; - } - std::cout << Logger::INFO << "buffer: " << buffer << std::endl; - buffer[n] = '\0'; - if (strcmp(buffer, "SERVER_HERE") == 0) { - std::cout << Logger::INFO << "Server found" << std::endl; - inet_ntop(AF_INET, &cliaddr.sin_addr, server_ip, INET_ADDRSTRLEN); - } else { - std::cout << Logger::ERROR << "Unable to find server, close socket" - << std::endl; - ; - close(sockfd); - return -1; - } - - close(sockfd); - return 0; + sockaddr_in from{}; + socklen_t fromlen = sizeof(from); + char buf[256]; + ssize_t n = recvfrom(udp, buf, sizeof(buf) - 1, 0, (sockaddr*)&from, &fromlen); + if (n < 0) { + std::cerr << "find_server: recvfrom failed" << std::endl; + return -1; + } + buf[n] = '\0'; + if (strcmp(buf, "SERVER_HERE") != 0) { + std::cerr << "find_server: unexpected response: " << buf << std::endl; + return -1; + } + inet_ntop(AF_INET, &from.sin_addr, server_ip, INET_ADDRSTRLEN); + return 0; } int transfer_files(fs::path directory) { - std::cout << Logger::INFO << "Init transfer_files" << std::endl; - char server_ip[INET_ADDRSTRLEN]; - if (find_server(server_ip) != 0) { - std::cout << Logger::ERROR << "Failed to find server" << std::endl; - return -1; - } + char server_ip[INET_ADDRSTRLEN]; + if (find_server(server_ip) != 0) return -1; - int sock = 0; - struct sockaddr_in serv_addr; + Socket tcp(socket(AF_INET, SOCK_STREAM, 0)); + if (!tcp.valid()) return -1; - if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) { - std::cout << Logger::ERROR << "Socket creation error" << std::endl; - return -1; - } + sockaddr_in serv{}; + serv.sin_family = AF_INET; + serv.sin_port = htons(proto::TCP_PORT); + if (inet_pton(AF_INET, server_ip, &serv.sin_addr) <= 0) return -1; + if (connect(tcp, (sockaddr*)&serv, sizeof(serv)) < 0) return -1; - serv_addr.sin_family = AF_INET; - serv_addr.sin_port = htons(PORT); + uint64_t total = 0; + for (const auto& entry : fs::recursive_directory_iterator(directory)) { + if (fs::is_regular_file(entry.path())) + total += fs::file_size(entry.path()); + } - if (inet_pton(AF_INET, server_ip, &serv_addr.sin_addr) <= 0) { - std::cout << Logger::ERROR << "Invalid address / Address not supported" - << std::endl; - return -1; - } + g_client_state.reset(); + g_client_state.bytes_total.store(total); + g_client_state.setStatus("Connecting..."); - if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) { - std::cout << Logger::ERROR << "Connection failed" << std::endl; - return -1; - } + ThreadArgs* args = new ThreadArgs{tcp.fd, directory}; + tcp.release(); // ownership transferred to thread - pthread_t file_thread; - ThreadArgs *thread_args = new ThreadArgs{sock, directory}; - if (pthread_create(&file_thread, nullptr, send_files_thread, - (void *)thread_args) < 0) { - std::cout << Logger::ERROR << "Thread creation failed" << std::endl; - close(sock); - delete thread_args; - return -1; - } else { - std::cout << Logger::INFO << "Wait for file_thread" << std::endl; - pthread_join(file_thread, nullptr); - } - return 0; + pthread_t thread; + if (pthread_create(&thread, nullptr, send_files_thread, args) != 0) { + delete args; + return -1; + } + pthread_detach(thread); + return 0; } -#ifndef __SWITCH__ // for desktop -int main(int argc, char *argv[]) { - if (argc < 1) { - std::cerr << "Usage: " << argv[0] << " " << std::endl; - return 1; - } - - fs::path directory = fs::path(argv[1]); - std::cout << "directory is " << directory << std::endl; - transfer_files(directory); - - return 0; +#ifndef __SWITCH__ +int main(int argc, char* argv[]) { + if (argc < 2) { + std::cerr << "Usage: " << argv[0] << " " << std::endl; + return 1; + } + if (transfer_files(fs::path(argv[1])) != 0) return 1; + while (!isClientTransferDone()) usleep(16000); + return 0; } #endif diff --git a/source/io.cpp b/source/io.cpp index 068c288..f4ca4ef 100644 --- a/source/io.cpp +++ b/source/io.cpp @@ -178,24 +178,31 @@ std::tuple io::backup(size_t index, AccountUid uid) std::string dstPath = title.path() + "/" + suggestion; - if (io::directoryExists(dstPath)) { - int rc = io::deleteFolderRecursively((dstPath + "/").c_str()); - if (rc != 0) { - FileSystem::unmount(); - Logger::getInstance().log(Logger::ERROR, "Failed to recursively delete directory " + dstPath + " with result %d.", rc); - return std::make_tuple(false, (Result)rc, "Failed to delete the existing backup\ndirectory recursively."); - } + // 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(dstPath); - res = io::copyDirectory("save:/", dstPath + "/"); + io::createDirectory(tmpPath); + res = io::copyDirectory("save:/", tmpPath + "/"); if (R_FAILED(res)) { FileSystem::unmount(); - io::deleteFolderRecursively((dstPath + "/").c_str()); - Logger::getInstance().log(Logger::ERROR, "Failed to copy directory " + dstPath + " with result 0x%08lX. Skipping...", res); + 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(); @@ -266,6 +273,16 @@ std::tuple io::restore(size_t index, AccountUid 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++) { diff --git a/source/server.cpp b/source/server.cpp index 66a3110..1f7acd5 100644 --- a/source/server.cpp +++ b/source/server.cpp @@ -1,18 +1,16 @@ #include +#include #include -#include #include -#include -#include #include #include -#include #include #include #include #include #include #include +#include #ifdef __SWITCH__ #include @@ -20,312 +18,276 @@ #include #endif -#define PORT 8080 -#define BUFFER_SIZE 65536 -#define MULTICAST_PORT 8081 -#define MULTICAST_GROUP "239.0.0.1" // Multicast group IP +#include +#include +#include -namespace fs = std::filesystem; +static TransferState g_server_state; +static std::atomic g_server_client_sock{-1}; +static std::atomic g_broadcast_sock{-1}; + +bool isServerTransferDone() { return g_server_state.done.load(); } +bool isServerTransferCancelled() { return g_server_state.cancelled.load(); } +double getServerProgress() { return g_server_state.progress(); } +std::string getServerStatusText() { return g_server_state.getStatus(); } + +void cancelServerTransfer() { + g_server_state.cancelled.store(true); + int sock = g_server_client_sock.load(); + if (sock >= 0) shutdown(sock, SHUT_RDWR); + int bsock = g_broadcast_sock.load(); + if (bsock >= 0) shutdown(bsock, SHUT_RDWR); +} #ifdef __SWITCH__ -std::string replaceUsername(const std::string &path) { - std::string replacedString = StringUtils::removeNotAscii( - StringUtils::removeAccents(Account::username(g_currentUId))); - // Найдём позицию последнего символа '/' - size_t lastSlashPos = path.rfind('/'); - - // Если нет '/', возвращаем исходный путь - if (lastSlashPos == std::string::npos) { - return path; - } - - // Найдём позицию предыдущего символа '/' (начало последней папки) - size_t prevSlashPos = path.rfind('/', lastSlashPos - 1); - - // Если предыдущий '/' не найден, значит путь состоит из одной папки и файла - // Заменим последнюю папку и вернём полный путь - if (prevSlashPos == std::string::npos) { - return replacedString + path.substr(lastSlashPos); - } - - // Собираем путь, заменяя последнюю папку на "name" - return path.substr(0, prevSlashPos + 1) + replacedString + - path.substr(lastSlashPos); +static std::string replaceUsername(const std::string& path) { + std::string username = StringUtils::removeNotAscii( + StringUtils::removeAccents(Account::username(g_currentUId))); + size_t lastSlash = path.rfind('/'); + if (lastSlash == std::string::npos) return path; + size_t prevSlash = path.rfind('/', lastSlash - 1); + if (prevSlash == std::string::npos) + return username + path.substr(lastSlash); + return path.substr(0, prevSlash + 1) + username + path.substr(lastSlash); } #endif -// Читает ровно len байт из сокета, повторяя read при частичном получении. -static bool recv_all(int sock, void *buf, size_t len) { - size_t received = 0; - while (received < len) { - ssize_t n = read(sock, static_cast(buf) + received, len - received); - if (n <= 0) return false; - received += n; - } - return true; +static bool recv_all(int sock, void* buf, size_t len) { + size_t received = 0; + while (received < len) { + ssize_t n = read(sock, static_cast(buf) + received, len - received); + if (n <= 0) return false; + received += n; + } + return true; } -// Создаёт все компоненты пути через POSIX mkdir. -// std::filesystem::create_directories не работает с devkitPro-путями sdmc:/. -static void mkdirs(const std::string &path) { - for (size_t i = 1; i < path.size(); i++) { - if (path[i] == '/') { - std::string component = path.substr(0, i); - int rc = mkdir(component.c_str(), 0777); - std::cout << "mkdirs: mkdir [" << component << "] rc=" << rc << " errno=" << errno << std::endl; +static void mkdirs(const std::string& path) { + for (size_t i = 1; i < path.size(); i++) { + if (path[i] == '/') { + std::string component = path.substr(0, i); + mkdir(component.c_str(), 0777); + } } - } - int rc = mkdir(path.c_str(), 0777); - std::cout << "mkdirs: mkdir [" << path << "] rc=" << rc << " errno=" << errno << std::endl; + mkdir(path.c_str(), 0777); } -// Функция для получения файла -void receive_file(int sock, const std::string &relative_path, - size_t file_size) { - std::cout << "relative_path is: " << relative_path << std::endl; +static void receive_file(int sock, const std::string& relative_path, uint64_t file_size) { + std::cout << "Receiving: " << relative_path << " (" << file_size << " bytes)" << std::endl; - // Печатаем путь побайтово — ловим невидимые символы - std::cout << "receive_file len=" << relative_path.size() << " path=["; - for (unsigned char c : relative_path) { - if (c >= 0x20 && c <= 0x7e) std::cout << c; - else std::cout << "\\x" << std::hex << std::setw(2) << std::setfill('0') << (int)c << std::dec; - } - std::cout << "]" << std::endl; - - size_t last_slash = relative_path.rfind('/'); - std::string dir = (last_slash != std::string::npos) - ? relative_path.substr(0, last_slash) - : ""; - std::cout << "receive_file dir=[" << dir << "]" << std::endl; - if (!dir.empty()) mkdirs(dir); - - // Проверяем stat на папке перед fopen - struct stat st; - int statrc = stat(dir.c_str(), &st); - std::cout << "stat(dir) rc=" << statrc << " is_dir=" - << (statrc == 0 && S_ISDIR(st.st_mode)) << std::endl; - - FILE *outfile = fopen(relative_path.c_str(), "wb"); - if (!outfile) { - int saved_errno = errno; - std::cerr << "Failed to open for writing: " << relative_path - << " dir=[" << dir << "] fopen_errno=" << saved_errno << std::endl; - // Дренируем байты, чтобы отправитель не завис - char* drain_buf = new char[BUFFER_SIZE]; - size_t remaining = file_size; - while (remaining > 0) { - ssize_t n = read(sock, drain_buf, remaining < (size_t)BUFFER_SIZE ? remaining : (size_t)BUFFER_SIZE); - if (n <= 0) break; - remaining -= n; + size_t last_slash = relative_path.rfind('/'); + if (last_slash != std::string::npos) { + std::string dir = relative_path.substr(0, last_slash); + if (!dir.empty()) mkdirs(dir); } - delete[] drain_buf; - return; - } - char* buffer = new char[BUFFER_SIZE](); - size_t total_bytes_received = 0; - while (total_bytes_received < file_size) { - size_t to_read = std::min((size_t)BUFFER_SIZE, file_size - total_bytes_received); - ssize_t bytes_received = read(sock, buffer, to_read); - std::cout << "Bytes received: " << bytes_received << std::endl; - if (bytes_received <= 0) { - std::cerr << "Error reading file data from socket." << std::endl; - break; + FILE* outfile = fopen(relative_path.c_str(), "wb"); + if (!outfile) { + std::cerr << "Failed to open for writing: " << relative_path + << " errno=" << errno << std::endl; + // Drain so sender doesn't hang + std::vector drain(proto::BUF_SIZE); + uint64_t remaining = file_size; + while (remaining > 0) { + size_t to_read = (size_t)std::min(remaining, (uint64_t)proto::BUF_SIZE); + ssize_t n = read(sock, drain.data(), to_read); + if (n <= 0) break; + remaining -= (uint64_t)n; + } + return; } - fwrite(buffer, 1, bytes_received, outfile); - total_bytes_received += bytes_received; - } - std::cout << "File received successfully: " << relative_path << std::endl; - delete[] buffer; - fclose(outfile); + g_server_state.bytes_total.store(file_size); + g_server_state.bytes_done.store(0); + + std::vector buffer(proto::BUF_SIZE); + uint64_t total_received = 0; + while (total_received < file_size) { + size_t to_read = (size_t)std::min(file_size - total_received, (uint64_t)proto::BUF_SIZE); + ssize_t n = read(sock, buffer.data(), to_read); + if (n <= 0) { + std::cerr << "Read error receiving: " << relative_path << std::endl; + break; + } + fwrite(buffer.data(), 1, (size_t)n, outfile); + total_received += (uint64_t)n; + g_server_state.bytes_done.store(total_received); + } + + fclose(outfile); + std::cout << "Received: " << relative_path << std::endl; } -void *handle_client(void *socket_desc) { - int client_socket = *(int *)socket_desc; - free(socket_desc); +static void* handle_client(void* socket_desc) { + int client_socket = *(int*)socket_desc; + free(socket_desc); - std::cout << "Обработка нового клиента в потоке " << pthread_self() << "\n"; + while (true) { + uint32_t filename_len = 0; + if (!recv_all(client_socket, &filename_len, sizeof(filename_len))) + break; - while (true) { + if (filename_len == proto::EOF_SENTINEL) { + std::cout << "End of transfer." << std::endl; + break; + } - uint32_t filename_len; - ssize_t bytes_read = - read(client_socket, &filename_len, sizeof(filename_len)); + if (filename_len > proto::MAX_FILENAME) { + std::cerr << "filename_len=" << filename_len << " exceeds MAX_FILENAME, aborting." << std::endl; + break; + } - // Check for end-of-transmission signal - if (bytes_read <= 0 || filename_len == 0) { - std::cout << "End of transmission detected." << std::endl; - break; - pthread_exit(nullptr); + std::vector filename(filename_len + 1, '\0'); + if (!recv_all(client_socket, filename.data(), filename_len)) { + std::cerr << "Short read on filename, aborting." << std::endl; + break; + } + std::string filename_str(filename.data(), filename_len); + +#ifdef __SWITCH__ + filename_str = replaceUsername(filename_str); +#endif + + { + size_t sl = filename_str.rfind('/'); + g_server_state.setStatus( + sl != std::string::npos ? filename_str.substr(sl + 1) : filename_str); + } + + uint64_t file_size = 0; + if (!recv_all(client_socket, &file_size, sizeof(file_size))) { + std::cerr << "Short read on file_size, aborting." << std::endl; + break; + } + + receive_file(client_socket, filename_str, file_size); } - // Receive filename - char *filename = new char[filename_len + 1](); - if (!recv_all(client_socket, filename, filename_len)) { - std::cerr << "Short read on filename, aborting." << std::endl; - delete[] filename; - break; - } - filename[filename_len] = '\0'; - std::string filename_str(filename); - delete[] filename; - - std::cout << "Received filename_str is " << filename_str << std::endl; - - #ifdef __SWITCH__ - std::cout << "Replaced filename from " << filename_str << std::endl; - filename_str = replaceUsername(filename_str); - std::cout << "to " << filename_str << std::endl; - #endif - - size_t file_size; - if (!recv_all(client_socket, &file_size, sizeof(file_size))) { - std::cerr << "Short read on file_size, aborting." << std::endl; - break; - } - std::cout << "file size is: " << file_size << std::endl; - receive_file(client_socket, filename_str, file_size); - } - - close(client_socket); - pthread_exit(nullptr); + close(client_socket); + return nullptr; } -void *broadcast_listener(void *) { - int sockfd; - struct sockaddr_in servaddr; - char buffer[BUFFER_SIZE + 1]; - struct ip_mreq group; +struct AcceptArgs { int server_fd; }; - if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) { - perror("socket creation failed"); - pthread_exit(nullptr); - } +static void* accept_and_handle(void* arg) { + int server_fd = static_cast(arg)->server_fd; + delete static_cast(arg); - memset(&servaddr, 0, sizeof(servaddr)); - servaddr.sin_family = AF_INET; - servaddr.sin_addr.s_addr = htonl(INADDR_ANY); - servaddr.sin_port = htons(MULTICAST_PORT); + sockaddr_in client_addr{}; + socklen_t client_len = sizeof(client_addr); + int client_socket = accept(server_fd, (sockaddr*)&client_addr, &client_len); + close(server_fd); - if (bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr))) { - perror("binding datagram socket"); - close(sockfd); - pthread_exit(nullptr); - } - - group.imr_multiaddr.s_addr = inet_addr(MULTICAST_GROUP); - group.imr_interface.s_addr = htonl(INADDR_ANY); - - if (setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, (char *)&group, - sizeof(group)) < 0) { - perror("setsockopt failed"); - close(sockfd); - pthread_exit(nullptr); - } - - std::cout << "Broadcast listener started" << std::endl; - struct sockaddr_in client_addr; - socklen_t addr_len = sizeof(client_addr); - while (true) { - int n = recvfrom(sockfd, buffer, BUFFER_SIZE, 0, - (struct sockaddr *)&client_addr, &addr_len); - if (n < 0) { - perror("recvfrom failed"); - continue; + if (client_socket >= 0) { + g_server_client_sock.store(client_socket); + int* pclient = new (std::nothrow) int(client_socket); + if (pclient) handle_client(pclient); + g_server_client_sock.store(-1); } - std::cout << buffer << std::endl; - buffer[n] = '\0'; - if (strcmp(buffer, "DISCOVER_SERVER") == 0) { - const char *message = "SERVER_HERE"; - sendto(sockfd, message, strlen(message), 0, - (const struct sockaddr *)&client_addr, addr_len); - std::cout << "Server discovery response sent to multicast group" - << std::endl; - - pthread_exit(0); - } - } - close(sockfd); - pthread_exit(nullptr); + g_server_state.done.store(true); + return nullptr; +} + +static void* broadcast_listener(void* arg) { + Socket udp(socket(AF_INET, SOCK_DGRAM, 0)); + if (!udp.valid()) { + perror("broadcast_listener: socket"); + return nullptr; + } + + g_broadcast_sock.store(udp.fd); + + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_ANY); + addr.sin_port = htons(proto::MULTICAST_PORT); + + if (bind(udp, (sockaddr*)&addr, sizeof(addr)) < 0) { + perror("broadcast_listener: bind"); + g_broadcast_sock.store(-1); + return nullptr; + } + + ip_mreq group{}; + group.imr_multiaddr.s_addr = inet_addr(proto::MULTICAST_GROUP); + group.imr_interface.s_addr = htonl(INADDR_ANY); + if (setsockopt(udp, IPPROTO_IP, IP_ADD_MEMBERSHIP, &group, sizeof(group)) < 0) { + perror("broadcast_listener: setsockopt"); + g_broadcast_sock.store(-1); + return nullptr; + } + + std::cout << "Broadcast listener started" << std::endl; + char buf[256]; + sockaddr_in from{}; + socklen_t fromlen = sizeof(from); + + while (true) { + ssize_t n = recvfrom(udp, buf, sizeof(buf) - 1, 0, (sockaddr*)&from, &fromlen); + if (n < 0) { + if (g_server_state.cancelled.load()) break; + continue; + } + buf[n] = '\0'; + if (strcmp(buf, "DISCOVER_SERVER") == 0) { + const char* reply = "SERVER_HERE"; + sendto(udp, reply, strlen(reply), 0, (sockaddr*)&from, fromlen); + std::cout << "Discovery replied." << std::endl; + break; + } + } + + g_broadcast_sock.store(-1); + return nullptr; } int startSendingThread() { - pthread_t broadcast_thread; - if (pthread_create(&broadcast_thread, nullptr, broadcast_listener, nullptr) < - 0) { - perror("Thread creation failed"); - return 1; - } + pthread_t broadcast_thread; + if (pthread_create(&broadcast_thread, nullptr, broadcast_listener, nullptr) != 0) { + perror("startSendingThread: broadcast thread"); + return 1; + } + pthread_detach(broadcast_thread); - int server_fd, new_socket; - struct sockaddr_in address; - socklen_t addrlen = sizeof(address); - - if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) { - perror("Socket creation failed"); - exit(EXIT_FAILURE); - } - - address.sin_family = AF_INET; - address.sin_addr.s_addr = INADDR_ANY; - address.sin_port = htons(PORT); - - if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) { - perror("Bind failed"); - close(server_fd); - exit(EXIT_FAILURE); - } - - if (listen(server_fd, 3) < 0) { - perror("Listen failed"); - close(server_fd); - exit(EXIT_FAILURE); - } - - std::cout << "Wait for broadcast thread done " << std::endl; - - pthread_join(broadcast_thread, NULL); - - std::cout << "Broadcast thread done " << std::endl; - - std::cout << "Server listening on port " << PORT << std::endl; - - while (true) { - sockaddr_in client_address; - socklen_t client_len = sizeof(client_address); - int client_socket = - accept(server_fd, (sockaddr *)&client_address, &client_len); - - if (client_socket < 0) { - std::cerr << "Ошибка принятия подключения\n"; - continue; + Socket server(socket(AF_INET, SOCK_STREAM, 0)); + if (!server.valid()) { + perror("startSendingThread: socket"); + return 1; } - // Создаем новый поток для обработки клиента - pthread_t thread_id; - int *pclient = new (std::nothrow) int(client_socket); - if (!pclient) { - std::cerr << "Ошибка выделения памяти\n"; - close(client_socket); - continue; - } - if (pthread_create(&thread_id, nullptr, handle_client, pclient) != 0) { - std::cerr << "Ошибка создания потока\n"; - delete pclient; // Освобождаем память при ошибке - } else { - pthread_join(thread_id, NULL); - break; - } - } + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = INADDR_ANY; + addr.sin_port = htons(proto::TCP_PORT); - close(server_fd); - return 0; + if (bind(server, (sockaddr*)&addr, sizeof(addr)) < 0) { + perror("startSendingThread: bind"); + return 1; + } + if (listen(server, 3) < 0) { + perror("startSendingThread: listen"); + return 1; + } + + g_server_state.reset(); + g_server_state.setStatus("Waiting for connection..."); + + AcceptArgs* acc_args = new AcceptArgs{server.fd}; + pthread_t accept_thread; + if (pthread_create(&accept_thread, nullptr, accept_and_handle, acc_args) != 0) { + delete acc_args; + return 1; + } + pthread_detach(accept_thread); + server.release(); // accepted by accept_and_handle + return 0; } -#ifndef __SWITCH__ // for desktop +#ifndef __SWITCH__ int main() { - return startSendingThread(); + if (startSendingThread() != 0) return 1; + while (!isServerTransferDone()) usleep(16000); + return 0; } #endif