diff --git a/mediarepo-daemon/mediarepo-logic/src/dao/repo/mod.rs b/mediarepo-daemon/mediarepo-logic/src/dao/repo/mod.rs index e0713ab..a329f2a 100644 --- a/mediarepo-daemon/mediarepo-logic/src/dao/repo/mod.rs +++ b/mediarepo-daemon/mediarepo-logic/src/dao/repo/mod.rs @@ -16,6 +16,7 @@ use mediarepo_core::utils::parse_namespace_and_tag; use mediarepo_database::get_database; use mediarepo_database::queries::analysis::{get_all_counts, Counts}; +use crate::dao::tag::by_name::TagByNameQuery; use crate::dao::{DaoContext, DaoProvider}; use crate::namespace::Namespace; use crate::tag::Tag; @@ -66,78 +67,12 @@ impl Repo { &self.db } - /// Converts a list of tag names to tag ids - #[tracing::instrument(level = "debug", skip(self))] - pub async fn tag_names_to_ids(&self, tags: Vec) -> RepoResult> { - let parsed_tags = tags - .iter() - .map(|tag| parse_namespace_and_tag(tag.clone())) - .unique() - .collect(); - - let db_tags = self.tags_by_names(parsed_tags).await?; - let tag_map: HashMap = - HashMap::from_iter(db_tags.into_iter().map(|t| (t.normalized_name(), t.id()))); - - Ok(tag_map) - } - - /// Finds all tags by name - #[tracing::instrument(level = "debug", skip(self))] - pub async fn tags_by_names(&self, tags: Vec<(Option, String)>) -> RepoResult> { - Tag::all_by_name(self.db.clone(), tags).await - } - /// Finds all tags that are assigned to the given list of hashes #[tracing::instrument(level = "debug", skip_all)] pub async fn find_tags_for_file_identifiers(&self, cds: Vec>) -> RepoResult> { Tag::for_cd_list(self.db.clone(), cds).await } - /// Adds all tags that are not in the database to the database and returns the ones already existing as well - #[tracing::instrument(level = "debug", skip_all)] - pub async fn add_all_tags(&self, tags: Vec<(Option, String)>) -> RepoResult> { - let mut tags_to_add = tags.into_iter().unique().collect_vec(); - let mut namespaces_to_add = tags_to_add - .iter() - .filter_map(|(namespace, _)| namespace.clone()) - .unique() - .collect_vec(); - - let mut existing_namespaces = - Namespace::all_by_name(self.db.clone(), namespaces_to_add.clone()).await?; - { - let existing_namespaces_set = existing_namespaces - .iter() - .map(|n| n.name().clone()) - .collect::>(); - namespaces_to_add.retain(|namespace| !existing_namespaces_set.contains(namespace)); - } - existing_namespaces - .append(&mut Namespace::add_all(self.db.clone(), namespaces_to_add).await?); - - let mut existing_tags = self.tags_by_names(tags_to_add.clone()).await?; - { - let existing_tags_set = existing_tags - .iter() - .map(|t| (t.namespace().map(|n| n.name().clone()), t.name().clone())) - .collect::, String)>>(); - - tags_to_add.retain(|t| !existing_tags_set.contains(t)); - } - let namespace_map = existing_namespaces - .into_iter() - .map(|namespace| (namespace.name().clone(), namespace.id())) - .collect::>(); - let tags_to_add = tags_to_add - .into_iter() - .map(|(nsp, name)| (nsp.and_then(|n| namespace_map.get(&n)).map(|i| *i), name)) - .collect_vec(); - existing_tags.append(&mut Tag::add_all(self.db.clone(), tags_to_add).await?); - - Ok(existing_tags) - } - /// Adds or finds a tag #[tracing::instrument(level = "debug", skip(self))] pub async fn add_or_find_tag(&self, tag: S) -> RepoResult { diff --git a/mediarepo-daemon/mediarepo-logic/src/dao/tag/add.rs b/mediarepo-daemon/mediarepo-logic/src/dao/tag/add.rs new file mode 100644 index 0000000..f52da09 --- /dev/null +++ b/mediarepo-daemon/mediarepo-logic/src/dao/tag/add.rs @@ -0,0 +1,120 @@ +use crate::dao::tag::{map_tag_dto, TagDao}; +use crate::dto::{AddTagDto, NamespaceDto, TagDto}; +use mediarepo_core::error::RepoResult; +use mediarepo_database::entities::{namespace, tag}; +use sea_orm::prelude::*; +use sea_orm::ActiveValue::Set; +use sea_orm::{Condition, ConnectionTrait, DatabaseTransaction}; +use std::collections::HashMap; +use std::iter::FromIterator; + +impl TagDao { + #[tracing::instrument(level = "debug", skip(self))] + pub async fn add_all(&self, mut tags: Vec) -> RepoResult> { + let namespaces = tags.iter().filter_map(|t| t.namespace.clone()).collect(); + let trx = self.ctx.db.begin().await?; + let existing_tags = tags_by_name(&trx, tags.clone()).await?; + + if existing_tags.len() == tags.len() { + return Ok(existing_tags); + } + let existing_tag_map: HashMap = + HashMap::from_iter(existing_tags.into_iter().map(|t| (t.normalized_name(), t))); + + let namespace_map = add_or_get_all_namespaces(&trx, namespaces).await?; + tags.retain(|dto| !existing_tag_map.contains_key(&dto.normalized_name())); + let tag_models: Vec = tags + .iter() + .map(|t| tag::ActiveModel { + name: Set(t.name.to_owned()), + namespace_id: Set(t + .namespace + .as_ref() + .and_then(|n| namespace_map.get(n)) + .map(|n| n.id())), + ..Default::default() + }) + .collect(); + tag::Entity::insert_many(tag_models).exec(&trx).await?; + let mut tag_dtos = tags_by_name(&trx, tags).await?; + trx.commit().await?; + tag_dtos.append(&mut existing_tag_map.into_iter().map(|(_, dto)| dto).collect()); + + Ok(tag_dtos) + } +} + +async fn add_or_get_all_namespaces( + trx: &DatabaseTransaction, + mut namespaces: Vec, +) -> RepoResult> { + let existing_namespaces = namespaces_by_name(trx, namespaces.clone()).await?; + let mut namespace_map = HashMap::from_iter( + existing_namespaces + .into_iter() + .map(|nsp| (nsp.name().to_owned(), nsp)), + ); + if namespaces.len() == namespace_map.len() { + return Ok(namespace_map); + } + namespaces.retain(|nsp| !namespace_map.contains_key(nsp)); + let namespace_models: Vec = namespaces + .iter() + .map(|nsp| namespace::ActiveModel { + name: Set(nsp.to_owned()), + ..Default::default() + }) + .collect(); + namespace::Entity::insert_many(namespace_models) + .exec(trx) + .await?; + let additional_namespaces = namespaces_by_name(trx, namespaces.clone()).await?; + + for nsp in additional_namespaces { + namespace_map.insert(nsp.name().to_owned(), nsp); + } + + Ok(namespace_map) +} + +async fn namespaces_by_name( + trx: &DatabaseTransaction, + names: Vec, +) -> RepoResult> { + let namespaces: Vec = namespace::Entity::find() + .filter(namespace::Column::Name.is_in(names)) + .all(trx) + .await? + .into_iter() + .map(NamespaceDto::new) + .collect(); + + Ok(namespaces) +} + +async fn tags_by_name(trx: &DatabaseTransaction, tags: Vec) -> RepoResult> { + let condition = tags + .into_iter() + .map(build_tag_condition) + .fold(Condition::any(), Condition::add); + let tags = tag::Entity::find() + .find_also_related(namespace::Entity) + .filter(condition) + .all(trx) + .await? + .into_iter() + .map(map_tag_dto) + .collect(); + + Ok(tags) +} + +fn build_tag_condition(tag: AddTagDto) -> Condition { + if let Some(namespace) = tag.namespace { + Condition::all() + .add(tag::Column::Name.eq(tag.name)) + .add(namespace::Column::Name.eq(namespace)) + } else { + Condition::all().add(tag::Column::Name.eq(tag.name)) + } +} diff --git a/mediarepo-daemon/mediarepo-logic/src/dao/tag/by_name.rs b/mediarepo-daemon/mediarepo-logic/src/dao/tag/by_name.rs new file mode 100644 index 0000000..de7e4c9 --- /dev/null +++ b/mediarepo-daemon/mediarepo-logic/src/dao/tag/by_name.rs @@ -0,0 +1,63 @@ +use crate::dao::tag::{map_tag_dto, TagDao}; +use crate::dto::TagDto; +use mediarepo_core::error::RepoResult; +use mediarepo_database::entities::{namespace, tag}; +use sea_orm::prelude::*; +use sea_orm::sea_query::Expr; +use sea_orm::Condition; + +#[derive(Clone, Debug)] +pub struct TagByNameQuery { + pub namespace: Option, + pub name: String, +} + +impl TagDao { + /// Filters all tags by names + /// wildcards are supported + #[tracing::instrument(level = "debug", skip(self))] + pub async fn all_by_name(&self, names: Vec) -> RepoResult> { + let mut condition_count = 0; + let condition = names + .into_iter() + .filter_map(name_query_to_condition) + .inspect(|_| condition_count += 1) + .fold(Condition::any(), Condition::add); + if condition_count == 0 { + return Ok(vec![]); + } + + let tags = tag::Entity::find() + .find_also_related(namespace::Entity) + .filter(condition) + .all(&self.ctx.db) + .await? + .into_iter() + .map(map_tag_dto) + .collect(); + + Ok(tags) + } +} + +fn name_query_to_condition(query: TagByNameQuery) -> Option { + let TagByNameQuery { namespace, name } = query; + let mut condition = Condition::all(); + + if !name.ends_with('*') { + condition = condition.add(tag::Column::Name.eq(name)) + } else if name.len() > 1 { + condition = + condition.add(tag::Column::Name.like(&*format!("{}%", name.trim_end_matches("*")))) + } else if namespace.is_none() { + return None; + } + + condition = if let Some(namespace) = namespace { + condition.add(namespace::Column::Name.eq(namespace)) + } else { + condition.add(Expr::tbl(tag::Entity, tag::Column::NamespaceId).is_null()) + }; + + Some(condition) +} diff --git a/mediarepo-daemon/mediarepo-logic/src/dao/tag/mod.rs b/mediarepo-daemon/mediarepo-logic/src/dao/tag/mod.rs index 2f3dfa2..6e08e01 100644 --- a/mediarepo-daemon/mediarepo-logic/src/dao/tag/mod.rs +++ b/mediarepo-daemon/mediarepo-logic/src/dao/tag/mod.rs @@ -1,13 +1,20 @@ use sea_orm::prelude::*; use sea_orm::JoinType; use sea_orm::QuerySelect; +use std::collections::HashMap; +use std::iter::FromIterator; use mediarepo_core::error::RepoResult; +use mediarepo_core::mediarepo_api::types::filtering::TagQuery; +use mediarepo_core::utils::parse_namespace_and_tag; use mediarepo_database::entities::{content_descriptor, content_descriptor_tag, namespace, tag}; +use crate::dao::tag::by_name::TagByNameQuery; use crate::dao::{DaoContext, DaoProvider}; use crate::dto::{NamespaceDto, TagDto}; +pub mod add; +pub mod by_name; pub mod mappings; pub struct TagDao { @@ -71,6 +78,26 @@ impl TagDao { Ok(tags) } + + /// Returns a map mapping tag names to ids + #[tracing::instrument(level = "debug", skip(self))] + pub async fn normalized_tags_to_ids( + &self, + names: Vec, + ) -> RepoResult> { + let queries = names + .into_iter() + .map(parse_namespace_and_tag) + .map(|(namespace, name)| TagByNameQuery { namespace, name }) + .collect(); + let tags = self.all_by_name(queries).await?; + let tag_map = HashMap::from_iter( + tags.into_iter() + .map(|tag| (tag.normalized_name(), tag.id())), + ); + + Ok(tag_map) + } } fn map_tag_dto(result: (tag::Model, Option)) -> TagDto { diff --git a/mediarepo-daemon/mediarepo-logic/src/dto/tag.rs b/mediarepo-daemon/mediarepo-logic/src/dto/tag.rs index df59a5f..d45dcd4 100644 --- a/mediarepo-daemon/mediarepo-logic/src/dto/tag.rs +++ b/mediarepo-daemon/mediarepo-logic/src/dto/tag.rs @@ -28,4 +28,35 @@ impl TagDto { pub fn namespace(&self) -> Option<&NamespaceDto> { self.namespace.as_ref() } + + /// Returns the normalized name of the tag (namespace:tag) + pub fn normalized_name(&self) -> String { + if let Some(namespace) = &self.namespace { + format!("{}:{}", namespace.name(), self.name()) + } else { + self.name().to_owned() + } + } +} + +#[derive(Clone, Debug)] +pub struct AddTagDto { + pub namespace: Option, + pub name: String, +} + +impl AddTagDto { + pub fn from_tuple(tuple: (Option, String)) -> Self { + let (namespace, name) = tuple; + Self { namespace, name } + } + + /// Returns the normalized name of the tag (namespace:tag) + pub fn normalized_name(&self) -> String { + if let Some(namespace) = &self.namespace { + format!("{}:{}", namespace, &self.name) + } else { + self.name.to_owned() + } + } } diff --git a/mediarepo-daemon/mediarepo-socket/src/namespaces/files/mod.rs b/mediarepo-daemon/mediarepo-socket/src/namespaces/files/mod.rs index c379b58..644a6f0 100644 --- a/mediarepo-daemon/mediarepo-socket/src/namespaces/files/mod.rs +++ b/mediarepo-daemon/mediarepo-socket/src/namespaces/files/mod.rs @@ -15,7 +15,7 @@ use mediarepo_core::mediarepo_api::types::identifier::FileIdentifier; use mediarepo_core::thumbnailer::ThumbnailSize; use mediarepo_core::utils::parse_namespace_and_tag; use mediarepo_logic::dao::DaoProvider; -use mediarepo_logic::dto::{AddFileDto, UpdateFileDto, UpdateFileMetadataDto}; +use mediarepo_logic::dto::{AddFileDto, AddTagDto, UpdateFileDto, UpdateFileMetadataDto}; use crate::from_model::FromModel; use crate::namespaces::files::searching::find_files_for_filters; @@ -171,7 +171,13 @@ impl FilesNamespace { }; let tags = repo - .add_all_tags(tags.into_iter().map(parse_namespace_and_tag).collect()) + .tag() + .add_all( + tags.into_iter() + .map(parse_namespace_and_tag) + .map(AddTagDto::from_tuple) + .collect(), + ) .await?; let tag_ids: Vec = tags.into_iter().map(|t| t.id()).unique().collect(); repo.tag() diff --git a/mediarepo-daemon/mediarepo-socket/src/namespaces/files/searching.rs b/mediarepo-daemon/mediarepo-socket/src/namespaces/files/searching.rs index ab6d66b..fb78e4d 100644 --- a/mediarepo-daemon/mediarepo-socket/src/namespaces/files/searching.rs +++ b/mediarepo-daemon/mediarepo-socket/src/namespaces/files/searching.rs @@ -6,10 +6,10 @@ use mediarepo_core::mediarepo_api::types::files::FileStatus as ApiFileStatus; use mediarepo_core::mediarepo_api::types::filtering::{ FilterExpression, FilterQuery, PropertyQuery, TagQuery, ValueComparator, }; -use mediarepo_logic::dao::DaoProvider; -use mediarepo_logic::dao::file::find::{FilterFileProperty, FilterProperty, OrderingComparator}; use mediarepo_logic::dao::file::find::NegatableComparator::{Is, IsNot}; +use mediarepo_logic::dao::file::find::{FilterFileProperty, FilterProperty, OrderingComparator}; use mediarepo_logic::dao::repo::Repo; +use mediarepo_logic::dao::DaoProvider; use mediarepo_logic::dto::{FileDto, FileStatus}; #[tracing::instrument(level = "debug", skip(repo))] @@ -18,7 +18,7 @@ pub async fn find_files_for_filters( expressions: Vec, ) -> RepoResult> { let tag_names = get_tag_names_from_expressions(&expressions); - let tag_id_map = repo.tag_names_to_ids(tag_names).await?; + let tag_id_map = repo.tag().normalized_tags_to_ids(tag_names).await?; let filters = build_filters_from_expressions(expressions, &tag_id_map); repo.file().find(filters).await