diff --git a/mediarepo-daemon/Cargo.lock b/mediarepo-daemon/Cargo.lock index 6e12601..a98dc8a 100644 --- a/mediarepo-daemon/Cargo.lock +++ b/mediarepo-daemon/Cargo.lock @@ -446,9 +446,9 @@ checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" [[package]] name = "env_logger" -version = "0.9.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" +checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" dependencies = [ "atty", "humantime", @@ -668,9 +668,12 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "humantime" -version = "2.1.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" +dependencies = [ + "quick-error", +] [[package]] name = "idna" @@ -811,12 +814,12 @@ checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" name = "mediarepo" version = "0.1.0" dependencies = [ - "env_logger", "glob", "log", "mediarepo-core", "mediarepo-model", "mediarepo-socket", + "pretty_env_logger", "structopt", "tokio", "toml", @@ -1209,6 +1212,16 @@ version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" +[[package]] +name = "pretty_env_logger" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "926d36b9553851b8b0005f1275891b392ee4d2d833852c417ed025477350fb9d" +dependencies = [ + "env_logger", + "log", +] + [[package]] name = "proc-macro-crate" version = "1.1.0" @@ -1290,6 +1303,12 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quote" version = "1.0.10" diff --git a/mediarepo-daemon/Cargo.toml b/mediarepo-daemon/Cargo.toml index 5c69d62..e1b915c 100644 --- a/mediarepo-daemon/Cargo.toml +++ b/mediarepo-daemon/Cargo.toml @@ -17,7 +17,7 @@ crate-type = ["lib"] [dependencies] toml = {version = "0.5.8", optional=true} structopt = {version="0.3.23", optional=true} -env_logger = {version="0.9.0", optional=true} +pretty_env_logger = {version="0.4.0", optional=true} glob = {version="0.3.0", optional=true} log = "0.4.14" @@ -38,5 +38,5 @@ features = ["macros", "rt-multi-thread", "io-std", "io-util"] [features] default = ["runtime"] -runtime = ["toml", "structopt", "mediarepo-model", "mediarepo-socket", "env_logger", "glob"] +runtime = ["toml", "structopt", "mediarepo-model", "mediarepo-socket", "pretty_env_logger", "glob"] library = ["mediarepo-socket"] \ No newline at end of file diff --git a/mediarepo-daemon/mediarepo-core/src/lib.rs b/mediarepo-daemon/mediarepo-core/src/lib.rs index b0fae21..5bfcbea 100644 --- a/mediarepo-daemon/mediarepo-core/src/lib.rs +++ b/mediarepo-daemon/mediarepo-core/src/lib.rs @@ -4,6 +4,7 @@ pub mod file_hash_store; pub mod image_processing; pub mod settings; pub mod type_keys; +pub mod utils; pub use futures; pub use image; diff --git a/mediarepo-daemon/mediarepo-core/src/utils.rs b/mediarepo-daemon/mediarepo-core/src/utils.rs new file mode 100644 index 0000000..830ddbd --- /dev/null +++ b/mediarepo-daemon/mediarepo-core/src/utils.rs @@ -0,0 +1,7 @@ +/// Parses a normalized tag into its two components of namespace and tag +pub fn parse_namespace_and_tag(norm_tag: String) -> (Option, String) { + norm_tag + .split_once(':') + .map(|(n, t)| (Some(n.trim().to_string()), t.trim().to_string())) + .unwrap_or((None, norm_tag.trim().to_string())) +} diff --git a/mediarepo-daemon/mediarepo-model/src/lib.rs b/mediarepo-daemon/mediarepo-model/src/lib.rs index 05d3c70..fba5e43 100644 --- a/mediarepo-daemon/mediarepo-model/src/lib.rs +++ b/mediarepo-daemon/mediarepo-model/src/lib.rs @@ -1,7 +1,9 @@ pub mod file; pub mod file_type; pub mod hash; +pub mod namespace; pub mod repo; pub mod storage; +pub mod tag; pub mod thumbnail; pub mod type_keys; diff --git a/mediarepo-daemon/mediarepo-model/src/namespace.rs b/mediarepo-daemon/mediarepo-model/src/namespace.rs new file mode 100644 index 0000000..041e4be --- /dev/null +++ b/mediarepo-daemon/mediarepo-model/src/namespace.rs @@ -0,0 +1,62 @@ +use mediarepo_core::error::RepoResult; +use mediarepo_database::entities::namespace; +use sea_orm::prelude::*; +use sea_orm::{DatabaseConnection, Set}; + +#[derive(Clone)] +pub struct Namespace { + db: DatabaseConnection, + model: namespace::Model, +} + +impl Namespace { + pub(crate) fn new(db: DatabaseConnection, model: namespace::Model) -> Self { + Self { db, model } + } + + /// Retrieves the namespace by id + pub async fn by_id(db: DatabaseConnection, id: i64) -> RepoResult> { + let namespace = namespace::Entity::find_by_id(id) + .one(&db) + .await? + .map(|model| Self::new(db, model)); + + Ok(namespace) + } + + /// Retrieves a namespace by its name + pub async fn by_name>( + db: DatabaseConnection, + name: S, + ) -> RepoResult> { + let namespace = namespace::Entity::find() + .filter(namespace::Column::Name.eq(name.as_ref())) + .one(&db) + .await? + .map(|model| Self::new(db, model)); + + Ok(namespace) + } + + /// Adds a namespace to the database + pub async fn add(db: DatabaseConnection, name: S) -> RepoResult { + let active_model = namespace::ActiveModel { + name: Set(name.to_string()), + ..Default::default() + }; + let active_model = active_model.insert(&db).await?; + let namespace = Self::by_id(db, active_model.id.unwrap()).await?.unwrap(); + + Ok(namespace) + } + + /// The ID of the namespace + pub fn id(&self) -> i64 { + self.model.id + } + + /// The name of the namespace + pub fn name(&self) -> &String { + &self.model.name + } +} diff --git a/mediarepo-daemon/mediarepo-model/src/repo.rs b/mediarepo-daemon/mediarepo-model/src/repo.rs index df1d06f..6e3862c 100644 --- a/mediarepo-daemon/mediarepo-model/src/repo.rs +++ b/mediarepo-daemon/mediarepo-model/src/repo.rs @@ -1,9 +1,12 @@ use crate::file::File; use crate::file_type::FileType; +use crate::namespace::Namespace; use crate::storage::Storage; +use crate::tag::Tag; use crate::thumbnail::Thumbnail; use mediarepo_core::error::{RepoError, RepoResult}; use mediarepo_core::image_processing::ThumbnailSize; +use mediarepo_core::utils::parse_namespace_and_tag; use mediarepo_database::get_database; use sea_orm::DatabaseConnection; use std::io::Cursor; @@ -133,6 +136,55 @@ impl Repo { Ok(()) } + /// Returns all tags stored in the database + pub async fn tags(&self) -> RepoResult> { + Tag::all(self.db.clone()).await + } + + /// Adds or finds a tag + pub async fn add_or_find_tag(&self, tag: S) -> RepoResult { + let (namespace, name) = parse_namespace_and_tag(tag.to_string()); + if let Some(namespace) = namespace { + self.add_or_find_namespaced_tag(name, namespace).await + } else { + self.add_or_find_unnamespaced_tag(name).await + } + } + + /// Adds or finds an unnamespaced tag + async fn add_or_find_unnamespaced_tag(&self, name: String) -> RepoResult { + if let Some(tag) = Tag::by_name(self.db.clone(), &name).await? { + Ok(tag) + } else { + self.add_unnamespaced_tag(name).await + } + } + + /// Adds an unnamespaced tag + async fn add_unnamespaced_tag(&self, name: String) -> RepoResult { + Tag::add(self.db.clone(), name, None).await + } + + /// Adds or finds a namespaced tag + async fn add_or_find_namespaced_tag(&self, name: String, namespace: String) -> RepoResult { + if let Some(tag) = Tag::by_name_and_namespace(self.db.clone(), &name, &namespace).await? { + Ok(tag) + } else { + self.add_namespaced_tag(name, namespace).await + } + } + + /// Adds a namespaced tag + async fn add_namespaced_tag(&self, name: String, namespace: String) -> RepoResult { + let namespace = + if let Some(namespace) = Namespace::by_name(self.db.clone(), &namespace).await? { + namespace + } else { + Namespace::add(self.db.clone(), namespace).await? + }; + Tag::add(self.db.clone(), name, Some(namespace.id())).await + } + fn get_main_storage(&self) -> RepoResult<&Storage> { if let Some(storage) = &self.main_storage { Ok(storage) diff --git a/mediarepo-daemon/mediarepo-model/src/tag.rs b/mediarepo-daemon/mediarepo-model/src/tag.rs new file mode 100644 index 0000000..73a9481 --- /dev/null +++ b/mediarepo-daemon/mediarepo-model/src/tag.rs @@ -0,0 +1,119 @@ +use crate::namespace::Namespace; +use mediarepo_core::error::RepoResult; +use mediarepo_database::entities::namespace; +use mediarepo_database::entities::tag; +use sea_orm::prelude::*; +use sea_orm::QuerySelect; +use sea_orm::{DatabaseConnection, JoinType, Set}; + +#[derive(Clone)] +pub struct Tag { + db: DatabaseConnection, + model: tag::Model, + namespace: Option, +} + +impl Tag { + pub(crate) fn new( + db: DatabaseConnection, + model: tag::Model, + namespace: Option, + ) -> Self { + Self { + db, + model, + namespace, + } + } + + /// Returns all tags stored in the database + pub async fn all(db: DatabaseConnection) -> RepoResult> { + let tags: Vec = tag::Entity::find() + .find_also_related(namespace::Entity) + .all(&db) + .await? + .into_iter() + .map(|(tag, namespace)| Self::new(db.clone(), tag, namespace)) + .collect(); + + Ok(tags) + } + + /// Returns the tag by id + pub async fn by_id(db: DatabaseConnection, id: i64) -> RepoResult> { + let tag = tag::Entity::find_by_id(id) + .find_also_related(namespace::Entity) + .one(&db) + .await? + .map(|(model, namespace)| Self::new(db, model, namespace)); + + Ok(tag) + } + + /// Retrieves the unnamespaced tag by name + pub async fn by_name>( + db: DatabaseConnection, + name: S, + ) -> RepoResult> { + let tag = tag::Entity::find() + .filter(tag::Column::Name.eq(name.as_ref())) + .filter(tag::Column::NamespaceId.eq(Option::::None)) + .one(&db) + .await? + .map(|t| Tag::new(db, t, None)); + + Ok(tag) + } + + /// Retrieves the namespaced tag by name and namespace + pub async fn by_name_and_namespace, S2: AsRef>( + db: DatabaseConnection, + name: S1, + namespace: S2, + ) -> RepoResult> { + let tag = tag::Entity::find() + .find_also_related(namespace::Entity) + .join(JoinType::InnerJoin, namespace::Relation::Tag.def()) + .filter(namespace::Column::Name.eq(namespace.as_ref())) + .filter(tag::Column::Name.eq(name.as_ref())) + .one(&db) + .await? + .map(|(t, n)| Self::new(db.clone(), t, n)); + + Ok(tag) + } + + /// Adds a new tag to the database + pub async fn add( + db: DatabaseConnection, + name: S, + namespace_id: Option, + ) -> RepoResult { + let active_model = tag::ActiveModel { + name: Set(name.to_string()), + namespace_id: Set(namespace_id), + ..Default::default() + }; + let active_model = active_model.insert(&db).await?; + let tag = Self::by_id(db, active_model.id.unwrap()).await?.unwrap(); + + Ok(tag) + } + + /// The ID of the tag + pub fn id(&self) -> i64 { + self.model.id + } + + /// The name of the tag + pub fn name(&self) -> &String { + &self.model.name + } + + /// The namespace of the tag + pub fn namespace(&self) -> Option { + self.namespace + .clone() + .map(|n| Namespace::new(self.db.clone(), n)) + } +} diff --git a/mediarepo-daemon/mediarepo-socket/src/namespaces/mod.rs b/mediarepo-daemon/mediarepo-socket/src/namespaces/mod.rs index bb68efe..bdd4c2d 100644 --- a/mediarepo-daemon/mediarepo-socket/src/namespaces/mod.rs +++ b/mediarepo-daemon/mediarepo-socket/src/namespaces/mod.rs @@ -1,7 +1,10 @@ use mediarepo_core::rmp_ipc::{namespace, namespace::Namespace, IPCBuilder}; pub mod files; +pub mod tags; pub fn build_namespaces(builder: IPCBuilder) -> IPCBuilder { - builder.add_namespace(namespace!(files::FilesNamespace)) + builder + .add_namespace(namespace!(files::FilesNamespace)) + .add_namespace(namespace!(tags::TagsNamespace)) } diff --git a/mediarepo-daemon/mediarepo-socket/src/namespaces/tags.rs b/mediarepo-daemon/mediarepo-socket/src/namespaces/tags.rs new file mode 100644 index 0000000..fdd69ed --- /dev/null +++ b/mediarepo-daemon/mediarepo-socket/src/namespaces/tags.rs @@ -0,0 +1,34 @@ +use crate::types::responses::TagResponse; +use crate::utils::get_repo_from_context; +use mediarepo_core::rmp_ipc::prelude::*; + +pub struct TagsNamespace; + +impl NamespaceProvider for TagsNamespace { + fn name() -> &'static str { + "tags" + } + + fn register(handler: &mut EventHandler) { + events!(handler, + "all_tags" => Self::all_tags + ); + } +} + +impl TagsNamespace { + async fn all_tags(ctx: &Context, event: Event) -> IPCResult<()> { + let repo = get_repo_from_context(ctx).await; + let tags: Vec = repo + .tags() + .await? + .into_iter() + .map(TagResponse::from) + .collect(); + ctx.emitter + .emit_response_to(event.id(), Self::name(), "all_tags", tags) + .await?; + + Ok(()) + } +} diff --git a/mediarepo-daemon/mediarepo-socket/src/types/responses.rs b/mediarepo-daemon/mediarepo-socket/src/types/responses.rs index cef433c..d8a4183 100644 --- a/mediarepo-daemon/mediarepo-socket/src/types/responses.rs +++ b/mediarepo-daemon/mediarepo-socket/src/types/responses.rs @@ -1,6 +1,7 @@ use chrono::NaiveDateTime; use mediarepo_model::file::File; use mediarepo_model::file_type::FileType; +use mediarepo_model::tag::Tag; use mediarepo_model::thumbnail::Thumbnail; use serde::{Deserialize, Serialize}; @@ -55,3 +56,20 @@ pub struct InfoResponse { pub name: String, pub version: String, } + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct TagResponse { + pub id: i64, + pub name: String, + pub namespace: Option, +} + +impl From for TagResponse { + fn from(tag: Tag) -> Self { + Self { + id: tag.id(), + name: tag.name().to_owned(), + namespace: tag.namespace().map(|n| n.name().to_owned()), + } + } +} diff --git a/mediarepo-daemon/src/main.rs b/mediarepo-daemon/src/main.rs index e80165a..e2966ef 100644 --- a/mediarepo-daemon/src/main.rs +++ b/mediarepo-daemon/src/main.rs @@ -3,6 +3,7 @@ mod utils; use crate::constants::{DEFAULT_STORAGE_NAME, SETTINGS_PATH, THUMBNAIL_STORAGE_NAME}; use crate::utils::{create_paths_for_repo, get_repo, load_settings}; +use log::LevelFilter; use mediarepo_core::error::RepoResult; use mediarepo_core::futures::future; use mediarepo_core::settings::Settings; @@ -10,7 +11,10 @@ use mediarepo_core::type_keys::SettingsKey; use mediarepo_model::repo::Repo; use mediarepo_model::type_keys::RepoKey; use mediarepo_socket::get_builder; +use pretty_env_logger::env_logger::WriteStyle; +use std::env; use std::path::PathBuf; +use std::str::FromStr; use std::sync::Arc; use structopt::StructOpt; use tokio::fs; @@ -66,6 +70,7 @@ fn main() -> RepoResult<()> { } fn get_single_thread_runtime() -> Runtime { + log::info!("Using current thread runtime"); runtime::Builder::new_current_thread() .enable_all() .max_blocking_threads(1) @@ -74,6 +79,7 @@ fn get_single_thread_runtime() -> Runtime { } fn get_multi_thread_runtime() -> Runtime { + log::info!("Using multi thread runtime"); runtime::Builder::new_multi_thread() .enable_all() .build() @@ -81,7 +87,15 @@ fn get_multi_thread_runtime() -> Runtime { } fn build_logger() { - env_logger::builder() + pretty_env_logger::formatted_timed_builder() + .filter( + None, + env::var("RUST_LOG") + .ok() + .and_then(|level| LevelFilter::from_str(&level).ok()) + .unwrap_or(LevelFilter::Info), + ) + .write_style(WriteStyle::Always) .filter_module("sqlx", log::LevelFilter::Warn) .filter_module("tokio", log::LevelFilter::Info) .filter_module("tracing", log::LevelFilter::Warn)