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:
parent
e06a65ec71
commit
fbfcce1d86
9 changed files with 465 additions and 351 deletions
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
43
src/Managers/Backup.h
Normal 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;
|
||||
};
|
||||
|
||||
}}
|
323
src/Managers/BackupManager.cpp
Normal file
323
src/Managers/BackupManager.cpp
Normal 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(×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<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--;
|
||||
}
|
||||
|
||||
}}
|
55
src/Managers/BackupManager.h
Normal file
55
src/Managers/BackupManager.h
Normal 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;
|
||||
};
|
||||
|
||||
}}
|
|
@ -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(×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<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;
|
||||
}
|
||||
|
||||
}}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
}}
|
||||
|
|
Loading…
Reference in a new issue