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.
This commit is contained in:
Guillaume Jacquemin 2024-04-07 20:45:31 +02:00
parent e06a65ec71
commit fbfcce1d86
Signed by: williamjcm
SSH key fingerprint: SHA256:AYLOg+iTV0ElElnlu4vqM4edFazVdRiuQB0Y5LoKc4A
9 changed files with 465 additions and 351 deletions

View file

@ -38,6 +38,7 @@
#include <efsw/efsw.hpp>
#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<Managers::ProfileManager> _profileManager;
GameObjects::Profile* _currentProfile = nullptr;
Containers::Pointer<Managers::BackupManager> _backupManager;
Containers::Pointer<Managers::MassManager> _massManager;
GameObjects::Mass* _currentMass = nullptr;

View file

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

View file

@ -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,

View file

@ -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

43
src/Managers/Backup.h Normal file
View file

@ -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 <https://www.gnu.org/licenses/>.
#include <cstdint>
#include <Corrade/Containers/Array.h>
#include <Corrade/Containers/String.h>
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<Containers::String> includedFiles;
};
}}

View file

@ -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 <https://www.gnu.org/licenses/>.
#include <ctime>
#include <algorithm>
#include <chrono>
#include <Corrade/Containers/GrowableArray.h>
#include <Corrade/Containers/ScopeGuard.h>
#include <Corrade/Utility/Path.h>
#include <Corrade/Utility/String.h>
#include <zip.h>
#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<Backup>{};
scanSubdir(""_s);
}
Containers::ArrayView<const Backup>
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(&timestamp);
}();
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<std::size_t>(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--;
}
}}

View file

@ -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 <https://www.gnu.org/licenses/>.
#include <Corrade/Containers/Array.h>
#include <Corrade/Containers/ArrayView.h>
#include <Corrade/Containers/String.h>
#include <Corrade/Containers/StringView.h>
#include <efsw/efsw.hpp>
#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<const Backup>;
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<Backup> _backups;
};
}}

View file

@ -27,6 +27,7 @@
#include <zip.h>
#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<GameObjects::Profile>{};
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(&timestamp);
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<Backup>
ProfileManager::backups() {
return _backups;
}
void
ProfileManager::refreshBackups() {
_backups = Containers::Array<Backup>{};
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<std::size_t>(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;
}
}}

View file

@ -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<Containers::String> 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<Backup>;
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<GameObjects::Profile> _profiles;
Containers::Array<Backup> _backups;
};
}}