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; }; }}