diff --git a/source/blender/asset_system/AS_asset_catalog.hh b/source/blender/asset_system/AS_asset_catalog.hh index f23987022c8..335072bb2d4 100644 --- a/source/blender/asset_system/AS_asset_catalog.hh +++ b/source/blender/asset_system/AS_asset_catalog.hh @@ -8,7 +8,6 @@ #pragma once -#include #include #include #include @@ -17,7 +16,6 @@ #include "BLI_function_ref.hh" #include "BLI_map.hh" #include "BLI_set.hh" -#include "BLI_string_ref.hh" #include "BLI_uuid.h" #include "BLI_vector.hh" @@ -279,120 +277,6 @@ class AssetCatalogService { const OwningAssetCatalogMap &get_deleted_catalogs() const; }; -/** - * All catalogs that are owned by a single asset library, and managed by a single instance of - * #AssetCatalogService. The undo system for asset catalog edits contains historical copies of this - * struct. - */ -class AssetCatalogCollection { - protected: - /** All catalogs known, except the known-but-deleted ones. */ - OwningAssetCatalogMap catalogs_; - - /** Catalogs that have been deleted. They are kept around so that the load-merge-save of catalog - * definition files can actually delete them if they already existed on disk (instead of the - * merge operation resurrecting them). */ - OwningAssetCatalogMap deleted_catalogs_; - - /* For now only a single catalog definition file is supported. - * The aim is to support an arbitrary number of such files per asset library in the future. */ - std::unique_ptr catalog_definition_file_; - - /** Whether any of the catalogs have unsaved changes. */ - bool has_unsaved_changes_ = false; - - friend AssetCatalogService; - - public: - AssetCatalogCollection() = default; - AssetCatalogCollection(const AssetCatalogCollection &other) = delete; - AssetCatalogCollection(AssetCatalogCollection &&other) noexcept = default; - - std::unique_ptr deep_copy() const; - using OnDuplicateCatalogIdFn = - FunctionRef; - /** - * Copy the catalogs from \a other and append them to this collection. Copies no other data - * otherwise. - * - * \note If a catalog from \a other already exists in this collection (identified by catalog ID), - * it will be skipped and \a on_duplicate_items will be called. - */ - void add_catalogs_from_existing(const AssetCatalogCollection &other, - OnDuplicateCatalogIdFn on_duplicate_items); - - protected: - static OwningAssetCatalogMap copy_catalog_map(const OwningAssetCatalogMap &orig); -}; - -/** - * Keeps track of which catalogs are defined in a certain file on disk. - * Only contains non-owning pointers to the #AssetCatalog instances, so ensure the lifetime of this - * class is shorter than that of the #`AssetCatalog`s themselves. - */ -class AssetCatalogDefinitionFile { - protected: - /* Catalogs stored in this file. They are mapped by ID to make it possible to query whether a - * catalog is already known, without having to find the corresponding `AssetCatalog*`. */ - Map catalogs_; - - public: - /* For now this is the only version of the catalog definition files that is supported. - * Later versioning code may be added to handle older files. */ - const static int SUPPORTED_VERSION; - /* String that's matched in the catalog definition file to know that the line is the version - * declaration. It has to start with a space to ensure it won't match any hypothetical future - * field that starts with "VERSION". */ - const static std::string VERSION_MARKER; - const static std::string HEADER; - - CatalogFilePath file_path; - - public: - AssetCatalogDefinitionFile() = default; - - /** - * Write the catalog definitions to the same file they were read from. - * Return true when the file was written correctly, false when there was a problem. - */ - bool write_to_disk() const; - /** - * Write the catalog definitions to an arbitrary file path. - * - * Any existing file is backed up to "filename~". Any previously existing backup is overwritten. - * - * Return true when the file was written correctly, false when there was a problem. - */ - bool write_to_disk(const CatalogFilePath &dest_file_path) const; - - bool contains(CatalogID catalog_id) const; - /** Add a catalog, overwriting the one with the same catalog ID. */ - void add_overwrite(AssetCatalog *catalog); - /** Add a new catalog. Undefined behavior if a catalog with the same ID was already added. */ - void add_new(AssetCatalog *catalog); - - /** Remove the catalog from the collection of catalogs stored in this file. */ - void forget(CatalogID catalog_id); - - using AssetCatalogParsedFn = FunctionRef)>; - void parse_catalog_file(const CatalogFilePath &catalog_definition_file_path, - AssetCatalogParsedFn callback); - - std::unique_ptr copy_and_remap( - const OwningAssetCatalogMap &catalogs, const OwningAssetCatalogMap &deleted_catalogs) const; - - protected: - bool parse_version_line(StringRef line); - std::unique_ptr parse_catalog_line(StringRef line); - - /** - * Write the catalog definitions to the given file path. - * Return true when the file was written correctly, false when there was a problem. - */ - bool write_to_disk_unsafe(const CatalogFilePath &dest_file_path) const; - bool ensure_directory_exists(const CatalogFilePath directory_path) const; -}; - /** * Asset Catalog definition, containing a symbolic ID and a path that points to a node in the * catalog hierarchy. diff --git a/source/blender/asset_system/AS_asset_catalog_tree.hh b/source/blender/asset_system/AS_asset_catalog_tree.hh index e6794c84f53..c9b3effe518 100644 --- a/source/blender/asset_system/AS_asset_catalog_tree.hh +++ b/source/blender/asset_system/AS_asset_catalog_tree.hh @@ -16,6 +16,8 @@ #pragma once +#include + #include "AS_asset_catalog.hh" namespace blender::asset_system { diff --git a/source/blender/asset_system/CMakeLists.txt b/source/blender/asset_system/CMakeLists.txt index b48cfff5e09..9b711387ce9 100644 --- a/source/blender/asset_system/CMakeLists.txt +++ b/source/blender/asset_system/CMakeLists.txt @@ -13,6 +13,8 @@ set(INC_SYS set(SRC intern/asset_catalog.cc + intern/asset_catalog_collection.cc + intern/asset_catalog_definition_file.cc intern/asset_catalog_path.cc intern/asset_catalog_tree.cc intern/asset_identifier.cc @@ -34,6 +36,8 @@ set(SRC AS_asset_library.hh AS_asset_representation.hh AS_essentials_library.hh + intern/asset_catalog_collection.hh + intern/asset_catalog_definition_file.hh intern/asset_library_all.hh intern/asset_library_essentials.hh intern/asset_library_from_preferences.hh diff --git a/source/blender/asset_system/intern/asset_catalog.cc b/source/blender/asset_system/intern/asset_catalog.cc index e438f1cadbd..6dfa546491a 100644 --- a/source/blender/asset_system/intern/asset_catalog.cc +++ b/source/blender/asset_system/intern/asset_catalog.cc @@ -6,15 +6,16 @@ * \ingroup asset_system */ -#include #include #include #include "AS_asset_catalog.hh" #include "AS_asset_catalog_tree.hh" #include "AS_asset_library.hh" +#include "asset_catalog_collection.hh" +#include "asset_catalog_definition_file.hh" -#include "BLI_fileops.hh" +#include "BLI_fileops.h" #include "BLI_path_util.h" /* For S_ISREG() and S_ISDIR() on Windows. */ @@ -32,16 +33,6 @@ namespace blender::asset_system { const CatalogFilePath AssetCatalogService::DEFAULT_CATALOG_FILENAME = "blender_assets.cats.txt"; -const int AssetCatalogDefinitionFile::SUPPORTED_VERSION = 1; -const std::string AssetCatalogDefinitionFile::VERSION_MARKER = "VERSION "; - -const std::string AssetCatalogDefinitionFile::HEADER = - "# This is an Asset Catalog Definition file for Blender.\n" - "#\n" - "# Empty lines and lines starting with `#` will be ignored.\n" - "# The first non-ignored line should be the version indicator.\n" - "# Other lines are of the format \"UUID:catalog/path/for/assets:simple catalog name\"\n"; - AssetCatalogService::AssetCatalogService() : catalog_collection_(std::make_unique()) { @@ -689,313 +680,6 @@ void AssetCatalogService::undo_push() /* ---------------------------------------------------------------------- */ -std::unique_ptr AssetCatalogCollection::deep_copy() const -{ - auto copy = std::make_unique(); - - copy->has_unsaved_changes_ = this->has_unsaved_changes_; - copy->catalogs_ = this->copy_catalog_map(this->catalogs_); - copy->deleted_catalogs_ = this->copy_catalog_map(this->deleted_catalogs_); - - if (catalog_definition_file_) { - copy->catalog_definition_file_ = catalog_definition_file_->copy_and_remap( - copy->catalogs_, copy->deleted_catalogs_); - } - - return copy; -} - -static void copy_catalog_map_into_existing( - const OwningAssetCatalogMap &source, - OwningAssetCatalogMap &dest, - AssetCatalogCollection::OnDuplicateCatalogIdFn on_duplicate_items) -{ - for (const auto &orig_catalog_uptr : source.values()) { - if (dest.contains(orig_catalog_uptr->catalog_id)) { - if (on_duplicate_items) { - on_duplicate_items(*dest.lookup(orig_catalog_uptr->catalog_id), *orig_catalog_uptr); - } - continue; - } - - auto copy_catalog_uptr = std::make_unique(*orig_catalog_uptr); - dest.add_new(copy_catalog_uptr->catalog_id, std::move(copy_catalog_uptr)); - } -} - -void AssetCatalogCollection::add_catalogs_from_existing( - const AssetCatalogCollection &other, - AssetCatalogCollection::OnDuplicateCatalogIdFn on_duplicate_items) -{ - copy_catalog_map_into_existing(other.catalogs_, catalogs_, on_duplicate_items); -} - -OwningAssetCatalogMap AssetCatalogCollection::copy_catalog_map(const OwningAssetCatalogMap &orig) -{ - OwningAssetCatalogMap copy; - copy_catalog_map_into_existing( - orig, copy, /*on_duplicate_items=*/[](const AssetCatalog &, const AssetCatalog &) { - /* `copy` was empty before. If this happens it means there was a duplicate in the `orig` - * catalog map which should've been caught already. */ - BLI_assert_unreachable(); - }); - return copy; -} - -/* ---------------------------------------------------------------------- */ - -bool AssetCatalogDefinitionFile::contains(const CatalogID catalog_id) const -{ - return catalogs_.contains(catalog_id); -} - -void AssetCatalogDefinitionFile::add_new(AssetCatalog *catalog) -{ - catalogs_.add_new(catalog->catalog_id, catalog); -} - -void AssetCatalogDefinitionFile::add_overwrite(AssetCatalog *catalog) -{ - catalogs_.add_overwrite(catalog->catalog_id, catalog); -} - -void AssetCatalogDefinitionFile::forget(CatalogID catalog_id) -{ - catalogs_.remove(catalog_id); -} - -void AssetCatalogDefinitionFile::parse_catalog_file( - const CatalogFilePath &catalog_definition_file_path, - AssetCatalogParsedFn catalog_loaded_callback) -{ - fstream infile(catalog_definition_file_path, std::ios::in); - - if (!infile.is_open()) { - CLOG_ERROR(&LOG, "%s: unable to open file", catalog_definition_file_path.c_str()); - return; - } - bool seen_version_number = false; - std::string line; - while (std::getline(infile, line)) { - const StringRef trimmed_line = StringRef(line).trim(); - if (trimmed_line.is_empty() || trimmed_line[0] == '#') { - continue; - } - - if (!seen_version_number) { - /* The very first non-ignored line should be the version declaration. */ - const bool is_valid_version = this->parse_version_line(trimmed_line); - if (!is_valid_version) { - std::cerr << catalog_definition_file_path - << ": first line should be version declaration; ignoring file." << std::endl; - break; - } - seen_version_number = true; - continue; - } - - std::unique_ptr catalog = this->parse_catalog_line(trimmed_line); - if (!catalog) { - continue; - } - - AssetCatalog *non_owning_ptr = catalog.get(); - const bool keep_catalog = catalog_loaded_callback(std::move(catalog)); - if (!keep_catalog) { - continue; - } - - /* The AssetDefinitionFile should include this catalog when writing it back to disk. */ - this->add_overwrite(non_owning_ptr); - } -} - -bool AssetCatalogDefinitionFile::parse_version_line(const StringRef line) -{ - if (!line.startswith(VERSION_MARKER)) { - return false; - } - - const std::string version_string = line.substr(VERSION_MARKER.length()); - const int file_version = std::atoi(version_string.c_str()); - - /* No versioning, just a blunt check whether it's the right one. */ - return file_version == SUPPORTED_VERSION; -} - -std::unique_ptr AssetCatalogDefinitionFile::parse_catalog_line(const StringRef line) -{ - const char delim = ':'; - const int64_t first_delim = line.find_first_of(delim); - if (first_delim == StringRef::not_found) { - std::cerr << "Invalid catalog line in " << this->file_path << ": " << line << std::endl; - return std::unique_ptr(nullptr); - } - - /* Parse the catalog ID. */ - const std::string id_as_string = line.substr(0, first_delim).trim(); - bUUID catalog_id; - const bool uuid_parsed_ok = BLI_uuid_parse_string(&catalog_id, id_as_string.c_str()); - if (!uuid_parsed_ok) { - std::cerr << "Invalid UUID in " << this->file_path << ": " << line << std::endl; - return std::unique_ptr(nullptr); - } - - /* Parse the path and simple name. */ - const StringRef path_and_simple_name = line.substr(first_delim + 1); - const int64_t second_delim = path_and_simple_name.find_first_of(delim); - - std::string path_in_file; - std::string simple_name; - if (second_delim == 0) { - /* Delimiter as first character means there is no path. These lines are to be ignored. */ - return std::unique_ptr(nullptr); - } - - if (second_delim == StringRef::not_found) { - /* No delimiter means no simple name, just treat it as all "path". */ - path_in_file = path_and_simple_name; - simple_name = ""; - } - else { - path_in_file = path_and_simple_name.substr(0, second_delim); - simple_name = path_and_simple_name.substr(second_delim + 1).trim(); - } - - AssetCatalogPath catalog_path = path_in_file; - return std::make_unique(catalog_id, catalog_path.cleanup(), simple_name); -} - -bool AssetCatalogDefinitionFile::write_to_disk() const -{ - BLI_assert_msg(!this->file_path.empty(), "Writing to CDF requires its file path to be known"); - return this->write_to_disk(this->file_path); -} - -bool AssetCatalogDefinitionFile::write_to_disk(const CatalogFilePath &dest_file_path) const -{ - const CatalogFilePath writable_path = dest_file_path + ".writing"; - const CatalogFilePath backup_path = dest_file_path + "~"; - - if (!this->write_to_disk_unsafe(writable_path)) { - /* TODO: communicate what went wrong. */ - return false; - } - if (BLI_exists(dest_file_path.c_str())) { - if (BLI_rename_overwrite(dest_file_path.c_str(), backup_path.c_str())) { - /* TODO: communicate what went wrong. */ - return false; - } - } - if (BLI_rename_overwrite(writable_path.c_str(), dest_file_path.c_str())) { - /* TODO: communicate what went wrong. */ - return false; - } - - return true; -} - -bool AssetCatalogDefinitionFile::write_to_disk_unsafe(const CatalogFilePath &dest_file_path) const -{ - char directory[PATH_MAX]; - BLI_path_split_dir_part(dest_file_path.c_str(), directory, sizeof(directory)); - if (!ensure_directory_exists(directory)) { - /* TODO(Sybren): pass errors to the UI somehow. */ - return false; - } - - fstream output(dest_file_path, std::ios::out); - - /* TODO(@sybren): remember the line ending style that was originally read, then use that to write - * the file again. */ - - /* Write the header. */ - output << HEADER; - output << "" << std::endl; - output << VERSION_MARKER << SUPPORTED_VERSION << std::endl; - output << "" << std::endl; - - /* Write the catalogs, ordered by path (primary) and UUID (secondary). */ - AssetCatalogOrderedSet catalogs_by_path; - for (const AssetCatalog *catalog : catalogs_.values()) { - if (catalog->flags.is_deleted) { - continue; - } - catalogs_by_path.insert(catalog); - } - - for (const AssetCatalog *catalog : catalogs_by_path) { - output << catalog->catalog_id << ":" << catalog->path << ":" << catalog->simple_name - << std::endl; - } - output.close(); - return !output.bad(); -} - -bool AssetCatalogDefinitionFile::ensure_directory_exists( - const CatalogFilePath directory_path) const -{ - /* TODO(@sybren): design a way to get such errors presented to users (or ensure that they never - * occur). */ - if (directory_path.empty()) { - std::cerr - << "AssetCatalogService: no asset library root configured, unable to ensure it exists." - << std::endl; - return false; - } - - if (BLI_exists(directory_path.data())) { - if (!BLI_is_dir(directory_path.data())) { - std::cerr << "AssetCatalogService: " << directory_path - << " exists but is not a directory, this is not a supported situation." - << std::endl; - return false; - } - - /* Root directory exists, work is done. */ - return true; - } - - /* Ensure the root directory exists. */ - std::error_code err_code; - if (!BLI_dir_create_recursive(directory_path.data())) { - std::cerr << "AssetCatalogService: error creating directory " << directory_path << ": " - << err_code << std::endl; - return false; - } - - /* Root directory has been created, work is done. */ - return true; -} - -std::unique_ptr AssetCatalogDefinitionFile::copy_and_remap( - const OwningAssetCatalogMap &catalogs, const OwningAssetCatalogMap &deleted_catalogs) const -{ - auto copy = std::make_unique(*this); - copy->catalogs_.clear(); - - /* Remap pointers of the copy from the original AssetCatalogCollection to the given one. */ - for (CatalogID catalog_id : catalogs_.keys()) { - /* The catalog can be in the regular or the deleted map. */ - const std::unique_ptr *remapped_catalog_uptr_ptr = catalogs.lookup_ptr( - catalog_id); - if (remapped_catalog_uptr_ptr) { - copy->catalogs_.add_new(catalog_id, remapped_catalog_uptr_ptr->get()); - continue; - } - - remapped_catalog_uptr_ptr = deleted_catalogs.lookup_ptr(catalog_id); - if (remapped_catalog_uptr_ptr) { - copy->catalogs_.add_new(catalog_id, remapped_catalog_uptr_ptr->get()); - continue; - } - - BLI_assert(!"A CDF should only reference known catalogs."); - } - - return copy; -} - AssetCatalog::AssetCatalog(const CatalogID catalog_id, const AssetCatalogPath &path, const std::string &simple_name) @@ -1030,6 +714,8 @@ std::string AssetCatalog::sensible_simple_name_for_path(const AssetCatalogPath & return "..." + name.substr(name.length() - 60); } +/* ---------------------------------------------------------------------- */ + AssetCatalogFilter::AssetCatalogFilter(Set &&matching_catalog_ids, Set &&known_catalog_ids) : matching_catalog_ids_(std::move(matching_catalog_ids)), diff --git a/source/blender/asset_system/intern/asset_catalog_collection.cc b/source/blender/asset_system/intern/asset_catalog_collection.cc new file mode 100644 index 00000000000..d3b4922c25f --- /dev/null +++ b/source/blender/asset_system/intern/asset_catalog_collection.cc @@ -0,0 +1,68 @@ +/* SPDX-FileCopyrightText: 2023 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +/** \file + * \ingroup asset_system + */ + +#include "asset_catalog_definition_file.hh" + +#include "asset_catalog_collection.hh" + +namespace blender::asset_system { + +std::unique_ptr AssetCatalogCollection::deep_copy() const +{ + auto copy = std::make_unique(); + + copy->has_unsaved_changes_ = this->has_unsaved_changes_; + copy->catalogs_ = this->copy_catalog_map(this->catalogs_); + copy->deleted_catalogs_ = this->copy_catalog_map(this->deleted_catalogs_); + + if (catalog_definition_file_) { + copy->catalog_definition_file_ = catalog_definition_file_->copy_and_remap( + copy->catalogs_, copy->deleted_catalogs_); + } + + return copy; +} + +static void copy_catalog_map_into_existing( + const OwningAssetCatalogMap &source, + OwningAssetCatalogMap &dest, + AssetCatalogCollection::OnDuplicateCatalogIdFn on_duplicate_items) +{ + for (const auto &orig_catalog_uptr : source.values()) { + if (dest.contains(orig_catalog_uptr->catalog_id)) { + if (on_duplicate_items) { + on_duplicate_items(*dest.lookup(orig_catalog_uptr->catalog_id), *orig_catalog_uptr); + } + continue; + } + + auto copy_catalog_uptr = std::make_unique(*orig_catalog_uptr); + dest.add_new(copy_catalog_uptr->catalog_id, std::move(copy_catalog_uptr)); + } +} + +void AssetCatalogCollection::add_catalogs_from_existing( + const AssetCatalogCollection &other, + AssetCatalogCollection::OnDuplicateCatalogIdFn on_duplicate_items) +{ + copy_catalog_map_into_existing(other.catalogs_, catalogs_, on_duplicate_items); +} + +OwningAssetCatalogMap AssetCatalogCollection::copy_catalog_map(const OwningAssetCatalogMap &orig) +{ + OwningAssetCatalogMap copy; + copy_catalog_map_into_existing( + orig, copy, /*on_duplicate_items=*/[](const AssetCatalog &, const AssetCatalog &) { + /* `copy` was empty before. If this happens it means there was a duplicate in the `orig` + * catalog map which should've been caught already. */ + BLI_assert_unreachable(); + }); + return copy; +} + +} // namespace blender::asset_system diff --git a/source/blender/asset_system/intern/asset_catalog_collection.hh b/source/blender/asset_system/intern/asset_catalog_collection.hh new file mode 100644 index 00000000000..fd45dc2531d --- /dev/null +++ b/source/blender/asset_system/intern/asset_catalog_collection.hh @@ -0,0 +1,61 @@ +/* SPDX-FileCopyrightText: 2023 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +/** \file + * \ingroup asset_system + */ + +#pragma once + +#include "AS_asset_catalog.hh" + +namespace blender::asset_system { + +/** + * All catalogs that are owned by a single asset library, and managed by a single instance of + * #AssetCatalogService. The undo system for asset catalog edits contains historical copies of this + * struct. + */ +class AssetCatalogCollection { + protected: + /** All catalogs known, except the known-but-deleted ones. */ + OwningAssetCatalogMap catalogs_; + + /** Catalogs that have been deleted. They are kept around so that the load-merge-save of catalog + * definition files can actually delete them if they already existed on disk (instead of the + * merge operation resurrecting them). */ + OwningAssetCatalogMap deleted_catalogs_; + + /* For now only a single catalog definition file is supported. + * The aim is to support an arbitrary number of such files per asset library in the future. */ + std::unique_ptr catalog_definition_file_; + + /** Whether any of the catalogs have unsaved changes. */ + bool has_unsaved_changes_ = false; + + friend AssetCatalogService; + + public: + AssetCatalogCollection() = default; + AssetCatalogCollection(const AssetCatalogCollection &other) = delete; + AssetCatalogCollection(AssetCatalogCollection &&other) noexcept = default; + + std::unique_ptr deep_copy() const; + using OnDuplicateCatalogIdFn = + FunctionRef; + /** + * Copy the catalogs from \a other and append them to this collection. Copies no other data + * otherwise. + * + * \note If a catalog from \a other already exists in this collection (identified by catalog ID), + * it will be skipped and \a on_duplicate_items will be called. + */ + void add_catalogs_from_existing(const AssetCatalogCollection &other, + OnDuplicateCatalogIdFn on_duplicate_items); + + protected: + static OwningAssetCatalogMap copy_catalog_map(const OwningAssetCatalogMap &orig); +}; + +} diff --git a/source/blender/asset_system/intern/asset_catalog_definition_file.cc b/source/blender/asset_system/intern/asset_catalog_definition_file.cc new file mode 100644 index 00000000000..9dc2c0efe9e --- /dev/null +++ b/source/blender/asset_system/intern/asset_catalog_definition_file.cc @@ -0,0 +1,284 @@ +/* SPDX-FileCopyrightText: 2023 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +/** \file + * \ingroup asset_system + */ + +#include + +#include "BLI_fileops.hh" +#include "BLI_path_util.h" + +#include "CLG_log.h" + +#include "asset_catalog_definition_file.hh" + +static CLG_LogRef LOG = {"asset_system.asset_catalog_definition_file"}; + +namespace blender::asset_system { + +const int AssetCatalogDefinitionFile::SUPPORTED_VERSION = 1; +const std::string AssetCatalogDefinitionFile::VERSION_MARKER = "VERSION "; + +const std::string AssetCatalogDefinitionFile::HEADER = + "# This is an Asset Catalog Definition file for Blender.\n" + "#\n" + "# Empty lines and lines starting with `#` will be ignored.\n" + "# The first non-ignored line should be the version indicator.\n" + "# Other lines are of the format \"UUID:catalog/path/for/assets:simple catalog name\"\n"; + +bool AssetCatalogDefinitionFile::contains(const CatalogID catalog_id) const +{ + return catalogs_.contains(catalog_id); +} + +void AssetCatalogDefinitionFile::add_new(AssetCatalog *catalog) +{ + catalogs_.add_new(catalog->catalog_id, catalog); +} + +void AssetCatalogDefinitionFile::add_overwrite(AssetCatalog *catalog) +{ + catalogs_.add_overwrite(catalog->catalog_id, catalog); +} + +void AssetCatalogDefinitionFile::forget(CatalogID catalog_id) +{ + catalogs_.remove(catalog_id); +} + +void AssetCatalogDefinitionFile::parse_catalog_file( + const CatalogFilePath &catalog_definition_file_path, + AssetCatalogParsedFn catalog_loaded_callback) +{ + fstream infile(catalog_definition_file_path, std::ios::in); + + if (!infile.is_open()) { + CLOG_ERROR(&LOG, "%s: unable to open file", catalog_definition_file_path.c_str()); + return; + } + bool seen_version_number = false; + std::string line; + while (std::getline(infile, line)) { + const StringRef trimmed_line = StringRef(line).trim(); + if (trimmed_line.is_empty() || trimmed_line[0] == '#') { + continue; + } + + if (!seen_version_number) { + /* The very first non-ignored line should be the version declaration. */ + const bool is_valid_version = this->parse_version_line(trimmed_line); + if (!is_valid_version) { + std::cerr << catalog_definition_file_path + << ": first line should be version declaration; ignoring file." << std::endl; + break; + } + seen_version_number = true; + continue; + } + + std::unique_ptr catalog = this->parse_catalog_line(trimmed_line); + if (!catalog) { + continue; + } + + AssetCatalog *non_owning_ptr = catalog.get(); + const bool keep_catalog = catalog_loaded_callback(std::move(catalog)); + if (!keep_catalog) { + continue; + } + + /* The AssetDefinitionFile should include this catalog when writing it back to disk. */ + this->add_overwrite(non_owning_ptr); + } +} + +bool AssetCatalogDefinitionFile::parse_version_line(const StringRef line) +{ + if (!line.startswith(VERSION_MARKER)) { + return false; + } + + const std::string version_string = line.substr(VERSION_MARKER.length()); + const int file_version = std::atoi(version_string.c_str()); + + /* No versioning, just a blunt check whether it's the right one. */ + return file_version == SUPPORTED_VERSION; +} + +std::unique_ptr AssetCatalogDefinitionFile::parse_catalog_line(const StringRef line) +{ + const char delim = ':'; + const int64_t first_delim = line.find_first_of(delim); + if (first_delim == StringRef::not_found) { + std::cerr << "Invalid catalog line in " << this->file_path << ": " << line << std::endl; + return std::unique_ptr(nullptr); + } + + /* Parse the catalog ID. */ + const std::string id_as_string = line.substr(0, first_delim).trim(); + bUUID catalog_id; + const bool uuid_parsed_ok = BLI_uuid_parse_string(&catalog_id, id_as_string.c_str()); + if (!uuid_parsed_ok) { + std::cerr << "Invalid UUID in " << this->file_path << ": " << line << std::endl; + return std::unique_ptr(nullptr); + } + + /* Parse the path and simple name. */ + const StringRef path_and_simple_name = line.substr(first_delim + 1); + const int64_t second_delim = path_and_simple_name.find_first_of(delim); + + std::string path_in_file; + std::string simple_name; + if (second_delim == 0) { + /* Delimiter as first character means there is no path. These lines are to be ignored. */ + return std::unique_ptr(nullptr); + } + + if (second_delim == StringRef::not_found) { + /* No delimiter means no simple name, just treat it as all "path". */ + path_in_file = path_and_simple_name; + simple_name = ""; + } + else { + path_in_file = path_and_simple_name.substr(0, second_delim); + simple_name = path_and_simple_name.substr(second_delim + 1).trim(); + } + + AssetCatalogPath catalog_path = path_in_file; + return std::make_unique(catalog_id, catalog_path.cleanup(), simple_name); +} + +bool AssetCatalogDefinitionFile::write_to_disk() const +{ + BLI_assert_msg(!this->file_path.empty(), "Writing to CDF requires its file path to be known"); + return this->write_to_disk(this->file_path); +} + +bool AssetCatalogDefinitionFile::write_to_disk(const CatalogFilePath &dest_file_path) const +{ + const CatalogFilePath writable_path = dest_file_path + ".writing"; + const CatalogFilePath backup_path = dest_file_path + "~"; + + if (!this->write_to_disk_unsafe(writable_path)) { + /* TODO: communicate what went wrong. */ + return false; + } + if (BLI_exists(dest_file_path.c_str())) { + if (BLI_rename_overwrite(dest_file_path.c_str(), backup_path.c_str())) { + /* TODO: communicate what went wrong. */ + return false; + } + } + if (BLI_rename_overwrite(writable_path.c_str(), dest_file_path.c_str())) { + /* TODO: communicate what went wrong. */ + return false; + } + + return true; +} + +bool AssetCatalogDefinitionFile::write_to_disk_unsafe(const CatalogFilePath &dest_file_path) const +{ + char directory[PATH_MAX]; + BLI_path_split_dir_part(dest_file_path.c_str(), directory, sizeof(directory)); + if (!ensure_directory_exists(directory)) { + /* TODO(Sybren): pass errors to the UI somehow. */ + return false; + } + + fstream output(dest_file_path, std::ios::out); + + /* TODO(@sybren): remember the line ending style that was originally read, then use that to write + * the file again. */ + + /* Write the header. */ + output << HEADER; + output << "" << std::endl; + output << VERSION_MARKER << SUPPORTED_VERSION << std::endl; + output << "" << std::endl; + + /* Write the catalogs, ordered by path (primary) and UUID (secondary). */ + AssetCatalogOrderedSet catalogs_by_path; + for (const AssetCatalog *catalog : catalogs_.values()) { + if (catalog->flags.is_deleted) { + continue; + } + catalogs_by_path.insert(catalog); + } + + for (const AssetCatalog *catalog : catalogs_by_path) { + output << catalog->catalog_id << ":" << catalog->path << ":" << catalog->simple_name + << std::endl; + } + output.close(); + return !output.bad(); +} + +bool AssetCatalogDefinitionFile::ensure_directory_exists( + const CatalogFilePath directory_path) const +{ + /* TODO(@sybren): design a way to get such errors presented to users (or ensure that they never + * occur). */ + if (directory_path.empty()) { + std::cerr + << "AssetCatalogService: no asset library root configured, unable to ensure it exists." + << std::endl; + return false; + } + + if (BLI_exists(directory_path.data())) { + if (!BLI_is_dir(directory_path.data())) { + std::cerr << "AssetCatalogService: " << directory_path + << " exists but is not a directory, this is not a supported situation." + << std::endl; + return false; + } + + /* Root directory exists, work is done. */ + return true; + } + + /* Ensure the root directory exists. */ + std::error_code err_code; + if (!BLI_dir_create_recursive(directory_path.data())) { + std::cerr << "AssetCatalogService: error creating directory " << directory_path << ": " + << err_code << std::endl; + return false; + } + + /* Root directory has been created, work is done. */ + return true; +} + +std::unique_ptr AssetCatalogDefinitionFile::copy_and_remap( + const OwningAssetCatalogMap &catalogs, const OwningAssetCatalogMap &deleted_catalogs) const +{ + auto copy = std::make_unique(*this); + copy->catalogs_.clear(); + + /* Remap pointers of the copy from the original AssetCatalogCollection to the given one. */ + for (CatalogID catalog_id : catalogs_.keys()) { + /* The catalog can be in the regular or the deleted map. */ + const std::unique_ptr *remapped_catalog_uptr_ptr = catalogs.lookup_ptr( + catalog_id); + if (remapped_catalog_uptr_ptr) { + copy->catalogs_.add_new(catalog_id, remapped_catalog_uptr_ptr->get()); + continue; + } + + remapped_catalog_uptr_ptr = deleted_catalogs.lookup_ptr(catalog_id); + if (remapped_catalog_uptr_ptr) { + copy->catalogs_.add_new(catalog_id, remapped_catalog_uptr_ptr->get()); + continue; + } + + BLI_assert(!"A CDF should only reference known catalogs."); + } + + return copy; +} + +} // namespace blender::asset_system diff --git a/source/blender/asset_system/intern/asset_catalog_definition_file.hh b/source/blender/asset_system/intern/asset_catalog_definition_file.hh new file mode 100644 index 00000000000..78e202b5c13 --- /dev/null +++ b/source/blender/asset_system/intern/asset_catalog_definition_file.hh @@ -0,0 +1,87 @@ +/* SPDX-FileCopyrightText: 2023 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +/** \file + * \ingroup asset_system + * + * Classes internal to the asset system for asset catalog management. + */ + +#pragma once + +#include "AS_asset_catalog.hh" + +#include "BLI_string_ref.hh" + +namespace blender::asset_system { + +/** + * Keeps track of which catalogs are defined in a certain file on disk. + * Only contains non-owning pointers to the #AssetCatalog instances, so ensure the lifetime of this + * class is shorter than that of the #`AssetCatalog`s themselves. + */ +class AssetCatalogDefinitionFile { + protected: + /* Catalogs stored in this file. They are mapped by ID to make it possible to query whether a + * catalog is already known, without having to find the corresponding `AssetCatalog*`. */ + Map catalogs_; + + public: + /* For now this is the only version of the catalog definition files that is supported. + * Later versioning code may be added to handle older files. */ + const static int SUPPORTED_VERSION; + /* String that's matched in the catalog definition file to know that the line is the version + * declaration. It has to start with a space to ensure it won't match any hypothetical future + * field that starts with "VERSION". */ + const static std::string VERSION_MARKER; + const static std::string HEADER; + + CatalogFilePath file_path; + + public: + AssetCatalogDefinitionFile() = default; + + /** + * Write the catalog definitions to the same file they were read from. + * Return true when the file was written correctly, false when there was a problem. + */ + bool write_to_disk() const; + /** + * Write the catalog definitions to an arbitrary file path. + * + * Any existing file is backed up to "filename~". Any previously existing backup is overwritten. + * + * Return true when the file was written correctly, false when there was a problem. + */ + bool write_to_disk(const CatalogFilePath &dest_file_path) const; + + bool contains(CatalogID catalog_id) const; + /** Add a catalog, overwriting the one with the same catalog ID. */ + void add_overwrite(AssetCatalog *catalog); + /** Add a new catalog. Undefined behavior if a catalog with the same ID was already added. */ + void add_new(AssetCatalog *catalog); + + /** Remove the catalog from the collection of catalogs stored in this file. */ + void forget(CatalogID catalog_id); + + using AssetCatalogParsedFn = FunctionRef)>; + void parse_catalog_file(const CatalogFilePath &catalog_definition_file_path, + AssetCatalogParsedFn callback); + + std::unique_ptr copy_and_remap( + const OwningAssetCatalogMap &catalogs, const OwningAssetCatalogMap &deleted_catalogs) const; + + protected: + bool parse_version_line(StringRef line); + std::unique_ptr parse_catalog_line(StringRef line); + + /** + * Write the catalog definitions to the given file path. + * Return true when the file was written correctly, false when there was a problem. + */ + bool write_to_disk_unsafe(const CatalogFilePath &dest_file_path) const; + bool ensure_directory_exists(const CatalogFilePath directory_path) const; +}; + +} // namespace blender::asset_system diff --git a/source/blender/asset_system/intern/asset_library.cc b/source/blender/asset_system/intern/asset_library.cc index 230ed39cb45..f7f46c03975 100644 --- a/source/blender/asset_system/intern/asset_library.cc +++ b/source/blender/asset_system/intern/asset_library.cc @@ -24,6 +24,8 @@ #include "DNA_userdef_types.h" +#include "asset_catalog_collection.hh" +#include "asset_catalog_definition_file.hh" #include "asset_library_service.hh" #include "asset_storage.hh" #include "utils.hh" diff --git a/source/blender/asset_system/intern/asset_library_all.cc b/source/blender/asset_system/intern/asset_library_all.cc index 4eae966ef9b..22a4d3a2cce 100644 --- a/source/blender/asset_system/intern/asset_library_all.cc +++ b/source/blender/asset_system/intern/asset_library_all.cc @@ -9,6 +9,8 @@ #include #include "AS_asset_catalog_tree.hh" +#include "asset_catalog_collection.hh" +#include "asset_catalog_definition_file.hh" #include "asset_library_all.hh" diff --git a/source/blender/asset_system/tests/asset_catalog_test.cc b/source/blender/asset_system/tests/asset_catalog_test.cc index c765d6d5cd4..c59f28bc7bd 100644 --- a/source/blender/asset_system/tests/asset_catalog_test.cc +++ b/source/blender/asset_system/tests/asset_catalog_test.cc @@ -4,6 +4,8 @@ #include "AS_asset_catalog.hh" #include "AS_asset_catalog_tree.hh" +#include "asset_catalog_collection.hh" +#include "asset_catalog_definition_file.hh" #include "BKE_preferences.h" diff --git a/source/blender/asset_system/tests/asset_catalog_tree_test.cc b/source/blender/asset_system/tests/asset_catalog_tree_test.cc index dcf002062fd..d6001d3b495 100644 --- a/source/blender/asset_system/tests/asset_catalog_tree_test.cc +++ b/source/blender/asset_system/tests/asset_catalog_tree_test.cc @@ -4,6 +4,8 @@ #include "AS_asset_catalog.hh" #include "AS_asset_catalog_tree.hh" +#include "asset_catalog_collection.hh" +#include "asset_catalog_definition_file.hh" #include "BLI_path_util.h"