From fbfcce1d86252b66c44e9ac3cdff9a431ac1e61d Mon Sep 17 00:00:00 2001 From: Guillaume Jacquemin Date: Sun, 7 Apr 2024 20:45:31 +0200 Subject: [PATCH] Managers: split ProfileManager functionality. There's now a BackupManager class, which handles all backup management functionalities ProfileManager used to have. ProfileManager also got adapted to paths being available from Configuration, which was long overdue. Application was adapted to the various changes. --- src/Application/Application.h | 4 +- .../Application_Initialisation.cpp | 4 +- .../Application_ProfileManager.cpp | 94 ++--- src/CMakeLists.txt | 3 + src/Managers/Backup.h | 43 +++ src/Managers/BackupManager.cpp | 323 ++++++++++++++++++ src/Managers/BackupManager.h | 55 +++ src/Managers/ProfileManager.cpp | 262 +------------- src/Managers/ProfileManager.h | 28 +- 9 files changed, 465 insertions(+), 351 deletions(-) create mode 100644 src/Managers/Backup.h create mode 100644 src/Managers/BackupManager.cpp create mode 100644 src/Managers/BackupManager.h diff --git a/src/Application/Application.h b/src/Application/Application.h index 0127256..49c74ce 100644 --- a/src/Application/Application.h +++ b/src/Application/Application.h @@ -38,6 +38,7 @@ #include +#include "../Managers/BackupManager.h" #include "../Managers/MassManager.h" #include "../Managers/ProfileManager.h" #include "../ToastQueue/ToastQueue.h" @@ -116,7 +117,6 @@ class Application: public Platform::Sdl2Application, public efsw::FileWatchListe void drawBackupListPopup(); void drawBackupRestorePopup(std::size_t backup_index); void drawBackupDeletePopup(std::size_t backup_index); - void drawBackupProfilePopup(std::size_t profile_index); void drawDeleteProfilePopup(std::size_t profile_index); void drawManager(); @@ -239,6 +239,8 @@ class Application: public Platform::Sdl2Application, public efsw::FileWatchListe Containers::Pointer _profileManager; GameObjects::Profile* _currentProfile = nullptr; + Containers::Pointer _backupManager; + Containers::Pointer _massManager; GameObjects::Mass* _currentMass = nullptr; diff --git a/src/Application/Application_Initialisation.cpp b/src/Application/Application_Initialisation.cpp index 31f68e8..1c96fbf 100644 --- a/src/Application/Application_Initialisation.cpp +++ b/src/Application/Application_Initialisation.cpp @@ -109,13 +109,15 @@ Application::initialiseManager() { SDL_zero(event); event.type = _initEventId; - _profileManager.emplace(conf().directories().gameSaves, conf().directories().backups); + _profileManager.emplace(); if(!_profileManager->ready()) { event.user.code = ProfileManagerFailure; SDL_PushEvent(&event); return; } + _backupManager.emplace(); + event.user.code = InitSuccess; SDL_PushEvent(&event); } diff --git a/src/Application/Application_ProfileManager.cpp b/src/Application/Application_ProfileManager.cpp index 29b5e8d..fa55485 100644 --- a/src/Application/Application_ProfileManager.cpp +++ b/src/Application/Application_ProfileManager.cpp @@ -58,7 +58,7 @@ Application::drawProfileManager() { } ImGui::SameLine(); if(ImGui::SmallButton("Backups")) { - _profileManager->refreshBackups(); + _backupManager->refresh(); ImGui::OpenPopup("Backups##BackupsModal"); } drawBackupListPopup(); @@ -100,11 +100,14 @@ Application::drawProfileManager() { ImGui::TableSetColumnIndex(2); if(ImGui::SmallButton(ICON_FA_FILE_ARCHIVE)) { - profile_index = i; - ImGui::OpenPopup("Include builds ?##IncludeBuildsDialog"); + if(!_backupManager->create(_profileManager->profiles()[i])) { + _queue.addToast(Toast::Type::Error, _backupManager->lastError(), std::chrono::seconds{5}); + } + else { + _queue.addToast(Toast::Type::Success, "Backup created successfully!"_s); + } } drawTooltip("Backup"); - drawBackupProfilePopup(profile_index); ImGui::SameLine(0.0f, 2.0f); if(drawUnsafeWidget(ImGui::SmallButton, ICON_FA_TRASH_ALT)) { profile_index = i; @@ -144,13 +147,13 @@ Application::drawBackupListPopup() { ImGui::TableSetColumnIndex(1); if(ImGui::SmallButton("Refresh")) { - _profileManager->refreshBackups(); + _backupManager->refresh(); } ImGui::EndTable(); } - if(_profileManager->backups().isEmpty()) { + if(_backupManager->backups().isEmpty()) { ImGui::TextDisabled("No backups were found."); } else if(ImGui::BeginTable("##Backups", 4, @@ -172,8 +175,8 @@ Application::drawBackupListPopup() { ImGui::TableSetColumnIndex(3); ImGui::TextUnformatted("Actions"); - for(std::size_t i = 0; i < _profileManager->backups().size(); ++i) { - auto& backup = _profileManager->backups()[i]; + for(std::size_t i = 0; i < _backupManager->backups().size(); ++i) { + auto& backup = _backupManager->backups()[i]; ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); @@ -249,13 +252,13 @@ Application::drawBackupRestorePopup(std::size_t backup_index) { ImGui::PushTextWrapPos(float(windowSize().x()) * 0.50f); ImGui::Text("Are you sure you want to restore the %s backup from %.4i-%.2i-%.2i %.2i:%.2i:%.2i ?\n\n" "Any existing data will be overwritten.", - _profileManager->backups()[backup_index].company.data(), - _profileManager->backups()[backup_index].timestamp.year, - _profileManager->backups()[backup_index].timestamp.month, - _profileManager->backups()[backup_index].timestamp.day, - _profileManager->backups()[backup_index].timestamp.hour, - _profileManager->backups()[backup_index].timestamp.minute, - _profileManager->backups()[backup_index].timestamp.second); + _backupManager->backups()[backup_index].company.data(), + _backupManager->backups()[backup_index].timestamp.year, + _backupManager->backups()[backup_index].timestamp.month, + _backupManager->backups()[backup_index].timestamp.day, + _backupManager->backups()[backup_index].timestamp.hour, + _backupManager->backups()[backup_index].timestamp.minute, + _backupManager->backups()[backup_index].timestamp.second); ImGui::PopTextWrapPos(); if(ImGui::BeginTable("##RestoreBackupLayout", 2)) { @@ -266,7 +269,7 @@ Application::drawBackupRestorePopup(std::size_t backup_index) { ImGui::TableSetColumnIndex(1); if(ImGui::Button("Yes")) { - if(!_profileManager->restoreBackup(backup_index)) { + if(!_backupManager->restore(backup_index)) { _queue.addToast(Toast::Type::Error, _profileManager->lastError()); } if(!_profileManager->refreshProfiles()) { @@ -298,13 +301,13 @@ Application::drawBackupDeletePopup(std::size_t backup_index) { ImGui::PushTextWrapPos(float(windowSize().x()) * 0.50f); ImGui::Text("Are you sure you want to delete the %s backup from %.4i-%.2i-%.2i %.2i:%.2i:%.2i ?\n\n" "This operation is irreversible.", - _profileManager->backups()[backup_index].company.data(), - _profileManager->backups()[backup_index].timestamp.year, - _profileManager->backups()[backup_index].timestamp.month, - _profileManager->backups()[backup_index].timestamp.day, - _profileManager->backups()[backup_index].timestamp.hour, - _profileManager->backups()[backup_index].timestamp.minute, - _profileManager->backups()[backup_index].timestamp.second); + _backupManager->backups()[backup_index].company.data(), + _backupManager->backups()[backup_index].timestamp.year, + _backupManager->backups()[backup_index].timestamp.month, + _backupManager->backups()[backup_index].timestamp.day, + _backupManager->backups()[backup_index].timestamp.hour, + _backupManager->backups()[backup_index].timestamp.minute, + _backupManager->backups()[backup_index].timestamp.second); ImGui::PopTextWrapPos(); if(ImGui::BeginTable("##DeleteBackupLayout", 2)) { @@ -315,7 +318,7 @@ Application::drawBackupDeletePopup(std::size_t backup_index) { ImGui::TableSetColumnIndex(1); if(ImGui::Button("Yes")) { - if(!_profileManager->deleteBackup(backup_index)) { + if(!_backupManager->remove(backup_index)) { _queue.addToast(Toast::Type::Error, _profileManager->lastError()); } ImGui::CloseCurrentPopup(); @@ -331,49 +334,6 @@ Application::drawBackupDeletePopup(std::size_t backup_index) { ImGui::EndPopup(); } -void -Application::drawBackupProfilePopup(std::size_t profile_index) { - if(!ImGui::BeginPopupModal("Include builds ?##IncludeBuildsDialog", nullptr, - ImGuiWindowFlags_AlwaysAutoResize|ImGuiWindowFlags_NoCollapse|ImGuiWindowFlags_NoMove)) - { - return; - } - - ImGui::TextUnformatted("Should builds be added to the backup ?"); - - if(ImGui::BeginTable("##NameBackupLayout", 2)) { - ImGui::TableSetupColumn("##Dummy", ImGuiTableColumnFlags_WidthStretch); - ImGui::TableSetupColumn("##YesNo", ImGuiTableColumnFlags_WidthFixed); - - ImGui::TableNextRow(); - - ImGui::TableSetColumnIndex(1); - if(ImGui::Button("Yes")) { - if(!_profileManager->backupProfile(profile_index, true)) { - SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error", - _profileManager->lastError().data(), window()); - } - ImGui::CloseCurrentPopup(); - } - ImGui::SameLine(); - if(ImGui::Button("No", ImGui::GetItemRectSize())) { - if(!_profileManager->backupProfile(profile_index, false)) { - SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error", - _profileManager->lastError().data(), window()); - } - ImGui::CloseCurrentPopup(); - } - ImGui::SameLine(); - if(ImGui::Button("Cancel")) { - ImGui::CloseCurrentPopup(); - } - - ImGui::EndTable(); - } - - ImGui::EndPopup(); -} - void Application::drawDeleteProfilePopup(std::size_t profile_index) { if(!ImGui::BeginPopupModal("Confirmation##DeleteProfileConfirmation", nullptr, diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index bbe0045..89f4ac5 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -192,6 +192,9 @@ add_executable(MassBuilderSaveTool GameObjects/Weapon.h GameObjects/Weapon.cpp GameObjects/WeaponPart.h + Managers/Backup.h + Managers/BackupManager.h + Managers/BackupManager.cpp Managers/MassManager.h Managers/MassManager.cpp Managers/ProfileManager.h diff --git a/src/Managers/Backup.h b/src/Managers/Backup.h new file mode 100644 index 0000000..db8b014 --- /dev/null +++ b/src/Managers/Backup.h @@ -0,0 +1,43 @@ +#pragma once + +// MassBuilderSaveTool +// Copyright (C) 2021-2024 Guillaume Jacquemin +// +// 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 . + +#include + +#include +#include + +using namespace Corrade; + +namespace mbst { namespace Managers { + +struct Backup { + Containers::String filename; + Containers::String company; + bool demo; + struct { + std::int32_t year; + std::int32_t month; + std::int32_t day; + std::int32_t hour; + std::int32_t minute; + std::int32_t second; + } timestamp; + Containers::Array includedFiles; +}; + +}} diff --git a/src/Managers/BackupManager.cpp b/src/Managers/BackupManager.cpp new file mode 100644 index 0000000..a455e49 --- /dev/null +++ b/src/Managers/BackupManager.cpp @@ -0,0 +1,323 @@ +// MassBuilderSaveTool +// Copyright (C) 2021-2024 Guillaume Jacquemin +// +// 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 . + +#include + +#include +#include + +#include +#include +#include +#include + +#include + +#include "../Configuration/Configuration.h" +#include "../Logger/Logger.h" +#include "../Utilities/Temp.h" + +#include "BackupManager.h" + +namespace mbst { namespace Managers { + +BackupManager::BackupManager() { + refresh(); +} + +Containers::StringView +BackupManager::lastError() { + return _lastError; +} + +void +BackupManager::refresh() { + _backups = Containers::Array{}; + + scanSubdir(""_s); +} + +Containers::ArrayView +BackupManager::backups() const { + return _backups; +} + +bool +BackupManager::create(const GameObjects::Profile& profile) { + if(!profile.valid()) { + LOG_ERROR(_lastError = "Profile is not valid."); + return false; + } + + const auto timestamp = []{ + std::time_t timestamp = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); + return *std::localtime(×tamp); + }(); + + auto filename = Utility::format("{}_{}{:.2d}{:.2d}_{:.2d}{:.2d}{:.2d}.backup.mbst", + Utility::String::replaceAll(profile.companyName(), ' ', '_').data(), + timestamp.tm_year + 1900, timestamp.tm_mon + 1, timestamp.tm_mday, + timestamp.tm_hour, timestamp.tm_min, timestamp.tm_sec); + auto temp_path = Utilities::getTempPath(filename); + + int error_code = 0; + auto zip = zip_open(temp_path.data(), ZIP_CREATE|ZIP_TRUNCATE, &error_code); + if(zip == nullptr) { + zip_error_t error; + zip_error_init_with_code(&error, error_code); + LOG_ERROR(_lastError = zip_error_strerror(&error)); + zip_error_fini(&error); + return false; + } + + Containers::ScopeGuard guard{&filename, [](Containers::String* str){ + Utilities::deleteTempFile(*str); + }}; + + Containers::StringView save_dir = conf().directories().gameSaves; + + auto profile_source = zip_source_file(zip, Utility::Path::join(save_dir, profile.filename()).data(), 0, 0); + if(!profile_source) { + LOG_ERROR(_lastError = zip_strerror(zip)); + zip_source_free(profile_source); + return false; + } + + if(zip_file_add(zip, profile.filename().data(), profile_source, ZIP_FL_ENC_UTF_8) == -1) { + LOG_ERROR(_lastError = zip_strerror(zip)); + zip_source_free(profile_source); + return false; + } + + auto comment = Utility::format("{}|{}|{}-{:.2d}-{:.2d}-{:.2d}-{:.2d}-{:.2d}", + profile.companyName(), profile.isDemo() ? "demo"_s : "full"_s, + timestamp.tm_year + 1900, timestamp.tm_mon + 1, timestamp.tm_mday, + timestamp.tm_hour, timestamp.tm_min, timestamp.tm_sec); + zip_set_archive_comment(zip, comment.data(), comment.size()); + + for(std::uint8_t i = 0; i < 32; ++i) { + auto build_filename = Utility::format("{}Unit{:.2d}{}.sav", + profile.isDemo() ? "Demo"_s : ""_s, i, + profile.account()); + + if(!Utility::Path::exists(Utility::Path::join(save_dir, build_filename))) { + continue; + } + + auto build_source = zip_source_file(zip, Utility::Path::join(save_dir, build_filename).data(), 0, 0); + if(!build_source) { + LOG_ERROR(_lastError = zip_strerror(zip)); + zip_source_free(build_source); + return false; + } + + if(zip_file_add(zip, build_filename.data(), build_source, ZIP_FL_ENC_UTF_8) == -1) { + LOG_ERROR(_lastError = zip_strerror(zip)); + zip_source_free(build_source); + return false; + } + } + + if(zip_close(zip) == -1) { + LOG_ERROR(_lastError = zip_strerror(zip)); + return false; + } + + if(!Utilities::moveFromTemp(filename, conf().directories().backups)) { + _lastError = Utility::format("Couldn't move {} to {}.", filename, conf().directories().backups); + return false; + } + + guard.release(); + + return true; +} + +bool +BackupManager::remove(std::size_t index) { + CORRADE_INTERNAL_ASSERT(index < _backups.size()); + + if(!Utility::Path::remove(Utility::Path::join(conf().directories().backups, _backups[index].filename))) { + LOG_ERROR(_lastError = "Couldn't delete " + _backups[index].filename); + return false; + } + + return true; +} + +bool +BackupManager::restore(std::size_t index) { + CORRADE_INTERNAL_ASSERT(index < _backups.size()); + + const auto& backup = _backups[index]; + + int error_code = 0; + auto zip = zip_open(Utility::Path::join(conf().directories().backups, backup.filename).data(), ZIP_RDONLY, + &error_code); + if(zip == nullptr) { + zip_error_t error; + zip_error_init_with_code(&error, error_code); + LOG_ERROR(_lastError = zip_error_strerror(&error)); + zip_error_fini(&error); + return false; + } + + Containers::ScopeGuard zip_guard{zip, zip_close}; + + auto error_format = "Extraction of file {} failed: {}"_s; + + for(Containers::StringView file : backup.includedFiles) { + auto temp_file = Utilities::getTempPath(file); + auto out = std::fopen(temp_file.cbegin(), "wb"); + if(out == nullptr) { + LOG_ERROR(_lastError = Utility::format(error_format.data(), file, std::strerror(errno))); + return false; + } + + Containers::ScopeGuard out_guard{out, std::fclose}; + + auto zf = zip_fopen(zip, file.data(), ZIP_FL_ENC_UTF_8); + if(zf == nullptr) { + LOG_ERROR(_lastError = Utility::format(error_format.data(), file, zip_strerror(zip))); + return false; + } + + Containers::ScopeGuard zf_guard{zf, zip_fclose}; + + Containers::StaticArray<8192, char> buf{ValueInit}; + + std::int64_t bytes_read; + while((bytes_read = zip_fread(zf, buf.data(), buf.size())) > 0ll) { + if(std::fwrite(buf.data(), sizeof(char), bytes_read, out) < static_cast(bytes_read)) { + LOG_ERROR(_lastError = Utility::format(error_format.data(), file, "not enough bytes written.")); + return false; + } + } + + if(bytes_read == -1) { + LOG_ERROR(_lastError = Utility::format(error_format.data(), file, "couldn't read bytes from archive.")); + return false; + } + + if(!Utilities::moveFromTemp(file, conf().directories().gameSaves)) { + _lastError = Utility::format("Couldn't move {} to {}.", file, conf().directories().gameSaves); + return false; + } + } + + return true; +} + +void +BackupManager::scanSubdir(Containers::StringView subdir) { + static std::uint8_t depth = 0; + + using Flag = Utility::Path::ListFlag; + auto files = Utility::Path::list(conf().directories().backups, Flag::SkipDirectories|Flag::SkipSpecial); + if(!files) { + LOG_ERROR_FORMAT("Couldn't list contents of {}.", conf().directories().backups); + } + + auto predicate = [](Containers::StringView file)->bool{ + return !(file.hasSuffix(".mbprofbackup"_s) || file.hasSuffix(".backup.mbst"_s)); + }; + + auto files_view = files->exceptSuffix(files->end() - std::remove_if(files->begin(), files->end(), predicate)); + + int error_code = 0; + zip_t* zip; + for(Containers::StringView file : files_view) { + Backup backup; + backup.filename = Utility::Path::join(subdir, file); + + zip = zip_open(Utility::Path::join(conf().directories().backups, file).data(), ZIP_RDONLY, &error_code); + if(zip == nullptr) { + continue; + } + + Containers::ScopeGuard guard{zip, zip_close}; + + auto num_entries = zip_get_num_entries(zip, ZIP_FL_UNCHANGED); + + if(num_entries == 0) { + continue; + } + + int comment_length; + Containers::StringView comment = zip_get_archive_comment(zip, &comment_length, ZIP_FL_UNCHANGED); + if(comment == nullptr) { + continue; + } + + auto info = comment.split('|'); + + if(info.size() != 3) { + continue; + } + + backup.company = info[0]; + + if(info[1].hasPrefix("full")) { + backup.demo = false; + } + else if(info[1].hasPrefix("demo")) { + backup.demo = true; + } + else { + continue; + } + + auto ts = info[2].split('-'); + if(ts.size() != 6) { + continue; + } + + backup.timestamp.year = std::strtol(ts[0].data(), nullptr, 10); + backup.timestamp.month = std::strtol(ts[1].data(), nullptr, 10); + backup.timestamp.day = std::strtol(ts[2].data(), nullptr, 10); + backup.timestamp.hour = std::strtol(ts[3].data(), nullptr, 10); + backup.timestamp.minute = std::strtol(ts[4].data(), nullptr, 10); + backup.timestamp.second = std::strtol(ts[5].data(), nullptr, 10); + + arrayReserve(backup.includedFiles, num_entries); + + for(auto i = 0; i < num_entries; i++) { + arrayAppend(backup.includedFiles, InPlaceInit, zip_get_name(zip, i, ZIP_FL_UNCHANGED)); + } + + arrayAppend(_backups, Utility::move(backup)); + } + + auto subdirs = Utility::Path::list(conf().directories().backups, + Flag::SkipFiles|Flag::SkipSpecial|Flag::SkipDotAndDotDot); + if(!subdirs) { + LOG_ERROR_FORMAT("Couldn't list contents of {}.", conf().directories().backups); + } + + if(depth == 5) { + return; + } + + depth++; + + for(auto& dir : *subdirs) { + scanSubdir(Utility::Path::join(subdir, dir)); + } + + depth--; +} + +}} diff --git a/src/Managers/BackupManager.h b/src/Managers/BackupManager.h new file mode 100644 index 0000000..f165f23 --- /dev/null +++ b/src/Managers/BackupManager.h @@ -0,0 +1,55 @@ +#pragma once + +// MassBuilderSaveTool +// Copyright (C) 2021-2024 Guillaume Jacquemin +// +// 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 . + +#include +#include +#include +#include + +#include + +#include "Backup.h" +#include "../GameObjects/Profile.h" + +using namespace Corrade; + +namespace mbst { namespace Managers { + +class BackupManager { + public: + BackupManager(); + + auto lastError() -> Containers::StringView; + + void refresh(); + + auto backups() const -> Containers::ArrayView; + + bool create(const GameObjects::Profile& profile); + bool remove(std::size_t index); + bool restore(std::size_t index); + + private: + void scanSubdir(Containers::StringView subdir); + + Containers::String _lastError; + + Containers::Array _backups; +}; + +}} diff --git a/src/Managers/ProfileManager.cpp b/src/Managers/ProfileManager.cpp index 2786e85..aab1024 100644 --- a/src/Managers/ProfileManager.cpp +++ b/src/Managers/ProfileManager.cpp @@ -27,6 +27,7 @@ #include +#include "../Configuration/Configuration.h" #include "../Logger/Logger.h" #include "ProfileManager.h" @@ -35,10 +36,7 @@ using namespace Containers::Literals; namespace mbst { namespace Managers { -ProfileManager::ProfileManager(Containers::StringView save_dir, Containers::StringView backup_dir): - _saveDirectory{save_dir}, - _backupsDirectory{backup_dir} -{ +ProfileManager::ProfileManager() { _ready = refreshProfiles(); } @@ -64,12 +62,11 @@ ProfileManager::refreshProfiles() { _profiles = Containers::Array{}; using Utility::Path::ListFlag; - auto files = Utility::Path::list(_saveDirectory, + auto files = Utility::Path::list(conf().directories().gameSaves, ListFlag::SkipSpecial|ListFlag::SkipDirectories|ListFlag::SkipDotAndDotDot); if(!files) { - _lastError = _saveDirectory + " can't be opened."; - LOG_ERROR(_lastError); + LOG_ERROR(_lastError = conf().directories().gameSaves + " can't be opened."); return false; } @@ -80,7 +77,7 @@ ProfileManager::refreshProfiles() { auto files_view = files->exceptSuffix(files->end() - std::remove_if(files->begin(), files->end(), predicate)); for(const auto& file : files_view) { - GameObjects::Profile profile{Utility::Path::join(_saveDirectory, file)}; + GameObjects::Profile profile{Utility::Path::join(conf().directories().gameSaves, file)}; if(!profile.valid()) { LOG_WARNING_FORMAT("Profile {} is invalid: {}", file, profile.lastError()); @@ -106,7 +103,7 @@ ProfileManager::getProfile(std::size_t index) { bool ProfileManager::deleteProfile(std::size_t index, bool delete_builds) { - if(!Utility::Path::remove(Utility::Path::join(_saveDirectory, _profiles[index].filename()))) { + if(!Utility::Path::remove(Utility::Path::join(conf().directories().gameSaves, _profiles[index].filename()))) { _lastError = Utility::format("Couldn't delete {} (filename: {}).", _profiles[index].companyName(), _profiles[index].filename()); @@ -120,7 +117,7 @@ ProfileManager::deleteProfile(std::size_t index, bool delete_builds) { auto filename = Utility::format("{}Unit{:.2d}{}.sav", _profiles[index].type() == GameObjects::Profile::Type::Demo ? "Demo": "", i, _profiles[index].account()); - Utility::Path::remove(Utility::Path::join(_saveDirectory, filename)); + Utility::Path::remove(Utility::Path::join(conf().directories().gameSaves, filename)); } } @@ -135,249 +132,4 @@ ProfileManager::deleteProfile(std::size_t index, bool delete_builds) { return true; } -bool -ProfileManager::backupProfile(std::size_t index, bool backup_builds) { - std::time_t timestamp = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); - std::tm* time = std::localtime(×tamp); - auto& profile = _profiles[index]; - - auto filename = Utility::format("{}_{}{:.2d}{:.2d}_{:.2d}{:.2d}{:.2d}.backup.mbst", - Utility::String::replaceAll(profile.companyName().data(), " ", "_").data(), - time->tm_year + 1900, time->tm_mon + 1, time->tm_mday, - time->tm_hour, time->tm_min, time->tm_sec); - - int error_code = 0; - zip_error_t error; - zip_t* zip = zip_open(Utility::Path::join(_backupsDirectory, filename).data(), ZIP_CREATE|ZIP_TRUNCATE, &error_code); - if(zip == nullptr) { - zip_error_init_with_code(&error, error_code); - _lastError = zip_error_strerror(&error); - LOG_ERROR(_lastError); - return false; - } - - zip_source_t* profile_source = zip_source_file(zip, Utility::Path::toNativeSeparators(Utility::Path::join(_saveDirectory, profile.filename())).data(), 0, 0); - if(profile_source == nullptr) { - _lastError = zip_strerror(zip); - LOG_ERROR(_lastError); - zip_source_free(profile_source); - return false; - } - - if(zip_file_add(zip, profile.filename().data(), profile_source, ZIP_FL_ENC_UTF_8) == -1) { - _lastError = zip_strerror(zip); - LOG_ERROR(_lastError); - zip_source_free(profile_source); - return false; - } - - auto comment = Utility::format("{}|{}|{}-{:.2d}-{:.2d}-{:.2d}-{:.2d}-{:.2d}", - profile.companyName(), - profile.isDemo() ? "demo"_s : "full"_s, - time->tm_year + 1900, time->tm_mon + 1, time->tm_mday, - time->tm_hour, time->tm_min, time->tm_sec); - zip_set_archive_comment(zip, comment.data(), comment.size()); - - if(backup_builds) { - for(std::uint8_t i = 0; i < 32; ++i) { - auto build_filename = Utility::format("{}Unit{:.2d}{}.sav", - profile.isDemo() ? "Demo"_s : ""_s, i, - profile.account()); - - if(!Utility::Path::exists(Utility::Path::join(_saveDirectory, build_filename))) { - continue; - } - - zip_source_t* build_source = zip_source_file(zip, Utility::Path::toNativeSeparators(Utility::Path::join(_saveDirectory, build_filename)).data(), 0, 0); - if(build_source == nullptr) { - zip_source_free(build_source); - continue; - } - - if(zip_file_add(zip, build_filename.data(), build_source, ZIP_FL_ENC_UTF_8) == -1) { - zip_source_free(build_source); - continue; - } - } - } - - if(zip_close(zip) == -1) { - _lastError = zip_strerror(zip); - LOG_ERROR(_lastError); - return false; - } - - refreshBackups(); - - return true; -} - -Containers::ArrayView -ProfileManager::backups() { - return _backups; -} - -void -ProfileManager::refreshBackups() { - _backups = Containers::Array{}; - - using Utility::Path::ListFlag; - auto files = Utility::Path::list(_backupsDirectory, - ListFlag::SkipSpecial|ListFlag::SkipDirectories|ListFlag::SkipDotAndDotDot); - - if(!files) { - _lastError = _backupsDirectory + " can't be opened."; - LOG_ERROR(_lastError); - return; - } - - auto predicate = [](Containers::StringView file)->bool{ - return !(file.hasSuffix(".mbprofbackup"_s) || file.hasSuffix(".backup.mbst")); - }; - - auto files_view = files->exceptSuffix(files->end() - std::remove_if(files->begin(), files->end(), predicate)); - - int error_code = 0; - zip_t* zip; - for(Containers::StringView file : files_view) { - Backup backup; - backup.filename = file; - - zip = zip_open(Utility::Path::join(_backupsDirectory, file).data(), ZIP_RDONLY, &error_code); - if(zip == nullptr) { - continue; - } - - Containers::ScopeGuard guard{zip, zip_close}; - - auto num_entries = zip_get_num_entries(zip, ZIP_FL_UNCHANGED); - - if(num_entries == 0) { - continue; - } - - int comment_length; - Containers::StringView comment = zip_get_archive_comment(zip, &comment_length, ZIP_FL_UNCHANGED); - if(comment == nullptr) { - continue; - } - - auto info = comment.split('|'); - - if(info.size() != 3) { - continue; - } - - backup.company = info[0]; - - if(info[1].hasPrefix("full")) { - backup.demo = false; - } - else if(info[1].hasPrefix("demo")) { - backup.demo = true; - } - else { - continue; - } - - auto ts = info[2].split('-'); - if(ts.size() != 6) { - continue; - } - - backup.timestamp.year = std::strtol(ts[0].data(), nullptr, 10); - backup.timestamp.month = std::strtol(ts[1].data(), nullptr, 10); - backup.timestamp.day = std::strtol(ts[2].data(), nullptr, 10); - backup.timestamp.hour = std::strtol(ts[3].data(), nullptr, 10); - backup.timestamp.minute = std::strtol(ts[4].data(), nullptr, 10); - backup.timestamp.second = std::strtol(ts[5].data(), nullptr, 10); - - arrayReserve(backup.includedFiles, num_entries); - - for(auto i = 0; i < num_entries; i++) { - arrayAppend(backup.includedFiles, InPlaceInit, zip_get_name(zip, i, ZIP_FL_UNCHANGED)); - } - - arrayAppend(_backups, Utility::move(backup)); - } -} - -bool -ProfileManager::deleteBackup(std::size_t index) { - if(!Utility::Path::remove(Utility::Path::join(_backupsDirectory, _backups[index].filename))) { - _lastError = "Couldn't delete " + _backups[index].filename; - LOG_ERROR(_lastError); - return false; - } - - auto file = _backups[index].filename; - auto it = std::remove_if(_backups.begin(), _backups.end(), [&file](Backup& backup){return backup.filename == file;}); - - if(it != _backups.end()) { - arrayRemoveSuffix(_backups, 1); - } - - return true; -} - -bool -ProfileManager::restoreBackup(std::size_t index) { - const Backup& backup = _backups[index]; - - auto error_format = "Extraction of file {} failed: {}"_s; - - int error_code = 0; - zip_t* zip; - - zip = zip_open(Utility::Path::join(_backupsDirectory, backup.filename).data(), ZIP_RDONLY, &error_code); - if(zip == nullptr) { - zip_error_t error; - zip_error_init_with_code(&error, error_code); - _lastError = zip_error_strerror(&error); - LOG_ERROR(_lastError); - return false; - } - - Containers::ScopeGuard zip_guard{zip, zip_close}; - - for(Containers::StringView file : backup.includedFiles) { - FILE* out = std::fopen(Utility::Path::join(_saveDirectory, file).data(), "wb"); - if(out == nullptr) { - _lastError = Utility::format(error_format.data(), file, std::strerror(errno)); - LOG_ERROR(_lastError); - return false; - } - - Containers::ScopeGuard out_guard{out, std::fclose}; - - zip_file_t* zf = zip_fopen(zip, file.data(), ZIP_FL_ENC_GUESS); - if(zf == nullptr) { - _lastError = Utility::format(error_format.data(), file, zip_strerror(zip)); - LOG_ERROR(_lastError); - return false; - } - - Containers::ScopeGuard zf_guard{zf, zip_fclose}; - - Containers::StaticArray<8192, char> buf{ValueInit}; - - std::int64_t bytes_read; - while((bytes_read = zip_fread(zf, buf.data(), buf.size())) > 0ll) { - if(std::fwrite(buf.data(), sizeof(char), bytes_read, out) < static_cast(bytes_read)) { - _lastError = Utility::format(error_format.data(), file, "not enough bytes written."); - LOG_ERROR(_lastError); - return false; - } - } - - if(bytes_read == -1) { - _lastError = Utility::format(error_format.data(), file, "couldn't read bytes from archive."); - LOG_ERROR(_lastError); - return false; - } - } - - return true; -} - }} diff --git a/src/Managers/ProfileManager.h b/src/Managers/ProfileManager.h index d1bbb7e..6116518 100644 --- a/src/Managers/ProfileManager.h +++ b/src/Managers/ProfileManager.h @@ -29,24 +29,9 @@ using namespace Corrade; namespace mbst { namespace Managers { -struct Backup { - Containers::String filename; - Containers::String company; - bool demo; - struct { - std::int32_t year; - std::int32_t month; - std::int32_t day; - std::int32_t hour; - std::int32_t minute; - std::int32_t second; - } timestamp; - Containers::Array includedFiles; -}; - class ProfileManager { public: - explicit ProfileManager(Containers::StringView save_dir, Containers::StringView backup_dir); + explicit ProfileManager(); auto ready() const -> bool; auto lastError() -> Containers::StringView; @@ -56,23 +41,12 @@ class ProfileManager { auto getProfile(std::size_t index) -> GameObjects::Profile*; bool deleteProfile(std::size_t index, bool delete_builds); - bool backupProfile(std::size_t index, bool backup_builds); - - auto backups() -> Containers::ArrayView; - void refreshBackups(); - - bool deleteBackup(std::size_t index); - bool restoreBackup(std::size_t index); private: bool _ready = false; Containers::String _lastError; - Containers::StringView _saveDirectory; - Containers::StringView _backupsDirectory; - Containers::Array _profiles; - Containers::Array _backups; }; }}