diff --git a/lib/libimhex/include/hex/api/content_registry/settings.hpp b/lib/libimhex/include/hex/api/content_registry/settings.hpp index c16719252..406ee09cd 100644 --- a/lib/libimhex/include/hex/api/content_registry/settings.hpp +++ b/lib/libimhex/include/hex/api/content_registry/settings.hpp @@ -239,6 +239,14 @@ EXPORT_MODULE namespace hex { nlohmann::json store() override { return {}; } }; + class Spacer : public Widget { + public: + bool draw(const std::string &name) override; + + void load(const nlohmann::json &) override {} + nlohmann::json store() override { return {}; } + }; + } namespace impl { diff --git a/lib/libimhex/include/hex/providers/provider.hpp b/lib/libimhex/include/hex/providers/provider.hpp index d9c9a047d..dab48c9e9 100644 --- a/lib/libimhex/include/hex/providers/provider.hpp +++ b/lib/libimhex/include/hex/providers/provider.hpp @@ -72,6 +72,21 @@ namespace hex::prv { [[nodiscard]] virtual std::vector getDataDescription() const = 0; }; + class IProviderDataBackupable { + public: + explicit IProviderDataBackupable(Provider *provider); + virtual ~IProviderDataBackupable() = default; + + void createBackupIfNeeded(const std::fs::path &inputFilePath); + private: + Provider *m_provider = nullptr; + bool m_backupCreated = false; + + bool m_shouldCreateBackups = true; + u64 m_maxSize; + std::string m_backupExtension; + }; + /** * @brief Represent the data source for a tab in the UI */ diff --git a/lib/libimhex/source/api/content_registry.cpp b/lib/libimhex/source/api/content_registry.cpp index 853d03b57..2902bd1b9 100644 --- a/lib/libimhex/source/api/content_registry.cpp +++ b/lib/libimhex/source/api/content_registry.cpp @@ -594,6 +594,12 @@ namespace hex { return false; } + bool Spacer::draw(const std::string& name) { + std::ignore = name; + ImGui::NewLine(); + + return false; + } } diff --git a/lib/libimhex/source/providers/provider.cpp b/lib/libimhex/source/providers/provider.cpp index 381ba357a..9ed32f0d0 100644 --- a/lib/libimhex/source/providers/provider.cpp +++ b/lib/libimhex/source/providers/provider.cpp @@ -7,12 +7,14 @@ #include #include #include +#include #include #include #include #include +#include namespace hex::prv { @@ -24,6 +26,28 @@ namespace hex::prv { } + IProviderDataBackupable::IProviderDataBackupable(Provider* provider) : m_provider(provider) { + m_shouldCreateBackups = ContentRegistry::Settings::read("hex.builtin.setting.general", "hex.builtin.setting.general.backups.file_backup.enable", true); + m_maxSize = ContentRegistry::Settings::read("hex.builtin.setting.general", "hex.builtin.setting.general.backups.file_backup.max_size", 1_MiB); + m_backupExtension = ContentRegistry::Settings::read("hex.builtin.setting.general", "hex.builtin.setting.general.backups.file_backup.extension", ".bak"); + } + + void IProviderDataBackupable::createBackupIfNeeded(const std::fs::path &inputFilePath) { + if (!m_shouldCreateBackups || m_backupCreated) + return; + + if (m_provider->getActualSize() > m_maxSize) + return; + + const std::fs::path backupFilePath = wolv::util::toUTF8String(inputFilePath) + m_backupExtension; + if (wolv::io::fs::copyFile(inputFilePath, backupFilePath, std::fs::copy_options::overwrite_existing)) { + if (wolv::io::fs::exists(backupFilePath)) { + m_backupCreated = true; + log::info("Created backup of provider data at '{}'", backupFilePath.string()); + } + } + } + Provider::Provider() : m_undoRedoStack(this), m_id(s_idCounter++) { diff --git a/plugins/builtin/include/content/providers/file_provider.hpp b/plugins/builtin/include/content/providers/file_provider.hpp index b8d061033..40256b852 100644 --- a/plugins/builtin/include/content/providers/file_provider.hpp +++ b/plugins/builtin/include/content/providers/file_provider.hpp @@ -13,9 +13,10 @@ namespace hex::plugin::builtin { class FileProvider : public prv::Provider, public prv::IProviderDataDescription, public prv::IProviderFilePicker, - public prv::IProviderMenuItems { + public prv::IProviderMenuItems, + public prv::IProviderDataBackupable { public: - FileProvider() = default; + FileProvider() : IProviderDataBackupable(this) {} ~FileProvider() override = default; [[nodiscard]] bool isAvailable() const override; diff --git a/plugins/builtin/romfs/lang/en_US.json b/plugins/builtin/romfs/lang/en_US.json index c5fd90a3b..75b3ad9de 100644 --- a/plugins/builtin/romfs/lang/en_US.json +++ b/plugins/builtin/romfs/lang/en_US.json @@ -493,11 +493,15 @@ "hex.builtin.setting.folders.description": "Specify additional search paths for patterns, scripts, Yara rules and more", "hex.builtin.setting.folders.remove_folder": "Remove currently selected folder from list", "hex.builtin.setting.general": "General", + "hex.builtin.setting.general.backups": "Backups", + "hex.builtin.setting.general.backups.auto_backup_time": "Periodically backup project", + "hex.builtin.setting.general.backups.auto_backup_time.format.simple": "Every {0}s", + "hex.builtin.setting.general.backups.auto_backup_time.format.extended": "Every {0}m {1}s", + "hex.builtin.setting.general.backups.file_backup.enable": "Backup data sources before modification if possible", + "hex.builtin.setting.general.backups.file_backup.max_size": "Max file size for file backups", + "hex.builtin.setting.general.backups.file_backup.extension": "Backup file extension", "hex.builtin.setting.general.patterns": "Patterns", "hex.builtin.setting.general.network": "Network", - "hex.builtin.setting.general.auto_backup_time": "Periodically backup project", - "hex.builtin.setting.general.auto_backup_time.format.simple": "Every {0}s", - "hex.builtin.setting.general.auto_backup_time.format.extended": "Every {0}m {1}s", "hex.builtin.setting.general.auto_apply_patterns": "Auto-load supported pattern", "hex.builtin.setting.general.suggest_patterns": "Suggest patterns based on loaded data", "hex.builtin.setting.general.server_contact": "Enable update checks and usage statistics", diff --git a/plugins/builtin/source/content/providers/file_provider.cpp b/plugins/builtin/source/content/providers/file_provider.cpp index ff8104edc..c955dc75b 100644 --- a/plugins/builtin/source/content/providers/file_provider.cpp +++ b/plugins/builtin/source/content/providers/file_provider.cpp @@ -70,13 +70,16 @@ namespace hex::plugin::builtin { if (m_loadedIntoMemory) std::memcpy(m_data.data() + offset, buffer, size); - else + else { + this->createBackupIfNeeded(m_file.getPath()); m_file.writeBufferAtomic(offset, static_cast(buffer), size); + } } void FileProvider::save() { if (m_loadedIntoMemory) { m_ignoreNextChangeEvent = true; + this->createBackupIfNeeded(m_file.getPath()); m_file.open(); m_file.writeVectorAtomic(0x00, m_data); m_file.setSize(m_data.size()); @@ -113,8 +116,10 @@ namespace hex::plugin::builtin { void FileProvider::resizeRaw(u64 newSize) { if (m_loadedIntoMemory) m_data.resize(newSize); - else + else { + this->createBackupIfNeeded(m_file.getPath()); m_file.setSize(newSize); + } m_fileSize = newSize; } diff --git a/plugins/builtin/source/content/settings_entries.cpp b/plugins/builtin/source/content/settings_entries.cpp index 77e88ee84..36c2b8306 100644 --- a/plugins/builtin/source/content/settings_entries.cpp +++ b/plugins/builtin/source/content/settings_entries.cpp @@ -227,9 +227,9 @@ namespace hex::plugin::builtin { if (value == 0) return "hex.ui.common.off"_lang; else if (value < 60) - return fmt::format("hex.builtin.setting.general.auto_backup_time.format.simple"_lang, value); + return fmt::format("hex.builtin.setting.general.backups.auto_backup_time.format.simple"_lang, value); else - return fmt::format("hex.builtin.setting.general.auto_backup_time.format.extended"_lang, value / 60, value % 60); + return fmt::format("hex.builtin.setting.general.backups.auto_backup_time.format.extended"_lang, value / 60, value % 60); }(); if (ImGui::SliderInt(name.data(), &m_value, 0, (30 * 60) / 30, format.c_str(), ImGuiSliderFlags_AlwaysClamp | ImGuiSliderFlags_NoInput)) { @@ -756,7 +756,6 @@ namespace hex::plugin::builtin { ContentRegistry::Settings::add("hex.builtin.setting.general", "", "hex.builtin.setting.general.show_tips", false); ContentRegistry::Settings::add("hex.builtin.setting.general", "", "hex.builtin.setting.general.save_recent_providers", true); - ContentRegistry::Settings::add("hex.builtin.setting.general", "", "hex.builtin.setting.general.auto_backup_time"); ContentRegistry::Settings::add("hex.builtin.setting.general", "", "hex.builtin.setting.general.max_mem_file_size", 512_MiB, 0_bytes, 32_GiB, 1_MiB) .setTooltip("hex.builtin.setting.general.max_mem_file_size.desc"); ContentRegistry::Settings::add("hex.builtin.setting.general", "hex.builtin.setting.general.patterns", "hex.builtin.setting.general.pattern_data_max_filter_items", 128, 32, 1024); @@ -773,6 +772,20 @@ namespace hex::plugin::builtin { ContentRegistry::Settings::add("hex.builtin.setting.general", "hex.builtin.setting.general.network", "hex.builtin.setting.general.server_contact"); ContentRegistry::Settings::add("hex.builtin.setting.general", "hex.builtin.setting.general.network", "hex.builtin.setting.general.upload_crash_logs", true); #endif + + + ContentRegistry::Settings::add("hex.builtin.setting.general", "hex.builtin.setting.general.backups", "hex.builtin.setting.general.backups.auto_backup_time"); + ContentRegistry::Settings::add("hex.builtin.setting.general", "hex.builtin.setting.general.backups", "hex.builtin.setting.general.backups.spacer"); + + auto fileBackupEnabledWidget = ContentRegistry::Settings::add("hex.builtin.setting.general", "hex.builtin.setting.general.backups", "hex.builtin.setting.general.backups.file_backup.enable", true); + ContentRegistry::Settings::add("hex.builtin.setting.general", "hex.builtin.setting.general.backups", "hex.builtin.setting.general.backups.file_backup.max_size", 512_MiB, 0_bytes, 32_GiB, 1_MiB) + .setEnabledCallback([=] { + return static_cast(fileBackupEnabledWidget.getWidget()).isChecked(); + }); + ContentRegistry::Settings::add("hex.builtin.setting.general", "hex.builtin.setting.general.backups", "hex.builtin.setting.general.backups.file_backup.extension", ".bak") + .setEnabledCallback([=] { + return static_cast(fileBackupEnabledWidget.getWidget()).isChecked(); + }); } /* Interface */