From 9b26fa4eee00c139e7d0af1c2405a8a656baaba5 Mon Sep 17 00:00:00 2001 From: Giuseppe Penone Date: Wed, 23 Nov 2022 22:44:25 +0000 Subject: [PATCH] working on performing the encryption and backup in a separate thread with verification of the backup before rotating (#2148) --- src/ct/ct_actions_file.cc | 2 +- src/ct/ct_filesystem.cc | 9 +++-- src/ct/ct_filesystem.h | 4 +-- src/ct/ct_imports.cc | 10 +++--- src/ct/ct_storage_control.cc | 44 +++++++++++++++-------- src/ct/ct_storage_sqlite.cc | 67 ++++++++++++++++++------------------ src/ct/ct_storage_xml.cc | 61 +++++++++++++++++--------------- src/ct/ct_types.h | 5 +++ 8 files changed, 114 insertions(+), 88 deletions(-) diff --git a/src/ct/ct_actions_file.cc b/src/ct/ct_actions_file.cc index a14ec65e8..004eb31b1 100644 --- a/src/ct/ct_actions_file.cc +++ b/src/ct/ct_actions_file.cc @@ -75,7 +75,7 @@ void CtActions::file_save_as() { fileSelArgs.curr_folder = currDocFilepath.parent_path(); fs::path suggested_basename = currDocFilepath.filename(); - fileSelArgs.curr_file_name = suggested_basename.stem().string() + CtMiscUtil::get_doc_extension(storageSelArgs.ctDocType, storageSelArgs.ctDocEncrypt); + fileSelArgs.curr_file_name = suggested_basename.stem() + CtMiscUtil::get_doc_extension(storageSelArgs.ctDocType, storageSelArgs.ctDocEncrypt); } fileSelArgs.filter_name = _("CherryTree Document"); std::string fileExtension = CtMiscUtil::get_doc_extension(storageSelArgs.ctDocType, storageSelArgs.ctDocEncrypt); diff --git a/src/ct/ct_filesystem.cc b/src/ct/ct_filesystem.cc index c40c00dbf..13fe7ff7c 100644 --- a/src/ct/ct_filesystem.cc +++ b/src/ct/ct_filesystem.cc @@ -575,18 +575,17 @@ path relative(const path& p, const path& base) return p; } -path path::extension() const +std::string path::extension() const { std::string name = filename().string(); auto last_pos = name.find_last_of('.'); if (last_pos == std::string::npos || last_pos == name.size() - 1 || last_pos == 0) { - return path(""); - } else { - return path(name.begin() + last_pos, name.end()); + return ""; } + return name.substr(last_pos); } -path path::stem() const +std::string path::stem() const { if (empty()) return ""; std::string name = filename().string(); diff --git a/src/ct/ct_filesystem.h b/src/ct/ct_filesystem.h index 57a096575..6925e4645 100644 --- a/src/ct/ct_filesystem.h +++ b/src/ct/ct_filesystem.h @@ -191,8 +191,8 @@ class path bool empty() const noexcept { return _path.empty(); } path filename() const { return Glib::path_get_basename(_path); } path parent_path() const { return Glib::path_get_dirname(_path); } - path extension() const; - path stem() const; + std::string extension() const; + std::string stem() const; std::string string_native() const; std::string string_unix() const; diff --git a/src/ct/ct_imports.cc b/src/ct/ct_imports.cc index 8c02044ea..eeb65735f 100644 --- a/src/ct/ct_imports.cc +++ b/src/ct/ct_imports.cc @@ -248,7 +248,7 @@ std::unique_ptr CtHtmlImport::import_file(const fs::path& file) return nullptr; } std::string htmlStr = Glib::file_get_contents(file.string()); - auto node = std::make_unique(file, file.stem().string()); + auto node = std::make_unique(file, file.stem()); CtHtml2Xml html2xml{_config}; html2xml.set_local_dir(file.parent_path().string()); html2xml.set_outter_xml_doc(node->xml_content.get()); @@ -421,7 +421,7 @@ std::unique_ptr CtZimImport::import_file(const fs::path& file) { if (file.extension() != ".txt") return nullptr; - std::unique_ptr node = std::make_unique(file, file.stem().string()); + std::unique_ptr node = std::make_unique(file, file.stem()); const std::string file_contents = Glib::file_get_contents(file.string()); @@ -446,7 +446,7 @@ std::unique_ptr CtPlainTextImport::import_file(const fs::path& f try { std::string converted = Glib::file_get_contents(file.string()); CtStrUtil::convert_if_not_utf8(converted, true/*sanitise*/); - auto node = std::make_unique(file, file.stem().string()); + auto node = std::make_unique(file, file.stem()); node->xml_content->create_root_node("root")->add_child("slot")->add_child("rich_text")->add_child_text(converted); node->node_syntax = CtConst::PLAIN_TEXT_ID; return node; @@ -470,7 +470,7 @@ std::unique_ptr CtMDImport::import_file(const fs::path& file) _parser->wipe_doc(); _parser->feed(file_contents); - std::unique_ptr node = std::make_unique(file, file.stem().string()); + std::unique_ptr node = std::make_unique(file, file.stem()); node->xml_content = _parser->doc().document(); return node; @@ -488,7 +488,7 @@ std::unique_ptr CtKeepnoteImport::import_file(const fs::path& fi CtHtml2Xml parser{_config}; parser.set_local_dir(file.parent_path().string()); parser.feed(keepnoteStr); - auto node = std::make_unique(file, file.parent_path().stem().string()); + auto node = std::make_unique(file, file.parent_path().stem()); node->xml_content->create_root_node_by_import(parser.doc().get_root_node()); //spdlog::debug(keepnoteStr); //spdlog::debug(node->xml_content->write_to_string()); diff --git a/src/ct/ct_storage_control.cc b/src/ct/ct_storage_control.cc index 4b1d1ca0f..4e60d41c6 100644 --- a/src/ct/ct_storage_control.cc +++ b/src/ct/ct_storage_control.cc @@ -52,7 +52,7 @@ std::unique_ptr get_entity_by_type(CtMainWin* pCtMainWin, CtDoc std::unique_ptr storage; fs::path extracted_file_path = file_path; try { - if (!fs::is_regular_file(file_path)) throw std::runtime_error("no file"); + if (not fs::is_regular_file(file_path)) throw std::runtime_error("no file"); // unpack file if need if (fs::get_doc_encrypt(file_path) == CtDocEncrypt::True) { @@ -67,7 +67,7 @@ std::unique_ptr get_entity_by_type(CtMainWin* pCtMainWin, CtDoc storage = get_entity_by_type(pCtMainWin, fs::get_doc_type(file_path)); // load from file - if (!storage->populate_treestore(extracted_file_path, error)) throw std::runtime_error(error); + if (not storage->populate_treestore(extracted_file_path, error)) throw std::runtime_error(error); // it's ready CtStorageControl* doc = new CtStorageControl{pCtMainWin}; @@ -88,6 +88,13 @@ std::unique_ptr get_entity_by_type(CtMainWin* pCtMainWin, CtDoc } } +/*static*/bool CtStorageControl::document_integrity_check_pass(CtMainWin* pCtMainWin, const fs::path& file_path, Glib::ustring& error) +{ + std::unique_ptr storage = get_entity_by_type(pCtMainWin, fs::get_doc_type(file_path)); + storage->set_is_dry_run(); + return storage->populate_treestore(file_path, error); +} + /*static*/CtStorageControl* CtStorageControl::save_as(CtMainWin* pCtMainWin, const fs::path& file_path, const Glib::ustring& password, @@ -164,7 +171,7 @@ bool CtStorageControl::save(bool need_vacuum, Glib::ustring &error) const std::string str_timestamp = std::to_string(g_get_monotonic_time()); fs::path main_backup = _file_path; - main_backup += str_timestamp; + main_backup += (str_timestamp + _file_path.extension()); const bool need_backup = _pCtConfig->backupCopy and _pCtConfig->backupNum > 0; const bool need_encrypt = _file_path != _extracted_file_path; try { @@ -206,7 +213,7 @@ bool CtStorageControl::save(bool need_vacuum, Glib::ustring &error) pBackupEncryptData->file_path = _file_path.string(); pBackupEncryptData->main_backup = main_backup.string(); if (need_encrypt) { - pBackupEncryptData->extracted_copy = _extracted_file_path.string() + str_timestamp; + pBackupEncryptData->extracted_copy = _extracted_file_path.string() + (str_timestamp + _extracted_file_path.extension()); _storage->close_connect(); // temporary, because of sqlite keepig the file if (not fs::copy_file(_extracted_file_path, pBackupEncryptData->extracted_copy)) { throw std::runtime_error(str::format(_("You Have No Write Access to %s"), _extracted_file_path.parent_path().string())); @@ -304,16 +311,6 @@ Glib::RefPtr CtStorageControl::get_delayed_text_buffer(const gint64 return true; } -/*static*/bool CtStorageControl::document_integrity_check_pass(CtMainWin* pCtMainWin, const fs::path& file_path, Glib::ustring& error) -{ - CtStorageControl* new_storage = CtStorageControl::load_from(pCtMainWin, file_path, error); - if (new_storage) { - delete new_storage; - return true; - } - return false; -} - CtStorageControl::CtStorageControl(CtMainWin* pCtMainWin) : _pCtMainWin{pCtMainWin} , _pCtConfig{pCtMainWin->get_ct_config()} @@ -340,6 +337,14 @@ void CtStorageControl::_backupEncryptThread() // encrypt the file if (pBackupEncryptData->needEncrypt) { + Glib::ustring error; + if (not CtStorageControl::document_integrity_check_pass(_pCtMainWin, pBackupEncryptData->extracted_copy, error)) { + spdlog::error("{} {}", __FUNCTION__, error.raw()); + _pCtMainWin->errorsDEQueue.push_back(_("Failed integrity check of the saved document. Try File-->Save As")); + _pCtMainWin->dispatcherErrorMsg.emit(); + continue; + } + spdlog::debug("{} integrity check ok", pBackupEncryptData->extracted_copy); const bool retValEncrypt = _package_file(pBackupEncryptData->extracted_copy, pBackupEncryptData->file_path, pBackupEncryptData->password); if (not fs::remove(pBackupEncryptData->extracted_copy)) { spdlog::debug("Failed to remove {}", pBackupEncryptData->extracted_copy); @@ -359,6 +364,17 @@ void CtStorageControl::_backupEncryptThread() continue; } + if (not pBackupEncryptData->needEncrypt) { + Glib::ustring error; + if (not CtStorageControl::document_integrity_check_pass(_pCtMainWin, pBackupEncryptData->main_backup, error)) { + spdlog::error("{} {}", __FUNCTION__, error.raw()); + _pCtMainWin->errorsDEQueue.push_back(_("Failed integrity check of the saved document. Try File-->Save As")); + _pCtMainWin->dispatcherErrorMsg.emit(); + continue; + } + spdlog::debug("{} integrity check ok", pBackupEncryptData->main_backup); + } + // backups with tildas can either be in the same directory where the db is or in a custom backup dir // main_backup is always in the same directory of the main db auto get_custom_backup_file = [&]()->std::string { diff --git a/src/ct/ct_storage_sqlite.cc b/src/ct/ct_storage_sqlite.cc index 972f4dacf..4f1d3dea5 100644 --- a/src/ct/ct_storage_sqlite.cc +++ b/src/ct/ct_storage_sqlite.cc @@ -224,21 +224,23 @@ void CtStorageSqlite::test_connection() bool CtStorageSqlite::populate_treestore(const fs::path& file_path, Glib::ustring& error) { _close_db(); - try - { + try { // open db _open_db(file_path); _file_path = file_path; if (!_check_database_integrity()) return false; - // load bookmarks Sqlite3StmtAuto stmt{_pDb, "SELECT node_id FROM bookmark ORDER BY sequence ASC"}; if (stmt.is_bad()) throw std::runtime_error(ERR_SQLITE_PREPV2 + sqlite3_errmsg(_pDb)); - while (sqlite3_step(stmt) == SQLITE_ROW) - _pCtMainWin->get_tree_store().bookmarks_add(sqlite3_column_int64(stmt, 0)); + while (sqlite3_step(stmt) == SQLITE_ROW) { + const auto bkmrk = sqlite3_column_int64(stmt, 0); + if (not _isDryRun) { + _pCtMainWin->get_tree_store().bookmarks_add(bkmrk); + } + } // load node tree std::function nodes_from_db; @@ -269,17 +271,16 @@ bool CtStorageSqlite::save_treestore(const fs::path& file_path, const int start_offset/*= 0*/, const int end_offset/*= -1*/) { - try - { + try { // it's the first time (or an export), a new file will be created - if (_pDb == nullptr) - { + if (_pDb == nullptr) { _open_db(file_path); _file_path = file_path; _create_all_tables_in_db(); if ( CtExporting::NONE == exporting or - CtExporting::ALL_TREE == exporting ) { + CtExporting::ALL_TREE == exporting ) + { _write_bookmarks_to_db(_pCtMainWin->get_tree_store().bookmarks_get()); } CtStorageNodeState node_state; @@ -296,7 +297,8 @@ bool CtStorageSqlite::save_treestore(const fs::path& file_path, save_node_fun = [&](CtTreeIter ct_tree_iter, const gint64 sequence, const gint64 father_id) { _write_node_to_db(&ct_tree_iter, sequence, father_id, node_state, start_offset, end_offset, &storage_cache); if ( CtExporting::CURRENT_NODE != exporting and - CtExporting::SELECTED_TEXT != exporting ) { + CtExporting::SELECTED_TEXT != exporting ) + { gint64 child_sequence{0}; CtTreeIter ct_tree_iter_child = ct_tree_iter.first_child(); while (ct_tree_iter_child) { @@ -310,7 +312,8 @@ bool CtStorageSqlite::save_treestore(const fs::path& file_path, // saving nodes gint64 sequence{0}; if ( CtExporting::NONE == exporting or - CtExporting::ALL_TREE == exporting ) { + CtExporting::ALL_TREE == exporting ) + { CtTreeIter ct_tree_iter = _pCtMainWin->get_tree_store().get_ct_iter_first(); while (ct_tree_iter) { ++sequence; @@ -324,8 +327,7 @@ bool CtStorageSqlite::save_treestore(const fs::path& file_path, } } // or need just update some info - else - { + else { CtStorageCache storage_cache; storage_cache.generate_cache(_pCtMainWin, &syncPending, false); @@ -338,16 +340,16 @@ bool CtStorageSqlite::save_treestore(const fs::path& file_path, _write_bookmarks_to_db(_pCtMainWin->get_tree_store().bookmarks_get()); } // update changed nodes - for (const auto& node_pair : syncPending.nodes_to_write_dict) - { + for (const auto& node_pair : syncPending.nodes_to_write_dict) { CtTreeIter ct_tree_iter = _pCtMainWin->get_tree_store().get_node_from_node_id(node_pair.first); CtTreeIter ct_tree_iter_parent = ct_tree_iter.parent(); _write_node_to_db(&ct_tree_iter, ct_tree_iter.get_node_sequence(), ct_tree_iter_parent ? ct_tree_iter_parent.get_node_id() : 0, node_pair.second, 0, -1, &storage_cache); } // remove nodes and their sub nodes - for (const auto node_id : syncPending.nodes_to_rm_set) + for (const auto node_id : syncPending.nodes_to_rm_set) { _remove_db_node_with_children(node_id); + } } return true; @@ -368,8 +370,7 @@ void CtStorageSqlite::vacuum() void CtStorageSqlite::_open_db(const fs::path& path) { if (_pDb) return; - if (sqlite3_open(path.c_str(), &_pDb) != SQLITE_OK) - { + if (sqlite3_open(path.c_str(), &_pDb) != SQLITE_OK) { std::string error = sqlite3_errmsg(_pDb); sqlite3_close(_pDb); // even after error, _pDb is initialized _pDb = nullptr; @@ -423,6 +424,10 @@ Gtk::TreeIter CtStorageSqlite::_node_from_db(gint64 node_id, gint64 sequence, Gt nodeData.tsCreation = sqlite3_column_int64(*uStmt, 6); nodeData.tsLastSave = sqlite3_column_int64(*uStmt, 7); + if (_isDryRun) { + return Gtk::TreeIter{}; + } + // buffer for imported node should be loaded now because file will be closed if (new_id != -1) { nodeData.rTextBuffer = get_delayed_text_buffer(node_id, nodeData.syntax, nodeData.anchoredWidgets); @@ -432,34 +437,29 @@ Gtk::TreeIter CtStorageSqlite::_node_from_db(gint64 node_id, gint64 sequence, Gt } Glib::RefPtr CtStorageSqlite::get_delayed_text_buffer(const gint64& node_id, - const std::string& syntax, - std::list& widgets) const + const std::string& syntax, + std::list& widgets) const { Sqlite3StmtAuto stmt{_pDb, "SELECT txt, has_codebox, has_table, has_image FROM node WHERE node_id=?"}; - if (stmt.is_bad()) - { + if (stmt.is_bad()) { spdlog::error("{}: {}", ERR_SQLITE_PREPV2, sqlite3_errmsg(_pDb)); return Glib::RefPtr(); } sqlite3_bind_int64(stmt, 1, node_id); - if (sqlite3_step(stmt) != SQLITE_ROW) - { + if (sqlite3_step(stmt) != SQLITE_ROW) { spdlog::error("!! missing node properties for id {}", node_id); return Glib::RefPtr(); } Glib::RefPtr rRetTextBuffer{nullptr}; const char* textContent = safe_sqlite3_column_text(stmt, 0); - if (CtConst::RICH_TEXT_ID != syntax) - { + if (CtConst::RICH_TEXT_ID != syntax) { rRetTextBuffer = _pCtMainWin->get_new_text_buffer(textContent); } - else - { - rRetTextBuffer = CtStorageXmlHelper(_pCtMainWin).create_buffer_no_widgets(syntax, textContent); - if (!rRetTextBuffer) - { + else { + rRetTextBuffer = CtStorageXmlHelper{_pCtMainWin}.create_buffer_no_widgets(syntax, textContent); + if (!rRetTextBuffer) { spdlog::error("!! xml read: {}", textContent); return rRetTextBuffer; } @@ -469,8 +469,9 @@ Glib::RefPtr CtStorageSqlite::get_delayed_text_buffer(const gint64& widgets.sort([](CtAnchoredWidget* w1, CtAnchoredWidget* w2) { return w1->getOffset() < w2->getOffset(); }); rRetTextBuffer->begin_not_undoable_action(); - for (auto widget: widgets) + for (auto widget : widgets) { widget->insertInTextBuffer(rRetTextBuffer); + } rRetTextBuffer->end_not_undoable_action(); rRetTextBuffer->set_modified(false); } diff --git a/src/ct/ct_storage_xml.cc b/src/ct/ct_storage_xml.cc index f975e9f56..460706d72 100644 --- a/src/ct/ct_storage_xml.cc +++ b/src/ct/ct_storage_xml.cc @@ -51,45 +51,47 @@ void CtStorageXml::test_connection() bool CtStorageXml::populate_treestore(const fs::path& file_path, Glib::ustring& error) { - try - { + try { // open file auto parser = _get_parser(file_path); // read bookmarks - for (xmlpp::Node* xml_node : parser->get_document()->get_root_node()->get_children("bookmarks")) - { + for (xmlpp::Node* xml_node : parser->get_document()->get_root_node()->get_children("bookmarks")) { Glib::ustring bookmarks_csv = static_cast(xml_node)->get_attribute_value("list"); - for (gint64& nodeId : CtStrUtil::gstring_split_to_int64(bookmarks_csv.c_str(), ",")) - _pCtMainWin->get_tree_store().bookmarks_add(nodeId); + for (auto nodeId : CtStrUtil::gstring_split_to_int64(bookmarks_csv.c_str(), ",")) { + if (not _isDryRun) { + _pCtMainWin->get_tree_store().bookmarks_add(nodeId); + } + } } // read nodes std::list nodes_with_duplicated_id; std::function nodes_from_xml; nodes_from_xml = [&](xmlpp::Element* xml_element, const gint64 sequence, Gtk::TreeIter parent_iter) { - bool has_duplicated_id = false; + bool has_duplicated_id{false}; Gtk::TreeIter new_iter = _node_from_xml(xml_element, sequence, parent_iter, -1, &has_duplicated_id); - if (has_duplicated_id) { + if (has_duplicated_id and not _isDryRun) { nodes_with_duplicated_id.push_back(_pCtMainWin->get_tree_store().to_ct_tree_iter(new_iter)); } gint64 child_sequence = 0; - for (xmlpp::Node* xml_node : xml_element->get_children("node")) + for (xmlpp::Node* xml_node : xml_element->get_children("node")) { nodes_from_xml(static_cast(xml_node), ++child_sequence, new_iter); + } }; gint64 sequence = 0; - for (xmlpp::Node* xml_node: parser->get_document()->get_root_node()->get_children("node")) + for (xmlpp::Node* xml_node : parser->get_document()->get_root_node()->get_children("node")) { nodes_from_xml(static_cast(xml_node), ++sequence, Gtk::TreeIter()); + } // fixes duplicated ids by setting new ids - for (auto& node: nodes_with_duplicated_id) { + for (auto& node : nodes_with_duplicated_id) { node.set_node_id(_pCtMainWin->get_tree_store().node_id_get()); } return true; } - catch (std::exception& e) - { + catch (std::exception& e) { error = std::string("CtDocXmlStorage got exception: ") + e.what(); return false; } @@ -102,8 +104,7 @@ bool CtStorageXml::save_treestore(const fs::path& file_path, const int start_offset/*= 0*/, const int end_offset/*=-1*/) { - try - { + try { xmlpp::Document xml_doc; xml_doc.create_root_node(CtConst::APP_NAME); @@ -121,8 +122,7 @@ bool CtStorageXml::save_treestore(const fs::path& file_path, if ( CtExporting::NONE == exporting or CtExporting::ALL_TREE == exporting ) { auto ct_tree_iter = _pCtMainWin->get_tree_store().get_ct_iter_first(); - while (ct_tree_iter) - { + while (ct_tree_iter) { _nodes_to_xml(&ct_tree_iter, xml_doc.get_root_node(), &storage_cache, exporting, start_offset, end_offset); ct_tree_iter++; } @@ -137,8 +137,7 @@ bool CtStorageXml::save_treestore(const fs::path& file_path, return true; } - catch (std::exception& e) - { + catch (std::exception& e) { error = e.what(); return false; } @@ -186,13 +185,16 @@ Glib::RefPtr CtStorageXml::get_delayed_text_buffer(const gint64& no Gtk::TreeIter CtStorageXml::_node_from_xml(xmlpp::Element* xml_element, gint64 sequence, Gtk::TreeIter parent_iter, gint64 new_id, bool* has_duplicated_id) { - if (has_duplicated_id) *has_duplicated_id = false; - + if (has_duplicated_id) { + *has_duplicated_id = false; + } CtNodeData node_data; - if (new_id == -1) + if (new_id == -1) { node_data.nodeId = CtStrUtil::gint64_from_gstring(xml_element->get_attribute_value("unique_id").c_str()); - else + } + else { node_data.nodeId = new_id; + } node_data.name = xml_element->get_attribute_value("name"); node_data.syntax = xml_element->get_attribute_value("prog_lang"); node_data.tags = xml_element->get_attribute_value("tags"); @@ -205,17 +207,20 @@ Gtk::TreeIter CtStorageXml::_node_from_xml(xmlpp::Element* xml_element, gint64 s node_data.tsCreation = CtStrUtil::gint64_from_gstring(xml_element->get_attribute_value("ts_creation").c_str()); node_data.tsLastSave = CtStrUtil::gint64_from_gstring(xml_element->get_attribute_value("ts_lastsave").c_str()); node_data.sequence = sequence; - if (new_id == -1) - { + + if (_isDryRun) { + return Gtk::TreeIter{}; + } + + if (new_id == -1) { if (_delayed_text_buffers.count(node_data.nodeId) != 0) { spdlog::debug("node has duplicated id {}, will be fixed", node_data.nodeId); if (has_duplicated_id) *has_duplicated_id = true; // create buffer now because we cannot put a duplicate id in _delayed_text_buffers // the id will be fixed on top level code - node_data.rTextBuffer = CtStorageXmlHelper(_pCtMainWin).create_buffer_and_widgets_from_xml(xml_element, node_data.syntax, node_data.anchoredWidgets, nullptr, -1); + node_data.rTextBuffer = CtStorageXmlHelper{_pCtMainWin}.create_buffer_and_widgets_from_xml(xml_element, node_data.syntax, node_data.anchoredWidgets, nullptr, -1); } - else - { + else { // because of widgets which are slow to insert for now, delay creating buffers // save node data in a separate document auto node_buffer = std::make_shared(); diff --git a/src/ct/ct_types.h b/src/ct/ct_types.h index db9289ed3..fd98ffd18 100644 --- a/src/ct/ct_types.h +++ b/src/ct/ct_types.h @@ -261,6 +261,11 @@ class CtStorageEntity virtual Glib::RefPtr get_delayed_text_buffer(const gint64& node_id, const std::string& syntax, std::list& widgets) const = 0; + + void set_is_dry_run() { _isDryRun = true; } + +protected: + bool _isDryRun{false}; }; struct CtExportOptions