Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/source
diff options
context:
space:
mode:
Diffstat (limited to 'source')
-rw-r--r--source/blender/blenkernel/BKE_asset.h8
-rw-r--r--source/blender/blenkernel/BKE_asset_catalog.hh252
-rw-r--r--source/blender/blenkernel/BKE_asset_library.h36
-rw-r--r--source/blender/blenkernel/BKE_asset_library.hh41
-rw-r--r--source/blender/blenkernel/CMakeLists.txt8
-rw-r--r--source/blender/blenkernel/intern/asset.cc25
-rw-r--r--source/blender/blenkernel/intern/asset_catalog.cc573
-rw-r--r--source/blender/blenkernel/intern/asset_catalog_test.cc443
-rw-r--r--source/blender/blenkernel/intern/asset_library.cc53
-rw-r--r--source/blender/blenkernel/intern/asset_library_test.cc82
-rw-r--r--source/blender/blenkernel/intern/asset_test.cc70
-rw-r--r--source/blender/blenlib/BLI_uuid.h15
-rw-r--r--source/blender/blenlib/intern/uuid.cc32
-rw-r--r--source/blender/editors/space_file/CMakeLists.txt1
-rw-r--r--source/blender/editors/space_file/filelist.c27
-rw-r--r--source/blender/makesdna/DNA_asset_types.h14
-rw-r--r--source/blender/makesdna/DNA_uuid_types.h6
-rw-r--r--source/blender/makesrna/intern/rna_asset.c56
18 files changed, 1742 insertions, 0 deletions
diff --git a/source/blender/blenkernel/BKE_asset.h b/source/blender/blenkernel/BKE_asset.h
index 50eb2859279..7ae5378272d 100644
--- a/source/blender/blenkernel/BKE_asset.h
+++ b/source/blender/blenkernel/BKE_asset.h
@@ -22,6 +22,8 @@
#include "BLI_utildefines.h"
+#include "DNA_asset_types.h"
+
#ifdef __cplusplus
extern "C" {
#endif
@@ -46,6 +48,12 @@ struct AssetTagEnsureResult BKE_asset_metadata_tag_ensure(struct AssetMetaData *
const char *name);
void BKE_asset_metadata_tag_remove(struct AssetMetaData *asset_data, struct AssetTag *tag);
+/** Clean up the catalog ID (whitespaces removed, length reduced, etc.) and assign it. */
+void BKE_asset_metadata_catalog_id_clear(struct AssetMetaData *asset_data);
+void BKE_asset_metadata_catalog_id_set(struct AssetMetaData *asset_data,
+ bUUID catalog_id,
+ const char *catalog_simple_name);
+
void BKE_asset_library_reference_init_default(struct AssetLibraryReference *library_ref);
struct PreviewImage *BKE_asset_metadata_preview_get_from_id(const struct AssetMetaData *asset_data,
diff --git a/source/blender/blenkernel/BKE_asset_catalog.hh b/source/blender/blenkernel/BKE_asset_catalog.hh
new file mode 100644
index 00000000000..2bbaa4b4222
--- /dev/null
+++ b/source/blender/blenkernel/BKE_asset_catalog.hh
@@ -0,0 +1,252 @@
+/*
+ * 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 2
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+/** \file
+ * \ingroup bke
+ */
+
+#pragma once
+
+#ifndef __cplusplus
+# error This is a C++ header. The C interface is yet to be implemented/designed.
+#endif
+
+#include "BLI_function_ref.hh"
+#include "BLI_map.hh"
+#include "BLI_string_ref.hh"
+#include "BLI_uuid.h"
+#include "BLI_vector.hh"
+
+#include <map>
+#include <memory>
+#include <string>
+
+namespace blender::bke {
+
+using CatalogID = bUUID;
+using CatalogPath = std::string;
+using CatalogPathComponent = std::string;
+/* Would be nice to be able to use `std::filesystem::path` for this, but it's currently not
+ * available on the minimum macOS target version. */
+using CatalogFilePath = std::string;
+
+class AssetCatalog;
+class AssetCatalogDefinitionFile;
+class AssetCatalogTree;
+
+/* Manages the asset catalogs of a single asset library (i.e. of catalogs defined in a single
+ * directory hierarchy). */
+class AssetCatalogService {
+ public:
+ static const char PATH_SEPARATOR;
+ static const CatalogFilePath DEFAULT_CATALOG_FILENAME;
+
+ public:
+ AssetCatalogService() = default;
+ explicit AssetCatalogService(const CatalogFilePath &asset_library_root);
+
+ /** Load asset catalog definitions from the files found in the asset library. */
+ void load_from_disk();
+ /** Load asset catalog definitions from the given file or directory. */
+ void load_from_disk(const CatalogFilePath &file_or_directory_path);
+
+ /**
+ * Write the catalog definitions to disk.
+ * The provided directory path is only used when there is no CDF loaded from disk yet but assets
+ * still have to be saved.
+ *
+ * Return true on success, which either means there were no in-memory categories to save, or the
+ * save was succesfful. */
+ bool write_to_disk(const CatalogFilePath &directory_for_new_files);
+
+ /**
+ * Merge on-disk changes into the in-memory asset catalogs.
+ * This should be called before writing the asset catalogs to disk.
+ *
+ * - New on-disk catalogs are loaded into memory.
+ * - Already-known on-disk catalogs are ignored (so will be overwritten with our in-memory
+ * data). This includes in-memory marked-as-deleted catalogs.
+ */
+ void merge_from_disk_before_writing();
+
+ /** Return catalog with the given ID. Return nullptr if not found. */
+ AssetCatalog *find_catalog(CatalogID catalog_id);
+
+ /** Create a catalog with some sensible auto-generated catalog ID.
+ * The catalog will be saved to the default catalog file.*/
+ AssetCatalog *create_catalog(const CatalogPath &catalog_path);
+
+ /**
+ * Soft-delete the catalog, ensuring it actually gets deleted when the catalog definition file is
+ * written. */
+ void delete_catalog(CatalogID catalog_id);
+
+ AssetCatalogTree *get_catalog_tree();
+
+ /** Return true iff there are no catalogs known. */
+ bool is_empty() const;
+
+ protected:
+ /* These pointers are owned by this AssetCatalogService. */
+ Map<CatalogID, std::unique_ptr<AssetCatalog>> catalogs_;
+ Map<CatalogID, std::unique_ptr<AssetCatalog>> deleted_catalogs_;
+ std::unique_ptr<AssetCatalogDefinitionFile> catalog_definition_file_;
+ std::unique_ptr<AssetCatalogTree> catalog_tree_;
+ CatalogFilePath asset_library_root_;
+
+ void load_directory_recursive(const CatalogFilePath &directory_path);
+ void load_single_file(const CatalogFilePath &catalog_definition_file_path);
+
+ std::unique_ptr<AssetCatalogDefinitionFile> parse_catalog_file(
+ const CatalogFilePath &catalog_definition_file_path);
+
+ /**
+ * Construct an in-memory catalog definition file (CDF) from the currently known catalogs.
+ * This object can then be processed further before saving to disk. */
+ std::unique_ptr<AssetCatalogDefinitionFile> construct_cdf_in_memory(
+ const CatalogFilePath &file_path);
+
+ std::unique_ptr<AssetCatalogTree> read_into_tree();
+ void rebuild_tree();
+};
+
+class AssetCatalogTreeItem {
+ friend class AssetCatalogService;
+
+ public:
+ using ChildMap = std::map<std::string, AssetCatalogTreeItem>;
+ using ItemIterFn = FunctionRef<void(const AssetCatalogTreeItem &)>;
+
+ AssetCatalogTreeItem(StringRef name, const AssetCatalogTreeItem *parent = nullptr);
+
+ StringRef get_name() const;
+ /** Return the full catalog path, defined as the name of this catalog prefixed by the full
+ * catalog path of its parent and a separator. */
+ CatalogPath catalog_path() const;
+ int count_parents() const;
+
+ static void foreach_item_recursive(const ChildMap &children_, const ItemIterFn callback);
+
+ protected:
+ /** Child tree items, ordered by their names. */
+ ChildMap children_;
+ /** The user visible name of this component. */
+ CatalogPathComponent name_;
+
+ /** Pointer back to the parent item. Used to reconstruct the hierarchy from an item (e.g. to
+ * build a path). */
+ const AssetCatalogTreeItem *parent_ = nullptr;
+};
+
+/**
+ * A representation of the catalog paths as tree structure. Each component of the catalog tree is
+ * represented by a #AssetCatalogTreeItem.
+ * There is no single root tree element, the #AssetCatalogTree instance itself represents the root.
+ */
+class AssetCatalogTree {
+ friend class AssetCatalogService;
+
+ public:
+ void foreach_item(const AssetCatalogTreeItem::ItemIterFn callback) const;
+
+ protected:
+ /** Child tree items, ordered by their names. */
+ AssetCatalogTreeItem::ChildMap children_;
+};
+
+/** 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 {
+ public:
+ CatalogFilePath file_path;
+
+ 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 new catalog. Undefined behaviour if a catalog with the same ID was already added. */
+ void add_new(AssetCatalog *catalog);
+
+ using AssetCatalogParsedFn = FunctionRef<bool(std::unique_ptr<AssetCatalog>)>;
+ void parse_catalog_file(const CatalogFilePath &catalog_definition_file_path,
+ AssetCatalogParsedFn callback);
+
+ 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<CatalogID, AssetCatalog *> catalogs_;
+
+ std::unique_ptr<AssetCatalog> 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. */
+class AssetCatalog {
+ public:
+ AssetCatalog() = default;
+ AssetCatalog(CatalogID catalog_id, const CatalogPath &path, const std::string &simple_name);
+
+ CatalogID catalog_id;
+ CatalogPath path;
+ /**
+ * Simple, human-readable name for the asset catalog. This is stored on assets alongside the
+ * catalog ID; the catalog ID is a UUID that is not human-readable, so to avoid complete dataloss
+ * when the catalog definition file gets lost, we also store a human-readable simple name for the
+ * catalog. */
+ std::string simple_name;
+
+ struct Flags {
+ /* Treat this catalog as deleted. Keeping deleted catalogs around is necessary to support
+ * merging of on-disk changes with in-memory changes. */
+ bool is_deleted = false;
+ } flags;
+
+ /**
+ * Create a new Catalog with the given path, auto-generating a sensible catalog simplename.
+ *
+ * NOTE: the given path will be cleaned up (trailing spaces removed, etc.), so the returned
+ * `AssetCatalog`'s path differ from the given one.
+ */
+ static std::unique_ptr<AssetCatalog> from_path(const CatalogPath &path);
+ static CatalogPath cleanup_path(const CatalogPath &path);
+
+ protected:
+ /** Generate a sensible catalog ID for the given path. */
+ static std::string sensible_simple_name_for_path(const CatalogPath &path);
+};
+
+} // namespace blender::bke
diff --git a/source/blender/blenkernel/BKE_asset_library.h b/source/blender/blenkernel/BKE_asset_library.h
new file mode 100644
index 00000000000..709b915f9ff
--- /dev/null
+++ b/source/blender/blenkernel/BKE_asset_library.h
@@ -0,0 +1,36 @@
+/*
+ * 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 2
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+/** \file
+ * \ingroup bke
+ */
+
+#pragma once
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** Forward declaration, defined in intern/asset_library.hh */
+typedef struct AssetLibrary AssetLibrary;
+
+/** TODO(@sybren): properly have a think/discussion about the API for this. */
+struct AssetLibrary *BKE_asset_library_load(const char *library_path);
+void BKE_asset_library_free(struct AssetLibrary *asset_library);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/source/blender/blenkernel/BKE_asset_library.hh b/source/blender/blenkernel/BKE_asset_library.hh
new file mode 100644
index 00000000000..68f7481574e
--- /dev/null
+++ b/source/blender/blenkernel/BKE_asset_library.hh
@@ -0,0 +1,41 @@
+/*
+ * 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 2
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+/** \file
+ * \ingroup bke
+ */
+
+#pragma once
+
+#ifndef __cplusplus
+# error This is a C++-only header file. Use BKE_asset_library.h instead.
+#endif
+
+#include "BKE_asset_library.h"
+
+#include "BKE_asset_catalog.hh"
+
+#include <memory>
+
+namespace blender::bke {
+
+struct AssetLibrary {
+ std::unique_ptr<AssetCatalogService> catalog_service;
+
+ void load(StringRefNull library_root_directory);
+};
+
+} // namespace blender::bke
diff --git a/source/blender/blenkernel/CMakeLists.txt b/source/blender/blenkernel/CMakeLists.txt
index de7864ef36a..d04d4558fed 100644
--- a/source/blender/blenkernel/CMakeLists.txt
+++ b/source/blender/blenkernel/CMakeLists.txt
@@ -83,6 +83,8 @@ set(SRC
intern/armature_pose.cc
intern/armature_selection.cc
intern/armature_update.c
+ intern/asset_catalog.cc
+ intern/asset_library.cc
intern/asset.cc
intern/attribute.c
intern/attribute_access.cc
@@ -302,6 +304,9 @@ set(SRC
BKE_appdir.h
BKE_armature.h
BKE_armature.hh
+ BKE_asset_catalog.hh
+ BKE_asset_library.h
+ BKE_asset_library.hh
BKE_asset.h
BKE_attribute.h
BKE_attribute_access.hh
@@ -783,6 +788,9 @@ if(WITH_GTESTS)
set(TEST_SRC
intern/action_test.cc
intern/armature_test.cc
+ intern/asset_catalog_test.cc
+ intern/asset_library_test.cc
+ intern/asset_test.cc
intern/cryptomatte_test.cc
intern/fcurve_test.cc
intern/lattice_deform_test.cc
diff --git a/source/blender/blenkernel/intern/asset.cc b/source/blender/blenkernel/intern/asset.cc
index f74018b20c5..ac9bcccc8bd 100644
--- a/source/blender/blenkernel/intern/asset.cc
+++ b/source/blender/blenkernel/intern/asset.cc
@@ -26,8 +26,10 @@
#include "BLI_listbase.h"
#include "BLI_string.h"
+#include "BLI_string_ref.hh"
#include "BLI_string_utils.h"
#include "BLI_utildefines.h"
+#include "BLI_uuid.h"
#include "BKE_asset.h"
#include "BKE_icons.h"
@@ -37,6 +39,8 @@
#include "MEM_guardedalloc.h"
+using namespace blender;
+
AssetMetaData *BKE_asset_metadata_create(void)
{
AssetMetaData *asset_data = (AssetMetaData *)MEM_callocN(sizeof(*asset_data), __func__);
@@ -115,6 +119,27 @@ void BKE_asset_library_reference_init_default(AssetLibraryReference *library_ref
memcpy(library_ref, DNA_struct_default_get(AssetLibraryReference), sizeof(*library_ref));
}
+void BKE_asset_metadata_catalog_id_clear(struct AssetMetaData *asset_data)
+{
+ asset_data->catalog_id = BLI_uuid_nil();
+ asset_data->catalog_simple_name[0] = '\0';
+}
+
+void BKE_asset_metadata_catalog_id_set(struct AssetMetaData *asset_data,
+ const bUUID catalog_id,
+ const char *catalog_simple_name)
+{
+ asset_data->catalog_id = catalog_id;
+
+ constexpr size_t max_simple_name_length = sizeof(asset_data->catalog_simple_name);
+
+ /* The substr() call is necessary to make copy() copy the first N characters (instead of refusing
+ * to copy and producing an empty string). */
+ StringRef trimmed_id =
+ StringRef(catalog_simple_name).trim().substr(0, max_simple_name_length - 1);
+ trimmed_id.copy(asset_data->catalog_simple_name, max_simple_name_length);
+}
+
/* Queries -------------------------------------------- */
PreviewImage *BKE_asset_metadata_preview_get_from_id(const AssetMetaData *UNUSED(asset_data),
diff --git a/source/blender/blenkernel/intern/asset_catalog.cc b/source/blender/blenkernel/intern/asset_catalog.cc
new file mode 100644
index 00000000000..0c64a0b085c
--- /dev/null
+++ b/source/blender/blenkernel/intern/asset_catalog.cc
@@ -0,0 +1,573 @@
+/*
+ * 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 2
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+/** \file
+ * \ingroup bke
+ */
+
+#include "BKE_asset_catalog.hh"
+
+#include "BLI_fileops.h"
+#include "BLI_path_util.h"
+#include "BLI_string_ref.hh"
+
+/* For S_ISREG() and S_ISDIR() on Windows. */
+#ifdef WIN32
+# include "BLI_winstuff.h"
+#endif
+
+#include <fstream>
+
+namespace blender::bke {
+
+const char AssetCatalogService::PATH_SEPARATOR = '/';
+const CatalogFilePath AssetCatalogService::DEFAULT_CATALOG_FILENAME = "blender_assets.cats.txt";
+
+AssetCatalogService::AssetCatalogService(const CatalogFilePath &asset_library_root)
+ : asset_library_root_(asset_library_root)
+{
+}
+
+bool AssetCatalogService::is_empty() const
+{
+ return catalogs_.is_empty();
+}
+
+AssetCatalog *AssetCatalogService::find_catalog(CatalogID catalog_id)
+{
+ std::unique_ptr<AssetCatalog> *catalog_uptr_ptr = this->catalogs_.lookup_ptr(catalog_id);
+ if (catalog_uptr_ptr == nullptr) {
+ return nullptr;
+ }
+ return catalog_uptr_ptr->get();
+}
+
+void AssetCatalogService::delete_catalog(CatalogID catalog_id)
+{
+ std::unique_ptr<AssetCatalog> *catalog_uptr_ptr = this->catalogs_.lookup_ptr(catalog_id);
+ if (catalog_uptr_ptr == nullptr) {
+ /* Catalog cannot be found, which is fine. */
+ return;
+ }
+
+ /* Mark the catalog as deleted. */
+ AssetCatalog *catalog = catalog_uptr_ptr->get();
+ catalog->flags.is_deleted = true;
+
+ /* Move ownership from this->catalogs_ to this->deleted_catalogs_. */
+ this->deleted_catalogs_.add(catalog_id, std::move(*catalog_uptr_ptr));
+
+ /* The catalog can now be removed from the map without freeing the actual AssetCatalog. */
+ this->catalogs_.remove(catalog_id);
+
+ this->rebuild_tree();
+}
+
+AssetCatalog *AssetCatalogService::create_catalog(const CatalogPath &catalog_path)
+{
+ std::unique_ptr<AssetCatalog> catalog = AssetCatalog::from_path(catalog_path);
+
+ /* So we can std::move(catalog) and still use the non-owning pointer: */
+ AssetCatalog *const catalog_ptr = catalog.get();
+
+ /* TODO(@sybren): move the `AssetCatalog::from_path()` function to another place, that can reuse
+ * catalogs when a catalog with the given path is already known, and avoid duplicate catalog IDs.
+ */
+ BLI_assert_msg(!catalogs_.contains(catalog->catalog_id), "duplicate catalog ID not supported");
+ catalogs_.add_new(catalog->catalog_id, std::move(catalog));
+
+ if (catalog_definition_file_) {
+ /* Ensure the new catalog gets written to disk at some point. If there is no CDF in memory yet,
+ * it's enough to have the catalog known to the service as it'll be saved to a new file. */
+ catalog_definition_file_->add_new(catalog_ptr);
+ }
+
+ return catalog_ptr;
+}
+
+static std::string asset_definition_default_file_path_from_dir(StringRef asset_library_root)
+{
+ char file_path[PATH_MAX];
+ BLI_join_dirfile(file_path,
+ sizeof(file_path),
+ asset_library_root.data(),
+ AssetCatalogService::DEFAULT_CATALOG_FILENAME.data());
+ return file_path;
+}
+
+void AssetCatalogService::load_from_disk()
+{
+ load_from_disk(asset_library_root_);
+}
+
+void AssetCatalogService::load_from_disk(const CatalogFilePath &file_or_directory_path)
+{
+ BLI_stat_t status;
+ if (BLI_stat(file_or_directory_path.data(), &status) == -1) {
+ // TODO(@sybren): throw an appropriate exception.
+ return;
+ }
+
+ if (S_ISREG(status.st_mode)) {
+ load_single_file(file_or_directory_path);
+ }
+ else if (S_ISDIR(status.st_mode)) {
+ load_directory_recursive(file_or_directory_path);
+ }
+ else {
+ // TODO(@sybren): throw an appropriate exception.
+ }
+
+ /* TODO: Should there be a sanitize step? E.g. to remove catalogs with identical paths? */
+
+ catalog_tree_ = read_into_tree();
+}
+
+void AssetCatalogService::load_directory_recursive(const CatalogFilePath &directory_path)
+{
+ // TODO(@sybren): implement proper multi-file support. For now, just load
+ // the default file if it is there.
+ CatalogFilePath file_path = asset_definition_default_file_path_from_dir(directory_path);
+
+ if (!BLI_exists(file_path.data())) {
+ /* No file to be loaded is perfectly fine. */
+ return;
+ }
+
+ this->load_single_file(file_path);
+}
+
+void AssetCatalogService::load_single_file(const CatalogFilePath &catalog_definition_file_path)
+{
+ /* TODO(@sybren): check that #catalog_definition_file_path is contained in #asset_library_root_,
+ * otherwise some assumptions may fail. */
+ std::unique_ptr<AssetCatalogDefinitionFile> cdf = parse_catalog_file(
+ catalog_definition_file_path);
+
+ BLI_assert_msg(!this->catalog_definition_file_,
+ "Only loading of a single catalog definition file is supported.");
+ this->catalog_definition_file_ = std::move(cdf);
+}
+
+std::unique_ptr<AssetCatalogDefinitionFile> AssetCatalogService::parse_catalog_file(
+ const CatalogFilePath &catalog_definition_file_path)
+{
+ auto cdf = std::make_unique<AssetCatalogDefinitionFile>();
+ cdf->file_path = catalog_definition_file_path;
+
+ auto catalog_parsed_callback = [this, catalog_definition_file_path](
+ std::unique_ptr<AssetCatalog> catalog) {
+ if (this->catalogs_.contains(catalog->catalog_id)) {
+ // TODO(@sybren): apparently another CDF was already loaded. This is not supported yet.
+ std::cerr << catalog_definition_file_path << ": multiple definitions of catalog "
+ << catalog->catalog_id << " in multiple files, ignoring this one." << std::endl;
+ /* Don't store 'catalog'; unique_ptr will free its memory. */
+ return false;
+ }
+
+ /* The AssetCatalog pointer is now owned by the AssetCatalogService. */
+ this->catalogs_.add_new(catalog->catalog_id, std::move(catalog));
+ return true;
+ };
+
+ cdf->parse_catalog_file(cdf->file_path, catalog_parsed_callback);
+
+ return cdf;
+}
+
+void AssetCatalogService::merge_from_disk_before_writing()
+{
+ /* TODO(Sybren): expand to support multiple CDFs. */
+
+ if (!catalog_definition_file_ || catalog_definition_file_->file_path.empty() ||
+ !BLI_is_file(catalog_definition_file_->file_path.c_str())) {
+ return;
+ }
+
+ auto catalog_parsed_callback = [this](std::unique_ptr<AssetCatalog> catalog) {
+ const bUUID catalog_id = catalog->catalog_id;
+
+ /* The following two conditions could be or'ed together. Keeping them separated helps when
+ * adding debug prints, breakpoints, etc. */
+ if (this->catalogs_.contains(catalog_id)) {
+ /* This catalog was already seen, so just ignore it. */
+ return false;
+ }
+ if (this->deleted_catalogs_.contains(catalog_id)) {
+ /* This catalog was already seen and subsequently deleted, so just ignore it. */
+ return false;
+ }
+
+ /* This is a new catalog, so let's keep it around. */
+ this->catalogs_.add_new(catalog_id, std::move(catalog));
+ return true;
+ };
+
+ catalog_definition_file_->parse_catalog_file(catalog_definition_file_->file_path,
+ catalog_parsed_callback);
+}
+
+bool AssetCatalogService::write_to_disk(const CatalogFilePath &directory_for_new_files)
+{
+ /* TODO(Sybren): expand to support multiple CDFs. */
+
+ if (!catalog_definition_file_) {
+ if (catalogs_.is_empty() && deleted_catalogs_.is_empty()) {
+ /* Avoid saving anything, when there is nothing to save. */
+ return true; /* Writing nothing when there is nothing to write is still a success. */
+ }
+
+ /* A CDF has to be created to contain all current in-memory catalogs. */
+ const CatalogFilePath cdf_path = asset_definition_default_file_path_from_dir(
+ directory_for_new_files);
+ catalog_definition_file_ = construct_cdf_in_memory(cdf_path);
+ }
+
+ merge_from_disk_before_writing();
+ return catalog_definition_file_->write_to_disk();
+}
+
+std::unique_ptr<AssetCatalogDefinitionFile> AssetCatalogService::construct_cdf_in_memory(
+ const CatalogFilePath &file_path)
+{
+ auto cdf = std::make_unique<AssetCatalogDefinitionFile>();
+ cdf->file_path = file_path;
+
+ for (auto &catalog : catalogs_.values()) {
+ cdf->add_new(catalog.get());
+ }
+
+ return cdf;
+}
+
+std::unique_ptr<AssetCatalogTree> AssetCatalogService::read_into_tree()
+{
+ auto tree = std::make_unique<AssetCatalogTree>();
+
+ /* Go through the catalogs, insert each path component into the tree where needed. */
+ for (auto &catalog : catalogs_.values()) {
+ const AssetCatalogTreeItem *parent = nullptr;
+ AssetCatalogTreeItem::ChildMap *insert_to_map = &tree->children_;
+
+ BLI_assert_msg(!ELEM(catalog->path[0], '/', '\\'),
+ "Malformed catalog path: Path should be formatted like a relative path");
+
+ const char *next_slash_ptr;
+ /* Looks more complicated than it is, this just iterates over path components. E.g.
+ * "just/some/path" iterates over "just", then "some" then "path". */
+ for (const char *name_begin = catalog->path.data(); name_begin && name_begin[0];
+ /* Jump to one after the next slash if there is any. */
+ name_begin = next_slash_ptr ? next_slash_ptr + 1 : nullptr) {
+ next_slash_ptr = BLI_path_slash_find(name_begin);
+
+ /* Note that this won't be null terminated. */
+ StringRef component_name = next_slash_ptr ?
+ StringRef(name_begin, next_slash_ptr - name_begin) :
+ /* Last component in the path. */
+ name_begin;
+
+ /* Insert new tree element - if no matching one is there yet! */
+ auto [item, was_inserted] = insert_to_map->emplace(
+ component_name, AssetCatalogTreeItem(component_name, parent));
+
+ /* Walk further into the path (no matter if a new item was created or not). */
+ parent = &item->second;
+ insert_to_map = &item->second.children_;
+ }
+ }
+
+ return tree;
+}
+
+void AssetCatalogService::rebuild_tree()
+{
+ this->catalog_tree_ = read_into_tree();
+}
+
+AssetCatalogTreeItem::AssetCatalogTreeItem(StringRef name, const AssetCatalogTreeItem *parent)
+ : name_(name), parent_(parent)
+{
+}
+
+StringRef AssetCatalogTreeItem::get_name() const
+{
+ return name_;
+}
+
+CatalogPath AssetCatalogTreeItem::catalog_path() const
+{
+ std::string current_path = name_;
+ for (const AssetCatalogTreeItem *parent = parent_; parent; parent = parent->parent_) {
+ current_path = parent->name_ + AssetCatalogService::PATH_SEPARATOR + current_path;
+ }
+ return current_path;
+}
+
+int AssetCatalogTreeItem::count_parents() const
+{
+ int i = 0;
+ for (const AssetCatalogTreeItem *parent = parent_; parent; parent = parent->parent_) {
+ i++;
+ }
+ return i;
+}
+
+void AssetCatalogTree::foreach_item(const AssetCatalogTreeItem::ItemIterFn callback) const
+{
+ AssetCatalogTreeItem::foreach_item_recursive(children_, callback);
+}
+
+void AssetCatalogTreeItem::foreach_item_recursive(const AssetCatalogTreeItem::ChildMap &children,
+ const ItemIterFn callback)
+{
+ for (const auto &[key, item] : children) {
+ callback(item);
+ foreach_item_recursive(item.children_, callback);
+ }
+}
+
+AssetCatalogTree *AssetCatalogService::get_catalog_tree()
+{
+ return catalog_tree_.get();
+}
+
+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::parse_catalog_file(
+ const CatalogFilePath &catalog_definition_file_path,
+ AssetCatalogParsedFn catalog_loaded_callback)
+{
+ std::fstream infile(catalog_definition_file_path);
+ std::string line;
+ while (std::getline(infile, line)) {
+ const StringRef trimmed_line = StringRef(line).trim();
+ if (trimmed_line.is_empty() || trimmed_line[0] == '#') {
+ continue;
+ }
+
+ std::unique_ptr<AssetCatalog> 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;
+ }
+
+ if (this->contains(non_owning_ptr->catalog_id)) {
+ std::cerr << catalog_definition_file_path << ": multiple definitions of catalog "
+ << non_owning_ptr->catalog_id << " in the same file, using first occurrence."
+ << std::endl;
+ /* Don't store 'catalog'; unique_ptr will free its memory. */
+ continue;
+ }
+
+ /* The AssetDefinitionFile should include this catalog when writing it back to disk. */
+ this->add_new(non_owning_ptr);
+ }
+}
+
+std::unique_ptr<AssetCatalog> 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 line in " << this->file_path << ": " << line << std::endl;
+ return std::unique_ptr<AssetCatalog>(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<AssetCatalog>(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);
+
+ CatalogPath catalog_path;
+ 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<AssetCatalog>(nullptr);
+ }
+
+ if (second_delim == StringRef::not_found) {
+ /* No delimiter means no simple name, just treat it as all "path". */
+ catalog_path = path_and_simple_name;
+ simple_name = "";
+ }
+ else {
+ catalog_path = path_and_simple_name.substr(0, second_delim);
+ simple_name = path_and_simple_name.substr(second_delim + 1).trim();
+ }
+
+ catalog_path = AssetCatalog::cleanup_path(catalog_path);
+ return std::make_unique<AssetCatalog>(catalog_id, catalog_path, 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(dest_file_path.c_str(), backup_path.c_str())) {
+ /* TODO: communicate what went wrong. */
+ return false;
+ }
+ }
+ if (BLI_rename(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_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;
+ }
+
+ std::ofstream output(dest_file_path);
+
+ // TODO(@sybren): remember the line ending style that was originally read, then use that to write
+ // the file again.
+
+ // Write the header.
+ // TODO(@sybren): move the header definition to some other place.
+ output << "# This is an Asset Catalog Definition file for Blender." << std::endl;
+ output << "#" << std::endl;
+ output << "# Empty lines and lines starting with `#` will be ignored." << std::endl;
+ output << "# Other lines are of the format \"UUID:catalog/path/for/assets:simple catalog name\""
+ << std::endl;
+ output << "" << std::endl;
+
+ // Write the catalogs.
+ // TODO(@sybren): order them by Catalog ID or Catalog Path.
+ for (const auto &catalog : catalogs_.values()) {
+ if (catalog->flags.is_deleted) {
+ continue;
+ }
+ 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;
+}
+
+AssetCatalog::AssetCatalog(const CatalogID catalog_id,
+ const CatalogPath &path,
+ const std::string &simple_name)
+ : catalog_id(catalog_id), path(path), simple_name(simple_name)
+{
+}
+
+std::unique_ptr<AssetCatalog> AssetCatalog::from_path(const CatalogPath &path)
+{
+ const CatalogPath clean_path = cleanup_path(path);
+ const CatalogID cat_id = BLI_uuid_generate_random();
+ const std::string simple_name = sensible_simple_name_for_path(clean_path);
+ auto catalog = std::make_unique<AssetCatalog>(cat_id, clean_path, simple_name);
+ return catalog;
+}
+
+std::string AssetCatalog::sensible_simple_name_for_path(const CatalogPath &path)
+{
+ std::string name = path;
+ std::replace(name.begin(), name.end(), AssetCatalogService::PATH_SEPARATOR, '-');
+ if (name.length() < MAX_NAME - 1) {
+ return name;
+ }
+
+ /* Trim off the start of the path, as that's the most generic part and thus contains the least
+ * information. */
+ return "..." + name.substr(name.length() - 60);
+}
+
+CatalogPath AssetCatalog::cleanup_path(const CatalogPath &path)
+{
+ /* TODO(@sybren): maybe go over each element of the path, and trim those? */
+ CatalogPath clean_path = StringRef(path).trim().trim(AssetCatalogService::PATH_SEPARATOR).trim();
+ return clean_path;
+}
+
+} // namespace blender::bke
diff --git a/source/blender/blenkernel/intern/asset_catalog_test.cc b/source/blender/blenkernel/intern/asset_catalog_test.cc
new file mode 100644
index 00000000000..0f389999d6d
--- /dev/null
+++ b/source/blender/blenkernel/intern/asset_catalog_test.cc
@@ -0,0 +1,443 @@
+/*
+ * 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 2
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * The Original Code is Copyright (C) 2020 Blender Foundation
+ * All rights reserved.
+ */
+
+#include "BKE_appdir.h"
+#include "BKE_asset_catalog.hh"
+
+#include "BLI_fileops.h"
+#include "BLI_path_util.h"
+
+#include "testing/testing.h"
+
+namespace blender::bke::tests {
+
+/* UUIDs from lib/tests/asset_library/blender_assets.cats.txt */
+const bUUID UUID_ID_WITHOUT_PATH("e34dd2c5-5d2e-4668-9794-1db5de2a4f71");
+const bUUID UUID_POSES_ELLIE("df60e1f6-2259-475b-93d9-69a1b4a8db78");
+const bUUID UUID_POSES_ELLIE_WHITESPACE("b06132f6-5687-4751-a6dd-392740eb3c46");
+const bUUID UUID_POSES_ELLIE_TRAILING_SLASH("3376b94b-a28d-4d05-86c1-bf30b937130d");
+const bUUID UUID_POSES_RUZENA("79a4f887-ab60-4bd4-94da-d572e27d6aed");
+const bUUID UUID_POSES_RUZENA_HAND("81811c31-1a88-4bd7-bb34-c6fc2607a12e");
+const bUUID UUID_POSES_RUZENA_FACE("82162c1f-06cc-4d91-a9bf-4f72c104e348");
+const bUUID UUID_WITHOUT_SIMPLENAME("d7916a31-6ca9-4909-955f-182ca2b81fa3");
+
+/* UUIDs from lib/tests/asset_library/modified_assets.cats.txt */
+const bUUID UUID_AGENT_47("c5744ba5-43f5-4f73-8e52-010ad4a61b34");
+
+/* Subclass that adds accessors such that protected fields can be used in tests. */
+class TestableAssetCatalogService : public AssetCatalogService {
+ public:
+ explicit TestableAssetCatalogService(const CatalogFilePath &asset_library_root)
+ : AssetCatalogService(asset_library_root)
+ {
+ }
+
+ AssetCatalogDefinitionFile *get_catalog_definition_file()
+ {
+ return catalog_definition_file_.get();
+ }
+};
+
+class AssetCatalogTest : public testing::Test {
+ protected:
+ CatalogFilePath asset_library_root_;
+ CatalogFilePath temp_library_path_;
+
+ void SetUp() override
+ {
+ const std::string test_files_dir = blender::tests::flags_test_asset_dir();
+ if (test_files_dir.empty()) {
+ FAIL();
+ }
+
+ asset_library_root_ = test_files_dir + "/" + "asset_library";
+ temp_library_path_ = "";
+ }
+
+ /* Register a temporary path, which will be removed at the end of the test.
+ * The returned path ends in a slash. */
+ CatalogFilePath use_temp_path()
+ {
+ BKE_tempdir_init("");
+ const CatalogFilePath tempdir = BKE_tempdir_session();
+ temp_library_path_ = tempdir + "test-temporary-path/";
+ return temp_library_path_;
+ }
+
+ CatalogFilePath create_temp_path()
+ {
+ CatalogFilePath path = use_temp_path();
+ BLI_dir_create_recursive(path.c_str());
+ return path;
+ }
+
+ struct CatalogPathInfo {
+ StringRef name;
+ int parent_count;
+ };
+
+ void assert_expected_tree_items(AssetCatalogTree *tree,
+ const std::vector<CatalogPathInfo> &expected_paths)
+ {
+ int i = 0;
+ tree->foreach_item([&](const AssetCatalogTreeItem &actual_item) {
+ ASSERT_LT(i, expected_paths.size())
+ << "More catalogs in tree than expected; did not expect " << actual_item.catalog_path();
+
+ char expected_filename[FILE_MAXFILE];
+ /* Is the catalog name as expected? "character", "Ellie", ... */
+ BLI_split_file_part(
+ expected_paths[i].name.data(), expected_filename, sizeof(expected_filename));
+ EXPECT_EQ(expected_filename, actual_item.get_name());
+ /* Does the computed number of parents match? */
+ EXPECT_EQ(expected_paths[i].parent_count, actual_item.count_parents());
+ EXPECT_EQ(expected_paths[i].name, actual_item.catalog_path());
+
+ i++;
+ });
+ }
+
+ void TearDown() override
+ {
+ if (!temp_library_path_.empty()) {
+ BLI_delete(temp_library_path_.c_str(), true, true);
+ temp_library_path_ = "";
+ }
+ }
+};
+
+TEST_F(AssetCatalogTest, load_single_file)
+{
+ AssetCatalogService service(asset_library_root_);
+ service.load_from_disk(asset_library_root_ + "/" + "blender_assets.cats.txt");
+
+ // Test getting a non-existant catalog ID.
+ EXPECT_EQ(nullptr, service.find_catalog(BLI_uuid_generate_random()));
+
+ // Test getting an invalid catalog (without path definition).
+ AssetCatalog *cat_without_path = service.find_catalog(UUID_ID_WITHOUT_PATH);
+ ASSERT_EQ(nullptr, cat_without_path);
+
+ // Test getting a regular catalog.
+ AssetCatalog *poses_ellie = service.find_catalog(UUID_POSES_ELLIE);
+ ASSERT_NE(nullptr, poses_ellie);
+ EXPECT_EQ(UUID_POSES_ELLIE, poses_ellie->catalog_id);
+ EXPECT_EQ("character/Ellie/poselib", poses_ellie->path);
+ EXPECT_EQ("POSES_ELLIE", poses_ellie->simple_name);
+
+ // Test whitespace stripping and support in the path.
+ AssetCatalog *poses_whitespace = service.find_catalog(UUID_POSES_ELLIE_WHITESPACE);
+ ASSERT_NE(nullptr, poses_whitespace);
+ EXPECT_EQ(UUID_POSES_ELLIE_WHITESPACE, poses_whitespace->catalog_id);
+ EXPECT_EQ("character/Ellie/poselib/white space", poses_whitespace->path);
+ EXPECT_EQ("POSES_ELLIE WHITESPACE", poses_whitespace->simple_name);
+
+ // Test getting a UTF-8 catalog ID.
+ AssetCatalog *poses_ruzena = service.find_catalog(UUID_POSES_RUZENA);
+ ASSERT_NE(nullptr, poses_ruzena);
+ EXPECT_EQ(UUID_POSES_RUZENA, poses_ruzena->catalog_id);
+ EXPECT_EQ("character/Ružena/poselib", poses_ruzena->path);
+ EXPECT_EQ("POSES_RUŽENA", poses_ruzena->simple_name);
+}
+
+TEST_F(AssetCatalogTest, load_single_file_into_tree)
+{
+ AssetCatalogService service(asset_library_root_);
+ service.load_from_disk(asset_library_root_ + "/" + "blender_assets.cats.txt");
+
+ /* Contains not only paths from the CDF but also the missing parents (implicitly defined
+ * catalogs). */
+ std::vector<CatalogPathInfo> expected_paths{
+ {"character", 0},
+ {"character/Ellie", 1},
+ {"character/Ellie/poselib", 2},
+ {"character/Ellie/poselib/white space", 3},
+ {"character/Ružena", 1},
+ {"character/Ružena/poselib", 2},
+ {"character/Ružena/poselib/face", 3},
+ {"character/Ružena/poselib/hand", 3},
+ {"path", 0}, // Implicit.
+ {"path/without", 1}, // Implicit.
+ {"path/without/simplename", 2}, // From CDF.
+ };
+
+ AssetCatalogTree *tree = service.get_catalog_tree();
+ assert_expected_tree_items(tree, expected_paths);
+}
+
+TEST_F(AssetCatalogTest, write_single_file)
+{
+ TestableAssetCatalogService service(asset_library_root_);
+ service.load_from_disk(asset_library_root_ + "/" +
+ AssetCatalogService::DEFAULT_CATALOG_FILENAME);
+
+ const CatalogFilePath save_to_path = use_temp_path() +
+ AssetCatalogService::DEFAULT_CATALOG_FILENAME;
+ AssetCatalogDefinitionFile *cdf = service.get_catalog_definition_file();
+ cdf->write_to_disk(save_to_path);
+
+ AssetCatalogService loaded_service(save_to_path);
+ loaded_service.load_from_disk();
+
+ // Test that the expected catalogs are there.
+ EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_ELLIE));
+ EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_ELLIE_WHITESPACE));
+ EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_ELLIE_TRAILING_SLASH));
+ EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_RUZENA));
+ EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_RUZENA_HAND));
+ EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_RUZENA_FACE));
+
+ // Test that the invalid catalog definition wasn't copied.
+ EXPECT_EQ(nullptr, loaded_service.find_catalog(UUID_ID_WITHOUT_PATH));
+
+ // TODO(@sybren): test ordering of catalogs in the file.
+}
+
+TEST_F(AssetCatalogTest, no_writing_empty_files)
+{
+ const CatalogFilePath temp_lib_root = create_temp_path();
+ AssetCatalogService service(temp_lib_root);
+ service.write_to_disk(temp_lib_root);
+
+ const CatalogFilePath default_cdf_path = temp_lib_root +
+ AssetCatalogService::DEFAULT_CATALOG_FILENAME;
+ EXPECT_FALSE(BLI_exists(default_cdf_path.c_str()));
+}
+
+TEST_F(AssetCatalogTest, create_first_catalog_from_scratch)
+{
+ /* Even from scratch a root directory should be known. */
+ const CatalogFilePath temp_lib_root = use_temp_path();
+ AssetCatalogService service;
+
+ /* Just creating the service should NOT create the path. */
+ EXPECT_FALSE(BLI_exists(temp_lib_root.c_str()));
+
+ AssetCatalog *cat = service.create_catalog("some/catalog/path");
+ ASSERT_NE(nullptr, cat);
+ EXPECT_EQ(cat->path, "some/catalog/path");
+ EXPECT_EQ(cat->simple_name, "some-catalog-path");
+
+ /* Creating a new catalog should not save anything to disk yet. */
+ EXPECT_FALSE(BLI_exists(temp_lib_root.c_str()));
+
+ /* Writing to disk should create the directory + the default file. */
+ service.write_to_disk(temp_lib_root);
+ EXPECT_TRUE(BLI_is_dir(temp_lib_root.c_str()));
+
+ const CatalogFilePath definition_file_path = temp_lib_root + "/" +
+ AssetCatalogService::DEFAULT_CATALOG_FILENAME;
+ EXPECT_TRUE(BLI_is_file(definition_file_path.c_str()));
+
+ AssetCatalogService loaded_service(temp_lib_root);
+ loaded_service.load_from_disk();
+
+ // Test that the expected catalog is there.
+ AssetCatalog *written_cat = loaded_service.find_catalog(cat->catalog_id);
+ ASSERT_NE(nullptr, written_cat);
+ EXPECT_EQ(written_cat->catalog_id, cat->catalog_id);
+ EXPECT_EQ(written_cat->path, cat->path);
+}
+
+TEST_F(AssetCatalogTest, create_catalog_after_loading_file)
+{
+ const CatalogFilePath temp_lib_root = create_temp_path();
+
+ /* Copy the asset catalog definition files to a separate location, so that we can test without
+ * overwriting the test file in SVN. */
+ const CatalogFilePath default_catalog_path = asset_library_root_ + "/" +
+ AssetCatalogService::DEFAULT_CATALOG_FILENAME;
+ const CatalogFilePath writable_catalog_path = temp_lib_root +
+ AssetCatalogService::DEFAULT_CATALOG_FILENAME;
+ BLI_copy(default_catalog_path.c_str(), writable_catalog_path.c_str());
+ EXPECT_TRUE(BLI_is_dir(temp_lib_root.c_str()));
+ EXPECT_TRUE(BLI_is_file(writable_catalog_path.c_str()));
+
+ TestableAssetCatalogService service(temp_lib_root);
+ service.load_from_disk();
+ EXPECT_EQ(writable_catalog_path, service.get_catalog_definition_file()->file_path);
+ EXPECT_NE(nullptr, service.find_catalog(UUID_POSES_ELLIE)) << "expected catalogs to be loaded";
+
+ /* This should create a new catalog but not write to disk. */
+ const AssetCatalog *new_catalog = service.create_catalog("new/catalog");
+ const bUUID new_catalog_id = new_catalog->catalog_id;
+
+ /* Reload the on-disk catalog file. */
+ TestableAssetCatalogService loaded_service(temp_lib_root);
+ loaded_service.load_from_disk();
+ EXPECT_EQ(writable_catalog_path, loaded_service.get_catalog_definition_file()->file_path);
+
+ EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_ELLIE))
+ << "expected pre-existing catalogs to be kept in the file";
+ EXPECT_EQ(nullptr, loaded_service.find_catalog(new_catalog_id))
+ << "expecting newly added catalog to not yet be saved to " << temp_lib_root;
+
+ /* Write and reload the catalog file. */
+ service.write_to_disk(temp_lib_root.c_str());
+ AssetCatalogService reloaded_service(temp_lib_root);
+ reloaded_service.load_from_disk();
+ EXPECT_NE(nullptr, reloaded_service.find_catalog(UUID_POSES_ELLIE))
+ << "expected pre-existing catalogs to be kept in the file";
+ EXPECT_NE(nullptr, reloaded_service.find_catalog(new_catalog_id))
+ << "expecting newly added catalog to exist in the file";
+}
+
+TEST_F(AssetCatalogTest, create_catalog_path_cleanup)
+{
+ const CatalogFilePath temp_lib_root = use_temp_path();
+ AssetCatalogService service(temp_lib_root);
+ AssetCatalog *cat = service.create_catalog(" /some/path / ");
+
+ EXPECT_FALSE(BLI_uuid_is_nil(cat->catalog_id));
+ EXPECT_EQ("some/path", cat->path);
+ EXPECT_EQ("some-path", cat->simple_name);
+}
+
+TEST_F(AssetCatalogTest, create_catalog_simple_name)
+{
+ const CatalogFilePath temp_lib_root = use_temp_path();
+ AssetCatalogService service(temp_lib_root);
+ AssetCatalog *cat = service.create_catalog(
+ "production/Spite Fright/Characters/Victora/Pose Library/Approved/Body Parts/Hands");
+
+ EXPECT_FALSE(BLI_uuid_is_nil(cat->catalog_id));
+ EXPECT_EQ("production/Spite Fright/Characters/Victora/Pose Library/Approved/Body Parts/Hands",
+ cat->path);
+ EXPECT_EQ("...ht-Characters-Victora-Pose Library-Approved-Body Parts-Hands", cat->simple_name);
+}
+
+TEST_F(AssetCatalogTest, delete_catalog_leaf)
+{
+ AssetCatalogService service(asset_library_root_);
+ service.load_from_disk(asset_library_root_ + "/" + "blender_assets.cats.txt");
+
+ /* Delete a leaf catalog, i.e. one that is not a parent of another catalog.
+ * This keeps this particular test easy. */
+ service.delete_catalog(UUID_POSES_RUZENA_HAND);
+ EXPECT_EQ(nullptr, service.find_catalog(UUID_POSES_RUZENA_HAND));
+
+ /* Contains not only paths from the CDF but also the missing parents (implicitly defined
+ * catalogs). This is why a leaf catalog was deleted. */
+ std::vector<CatalogPathInfo> expected_paths{
+ {"character", 0},
+ {"character/Ellie", 1},
+ {"character/Ellie/poselib", 2},
+ {"character/Ellie/poselib/white space", 3},
+ {"character/Ružena", 1},
+ {"character/Ružena/poselib", 2},
+ {"character/Ružena/poselib/face", 3},
+ // {"character/Ružena/poselib/hand", 3}, // this is the deleted one
+ {"path", 0},
+ {"path/without", 1},
+ {"path/without/simplename", 2},
+ };
+
+ AssetCatalogTree *tree = service.get_catalog_tree();
+ assert_expected_tree_items(tree, expected_paths);
+}
+
+TEST_F(AssetCatalogTest, delete_catalog_write_to_disk)
+{
+ TestableAssetCatalogService service(asset_library_root_);
+ service.load_from_disk(asset_library_root_ + "/" +
+ AssetCatalogService::DEFAULT_CATALOG_FILENAME);
+
+ service.delete_catalog(UUID_POSES_ELLIE);
+
+ const CatalogFilePath save_to_path = use_temp_path();
+ AssetCatalogDefinitionFile *cdf = service.get_catalog_definition_file();
+ cdf->write_to_disk(save_to_path + "/" + AssetCatalogService::DEFAULT_CATALOG_FILENAME);
+
+ AssetCatalogService loaded_service(save_to_path);
+ loaded_service.load_from_disk();
+
+ // Test that the expected catalogs are there, except the deleted one.
+ EXPECT_EQ(nullptr, loaded_service.find_catalog(UUID_POSES_ELLIE));
+ EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_ELLIE_WHITESPACE));
+ EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_ELLIE_TRAILING_SLASH));
+ EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_RUZENA));
+ EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_RUZENA_HAND));
+ EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_RUZENA_FACE));
+}
+
+TEST_F(AssetCatalogTest, merge_catalog_files)
+{
+ const CatalogFilePath cdf_dir = create_temp_path();
+ const CatalogFilePath original_cdf_file = asset_library_root_ + "/blender_assets.cats.txt";
+ const CatalogFilePath modified_cdf_file = asset_library_root_ + "/modified_assets.cats.txt";
+ const CatalogFilePath temp_cdf_file = cdf_dir + "blender_assets.cats.txt";
+ BLI_copy(original_cdf_file.c_str(), temp_cdf_file.c_str());
+
+ // Load the unmodified, original CDF.
+ TestableAssetCatalogService service(asset_library_root_);
+ service.load_from_disk(cdf_dir);
+
+ // Copy a modified file, to mimick a situation where someone changed the CDF after we loaded it.
+ BLI_copy(modified_cdf_file.c_str(), temp_cdf_file.c_str());
+
+ // Overwrite the modified file. This should merge the on-disk file with our catalogs.
+ service.write_to_disk(cdf_dir);
+
+ AssetCatalogService loaded_service(cdf_dir);
+ loaded_service.load_from_disk();
+
+ // Test that the expected catalogs are there.
+ EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_ELLIE));
+ EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_ELLIE_WHITESPACE));
+ EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_ELLIE_TRAILING_SLASH));
+ EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_RUZENA));
+ EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_RUZENA_HAND));
+ EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_RUZENA_FACE));
+ EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_AGENT_47)); // New in the modified file.
+
+ // When there are overlaps, the in-memory (i.e. last-saved) paths should win.
+ const AssetCatalog *ruzena_face = loaded_service.find_catalog(UUID_POSES_RUZENA_FACE);
+ EXPECT_EQ("character/Ružena/poselib/face", ruzena_face->path);
+}
+
+TEST_F(AssetCatalogTest, backups)
+{
+ const CatalogFilePath cdf_dir = create_temp_path();
+ const CatalogFilePath original_cdf_file = asset_library_root_ + "/blender_assets.cats.txt";
+ const CatalogFilePath writable_cdf_file = cdf_dir + "/blender_assets.cats.txt";
+ BLI_copy(original_cdf_file.c_str(), writable_cdf_file.c_str());
+
+ /* Read a CDF, modify, and write it. */
+ AssetCatalogService service(cdf_dir);
+ service.load_from_disk();
+ service.delete_catalog(UUID_POSES_ELLIE);
+ service.write_to_disk(cdf_dir);
+
+ const CatalogFilePath backup_path = writable_cdf_file + "~";
+ ASSERT_TRUE(BLI_is_file(backup_path.c_str()));
+
+ AssetCatalogService loaded_service;
+ loaded_service.load_from_disk(backup_path);
+
+ // Test that the expected catalogs are there, including the deleted one.
+ // This is the backup, after all.
+ EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_ELLIE));
+ EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_ELLIE_WHITESPACE));
+ EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_ELLIE_TRAILING_SLASH));
+ EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_RUZENA));
+ EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_RUZENA_HAND));
+ EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_RUZENA_FACE));
+}
+
+} // namespace blender::bke::tests
diff --git a/source/blender/blenkernel/intern/asset_library.cc b/source/blender/blenkernel/intern/asset_library.cc
new file mode 100644
index 00000000000..1153e7b29f5
--- /dev/null
+++ b/source/blender/blenkernel/intern/asset_library.cc
@@ -0,0 +1,53 @@
+/*
+ * 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 2
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+/** \file
+ * \ingroup bke
+ */
+
+#include "BKE_asset_library.hh"
+
+#include "MEM_guardedalloc.h"
+
+#include <memory>
+
+/**
+ * Loading an asset library at this point only means loading the catalogs. Later on this should
+ * invoke reading of asset representations too.
+ */
+struct AssetLibrary *BKE_asset_library_load(const char *library_path)
+{
+ blender::bke::AssetLibrary *lib = new blender::bke::AssetLibrary();
+ lib->load(library_path);
+ return reinterpret_cast<struct AssetLibrary *>(lib);
+}
+
+void BKE_asset_library_free(struct AssetLibrary *asset_library)
+{
+ blender::bke::AssetLibrary *lib = reinterpret_cast<blender::bke::AssetLibrary *>(asset_library);
+ delete lib;
+}
+
+namespace blender::bke {
+
+void AssetLibrary::load(StringRefNull library_root_directory)
+{
+ auto catalog_service = std::make_unique<AssetCatalogService>(library_root_directory);
+ catalog_service->load_from_disk();
+ this->catalog_service = std::move(catalog_service);
+}
+
+} // namespace blender::bke
diff --git a/source/blender/blenkernel/intern/asset_library_test.cc b/source/blender/blenkernel/intern/asset_library_test.cc
new file mode 100644
index 00000000000..37686175aed
--- /dev/null
+++ b/source/blender/blenkernel/intern/asset_library_test.cc
@@ -0,0 +1,82 @@
+/*
+ * 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 2
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * The Original Code is Copyright (C) 2020 Blender Foundation
+ * All rights reserved.
+ */
+
+#include "BKE_appdir.h"
+#include "BKE_asset_catalog.hh"
+#include "BKE_asset_library.hh"
+
+#include "testing/testing.h"
+
+namespace blender::bke::tests {
+
+TEST(AssetLibraryTest, load_and_free_c_functions)
+{
+ const std::string test_files_dir = blender::tests::flags_test_asset_dir();
+ if (test_files_dir.empty()) {
+ FAIL();
+ }
+
+ /* Load the asset library. */
+ const std::string library_path = test_files_dir + "/" + "asset_library";
+ ::AssetLibrary *library_c_ptr = BKE_asset_library_load(library_path.data());
+ ASSERT_NE(nullptr, library_c_ptr);
+
+ /* Check that it can be cast to the C++ type and has a Catalog Service. */
+ blender::bke::AssetLibrary *library_cpp_ptr = reinterpret_cast<blender::bke::AssetLibrary *>(
+ library_c_ptr);
+ AssetCatalogService *service = library_cpp_ptr->catalog_service.get();
+ ASSERT_NE(nullptr, service);
+
+ /* Check that the catalogs defined in the library are actually loaded. This just tests one single
+ * catalog, as that indicates the file has been loaded. Testing that that loading went OK is for
+ * the asset catalog service tests. */
+ const bUUID uuid_poses_ellie("df60e1f6-2259-475b-93d9-69a1b4a8db78");
+ AssetCatalog *poses_ellie = service->find_catalog(uuid_poses_ellie);
+ ASSERT_NE(nullptr, poses_ellie) << "unable to find POSES_ELLIE catalog";
+ EXPECT_EQ("character/Ellie/poselib", poses_ellie->path);
+
+ BKE_asset_library_free(library_c_ptr);
+}
+
+TEST(AssetLibraryTest, load_nonexistent_directory)
+{
+ const std::string test_files_dir = blender::tests::flags_test_asset_dir();
+ if (test_files_dir.empty()) {
+ FAIL();
+ }
+
+ /* Load the asset library. */
+ const std::string library_path = test_files_dir + "/" +
+ "asset_library/this/subdir/does/not/exist";
+ ::AssetLibrary *library_c_ptr = BKE_asset_library_load(library_path.data());
+ ASSERT_NE(nullptr, library_c_ptr);
+
+ /* Check that it can be cast to the C++ type and has a Catalog Service. */
+ blender::bke::AssetLibrary *library_cpp_ptr = reinterpret_cast<blender::bke::AssetLibrary *>(
+ library_c_ptr);
+ AssetCatalogService *service = library_cpp_ptr->catalog_service.get();
+ ASSERT_NE(nullptr, service);
+
+ /* Check that the catalog service doesn't have any catalogs. */
+ EXPECT_TRUE(service->is_empty());
+
+ BKE_asset_library_free(library_c_ptr);
+}
+
+} // namespace blender::bke::tests
diff --git a/source/blender/blenkernel/intern/asset_test.cc b/source/blender/blenkernel/intern/asset_test.cc
new file mode 100644
index 00000000000..f1bf21a4f83
--- /dev/null
+++ b/source/blender/blenkernel/intern/asset_test.cc
@@ -0,0 +1,70 @@
+/*
+ * 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 2
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * The Original Code is Copyright (C) 2020 Blender Foundation
+ * All rights reserved.
+ */
+
+#include "BKE_asset.h"
+
+#include "BLI_uuid.h"
+
+#include "DNA_asset_types.h"
+
+#include "testing/testing.h"
+
+namespace blender::bke::tests {
+
+TEST(AssetMetadataTest, set_catalog_id)
+{
+ AssetMetaData meta;
+ const bUUID uuid = BLI_uuid_generate_random();
+
+ /* Test trivial values. */
+ BKE_asset_metadata_catalog_id_clear(&meta);
+ EXPECT_TRUE(BLI_uuid_is_nil(meta.catalog_id));
+ EXPECT_STREQ("", meta.catalog_simple_name);
+
+ /* Test simple situation where the given short name is used as-is. */
+ BKE_asset_metadata_catalog_id_set(&meta, uuid, "simple");
+ EXPECT_TRUE(BLI_uuid_equal(uuid, meta.catalog_id));
+ EXPECT_STREQ("simple", meta.catalog_simple_name);
+
+ /* Test whitespace trimming. */
+ BKE_asset_metadata_catalog_id_set(&meta, uuid, " Govoriš angleško? ");
+ EXPECT_STREQ("Govoriš angleško?", meta.catalog_simple_name);
+
+ /* Test length trimming to 63 chars + terminating zero. */
+ constexpr char len66[] = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20";
+ constexpr char len63[] = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1";
+ BKE_asset_metadata_catalog_id_set(&meta, uuid, len66);
+ EXPECT_STREQ(len63, meta.catalog_simple_name);
+
+ /* Test length trimming happens after whitespace trimming. */
+ constexpr char len68[] =
+ " \
+ 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20 ";
+ BKE_asset_metadata_catalog_id_set(&meta, uuid, len68);
+ EXPECT_STREQ(len63, meta.catalog_simple_name);
+
+ /* Test length trimming to 63 bytes, and not 63 characters. ✓ in UTF-8 is three bytes long. */
+ constexpr char with_utf8[] =
+ "00010203040506✓0708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20";
+ BKE_asset_metadata_catalog_id_set(&meta, uuid, with_utf8);
+ EXPECT_STREQ("00010203040506✓0708090a0b0c0d0e0f101112131415161718191a1b1c1d",
+ meta.catalog_simple_name);
+}
+
+} // namespace blender::bke::tests
diff --git a/source/blender/blenlib/BLI_uuid.h b/source/blender/blenlib/BLI_uuid.h
index 9b85f8e65bc..592ac3d4607 100644
--- a/source/blender/blenlib/BLI_uuid.h
+++ b/source/blender/blenlib/BLI_uuid.h
@@ -73,4 +73,19 @@ bool BLI_uuid_parse_string(bUUID *uuid, const char *buffer) ATTR_NONNULL();
/** Output the UUID as formatted ASCII string, see #BLI_uuid_format(). */
std::ostream &operator<<(std::ostream &stream, bUUID uuid);
+namespace blender::bke {
+
+class bUUID : public ::bUUID {
+ public:
+ bUUID() = default;
+ bUUID(const ::bUUID &struct_uuid);
+ explicit bUUID(const std::string &string_formatted_uuid);
+
+ uint64_t hash() const;
+};
+
+bool operator==(bUUID uuid1, bUUID uuid2);
+
+} // namespace blender::bke
+
#endif
diff --git a/source/blender/blenlib/intern/uuid.cc b/source/blender/blenlib/intern/uuid.cc
index ae34bcb3d32..c1f7f8dfecd 100644
--- a/source/blender/blenlib/intern/uuid.cc
+++ b/source/blender/blenlib/intern/uuid.cc
@@ -24,6 +24,7 @@
#include <cstring>
#include <ctime>
#include <random>
+#include <sstream>
#include <string>
/* Ensure the UUID struct doesn't have any padding, to be compatible with memcmp(). */
@@ -137,3 +138,34 @@ std::ostream &operator<<(std::ostream &stream, bUUID uuid)
stream << buffer;
return stream;
}
+
+namespace blender::bke {
+
+bUUID::bUUID(const std::string &string_formatted_uuid)
+{
+ const bool parsed_ok = BLI_uuid_parse_string(this, string_formatted_uuid.c_str());
+ if (!parsed_ok) {
+ std::stringstream ss;
+ ss << "invalid UUID string " << string_formatted_uuid;
+ throw std::runtime_error(ss.str());
+ }
+}
+
+bUUID::bUUID(const ::bUUID &struct_uuid)
+{
+ *(static_cast<::bUUID *>(this)) = struct_uuid;
+}
+
+uint64_t bUUID::hash() const
+{
+ /* Convert the struct into two 64-bit numbers, and XOR them to get the hash. */
+ const uint64_t *uuid_as_int64 = reinterpret_cast<const uint64_t *>(this);
+ return uuid_as_int64[0] ^ uuid_as_int64[1];
+}
+
+bool operator==(bUUID uuid1, bUUID uuid2)
+{
+ return BLI_uuid_equal(uuid1, uuid2);
+}
+
+} // namespace blender::bke
diff --git a/source/blender/editors/space_file/CMakeLists.txt b/source/blender/editors/space_file/CMakeLists.txt
index 993a52b9084..b60f9df82f6 100644
--- a/source/blender/editors/space_file/CMakeLists.txt
+++ b/source/blender/editors/space_file/CMakeLists.txt
@@ -49,6 +49,7 @@ set(SRC
)
set(LIB
+ bf_blenkernel
)
if(WITH_HEADLESS)
diff --git a/source/blender/editors/space_file/filelist.c b/source/blender/editors/space_file/filelist.c
index 511b5b255e9..4f881184990 100644
--- a/source/blender/editors/space_file/filelist.c
+++ b/source/blender/editors/space_file/filelist.c
@@ -56,6 +56,7 @@
#endif
#include "BKE_asset.h"
+#include "BKE_asset_library.h"
#include "BKE_context.h"
#include "BKE_global.h"
#include "BKE_icons.h"
@@ -386,6 +387,7 @@ typedef struct FileList {
eFileSelectType type;
/* The library this list was created for. Stored here so we know when to re-read. */
AssetLibraryReference *asset_library_ref;
+ struct AssetLibrary *asset_library;
short flags;
@@ -1758,6 +1760,13 @@ void filelist_clear_ex(struct FileList *filelist, const bool do_cache, const boo
if (do_selection && filelist->selection_state) {
BLI_ghash_clear(filelist->selection_state, NULL, NULL);
}
+
+ if (filelist->asset_library != NULL) {
+ /* There is no way to refresh the catalogs stored by the AssetLibrary struct, so instead of
+ * "clearing" it, the entire struct is freed. It will be reallocated when needed. */
+ BKE_asset_library_free(filelist->asset_library);
+ filelist->asset_library = NULL;
+ }
}
void filelist_clear(struct FileList *filelist)
@@ -3136,6 +3145,9 @@ typedef struct FileListReadJob {
* The job system calls #filelist_readjob_update which moves any read file from #tmp_filelist
* into #filelist in a thread-safe way.
*
+ * #tmp_filelist also keeps an `AssetLibrary *` so that it can be loaded in the same thread, and
+ * moved to #filelist once all categories are loaded.
+ *
* NOTE: #tmp_filelist is freed in #filelist_readjob_free, so any copied pointers need to be set
* to NULL to avoid double-freeing them. */
struct FileList *tmp_filelist;
@@ -3266,6 +3278,13 @@ static void filelist_readjob_do(const bool do_lib,
BLI_stack_discard(todo_dirs);
}
BLI_stack_free(todo_dirs);
+
+ /* Check whether assets catalogs need to be loaded. */
+ if (job_params->filelist->asset_library_ref != NULL) {
+ /* Load asset catalogs, into the temp filelist for thread-safety.
+ * #filelist_readjob_endjob() will move it into the real filelist. */
+ job_params->tmp_filelist->asset_library = BKE_asset_library_load(filelist->filelist.root);
+ }
}
static void filelist_readjob_dir(FileListReadJob *job_params,
@@ -3355,6 +3374,8 @@ static void filelist_readjob_startjob(void *flrjv, short *stop, short *do_update
BLI_mutex_lock(&flrj->lock);
BLI_assert((flrj->tmp_filelist == NULL) && flrj->filelist);
+ BLI_assert_msg(flrj->filelist->asset_library == NULL,
+ "Asset library should not yet be assigned at start of read job");
flrj->tmp_filelist = MEM_dupallocN(flrj->filelist);
@@ -3415,6 +3436,12 @@ static void filelist_readjob_endjob(void *flrjv)
/* In case there would be some dangling update... */
filelist_readjob_update(flrjv);
+ /* Move ownership of the asset library from the temporary list to the true filelist. */
+ BLI_assert_msg(flrj->filelist->asset_library == NULL,
+ "asset library should not already have been allocated");
+ flrj->filelist->asset_library = flrj->tmp_filelist->asset_library;
+ flrj->tmp_filelist->asset_library = NULL; /* MUST be NULL to avoid double-free. */
+
flrj->filelist->flags &= ~FL_IS_PENDING;
flrj->filelist->flags |= FL_IS_READY;
}
diff --git a/source/blender/makesdna/DNA_asset_types.h b/source/blender/makesdna/DNA_asset_types.h
index 2975915eccd..f5bdad3e79e 100644
--- a/source/blender/makesdna/DNA_asset_types.h
+++ b/source/blender/makesdna/DNA_asset_types.h
@@ -22,6 +22,7 @@
#include "DNA_defs.h"
#include "DNA_listBase.h"
+#include "DNA_uuid_types.h"
#ifdef __cplusplus
extern "C" {
@@ -58,6 +59,19 @@ typedef struct AssetMetaData {
/** Custom asset meta-data. Cannot store pointers to IDs (#STRUCT_NO_DATABLOCK_IDPROPERTIES)! */
struct IDProperty *properties;
+ /**
+ * Asset Catalog identifier. Should not contain spaces.
+ * Mapped to a path in the asset catalog hierarchy by an #AssetCatalogService.
+ * Use #BKE_asset_metadata_catalog_id_set() to ensure a valid ID is set.
+ */
+ struct bUUID catalog_id;
+ /**
+ * Short name of the asset's catalog. This is for debugging purposes only, to allow (partial)
+ * reconstruction of asset catalogs in the unfortunate case that the mapping from catalog UUID to
+ * catalog path is lost. The catalog's simple name is copied to #catalog_simple_name whenever
+ * #catalog_id is updated. */
+ char catalog_simple_name[64]; /* MAX_NAME */
+
/** Optional description of this asset for display in the UI. Dynamic length. */
char *description;
/** User defined tags for this asset. The asset manager uses these for filtering, but how they
diff --git a/source/blender/makesdna/DNA_uuid_types.h b/source/blender/makesdna/DNA_uuid_types.h
index fa0a78f074b..dcebfed6be7 100644
--- a/source/blender/makesdna/DNA_uuid_types.h
+++ b/source/blender/makesdna/DNA_uuid_types.h
@@ -40,6 +40,12 @@ typedef struct bUUID {
uint8_t node[6];
} bUUID;
+/**
+ * Memory required for a string representation of a UUID according to RFC4122.
+ * This is 36 characters for the string + a trailing zero byte.
+ */
+#define UUID_STRING_LEN 37
+
#ifdef __cplusplus
}
#endif
diff --git a/source/blender/makesrna/intern/rna_asset.c b/source/blender/makesrna/intern/rna_asset.c
index dcef88d2e79..5df32150cc4 100644
--- a/source/blender/makesrna/intern/rna_asset.c
+++ b/source/blender/makesrna/intern/rna_asset.c
@@ -32,11 +32,15 @@
#ifdef RNA_RUNTIME
# include "BKE_asset.h"
+# include "BKE_asset_library.h"
+# include "BKE_context.h"
# include "BKE_idprop.h"
# include "BLI_listbase.h"
+# include "BLI_uuid.h"
# include "ED_asset.h"
+# include "ED_fileselect.h"
# include "RNA_access.h"
@@ -176,6 +180,40 @@ static void rna_AssetMetaData_active_tag_range(
*max = *softmax = MAX2(asset_data->tot_tags - 1, 0);
}
+static void rna_AssetMetaData_catalog_id_get(PointerRNA *ptr, char *value)
+{
+ const AssetMetaData *asset_data = ptr->data;
+ BLI_uuid_format(value, asset_data->catalog_id);
+}
+
+static int rna_AssetMetaData_catalog_id_length(PointerRNA *UNUSED(ptr))
+{
+ return UUID_STRING_LEN - 1;
+}
+
+static void rna_AssetMetaData_catalog_id_set(PointerRNA *ptr, const char *value)
+{
+ AssetMetaData *asset_data = ptr->data;
+ bUUID new_uuid;
+
+ if (value[0] == '\0') {
+ BKE_asset_metadata_catalog_id_clear(asset_data);
+ return;
+ }
+
+ if (!BLI_uuid_parse_string(&new_uuid, value)) {
+ // TODO(Sybren): raise ValueError exception once that's possible from an RNA setter.
+ printf("UUID %s not formatted correctly, ignoring new value\n", value);
+ return;
+ }
+
+ /* This just sets the new UUID and clears the catalog simple name. The actual
+ * catalog simple name will be updated by some update function, as it
+ * needs the asset library from the context. */
+ /* TODO(Sybren): write that update function. */
+ BKE_asset_metadata_catalog_id_set(asset_data, new_uuid, "");
+}
+
static PointerRNA rna_AssetHandle_file_data_get(PointerRNA *ptr)
{
AssetHandle *asset_handle = ptr->data;
@@ -310,6 +348,24 @@ static void rna_def_asset_data(BlenderRNA *brna)
prop = RNA_def_property(srna, "active_tag", PROP_INT, PROP_NONE);
RNA_def_property_int_funcs(prop, NULL, NULL, "rna_AssetMetaData_active_tag_range");
RNA_def_property_ui_text(prop, "Active Tag", "Index of the tag set for editing");
+
+ prop = RNA_def_property(srna, "catalog_id", PROP_STRING, PROP_NONE);
+ RNA_def_property_string_funcs(prop,
+ "rna_AssetMetaData_catalog_id_get",
+ "rna_AssetMetaData_catalog_id_length",
+ "rna_AssetMetaData_catalog_id_set");
+ RNA_def_property_flag(prop, PROP_CONTEXT_UPDATE);
+ RNA_def_property_ui_text(prop,
+ "Catalog UUID",
+ "Identifier for the asset's catalog, used by Blender to look up the "
+ "asset's catalog path. Must be a UUID according to RFC4122");
+
+ prop = RNA_def_property(srna, "catalog_simple_name", PROP_STRING, PROP_NONE);
+ RNA_def_property_clear_flag(prop, PROP_EDITABLE);
+ RNA_def_property_ui_text(prop,
+ "Catalog Simple Name",
+ "Simple name of the asset's catalog, for debugging and "
+ "data recovery purposes");
}
static void rna_def_asset_handle_api(StructRNA *srna)