Implement filtering based on file properties
Signed-off-by: trivernis <trivernis@protonmail.com>pull/4/head
parent
810f9986af
commit
9e0c72bd66
@ -0,0 +1,220 @@
|
||||
use chrono::NaiveDateTime;
|
||||
use mediarepo_database::entities::content_descriptor;
|
||||
use mediarepo_database::entities::content_descriptor_tag;
|
||||
use mediarepo_database::entities::file;
|
||||
use mediarepo_database::entities::file_metadata;
|
||||
use sea_orm::sea_query::{Alias, Expr, IntoColumnRef, Query, SimpleExpr};
|
||||
use sea_orm::ColumnTrait;
|
||||
use sea_orm::Condition;
|
||||
|
||||
macro_rules! apply_ordering_comparator {
|
||||
($column:expr, $filter:expr) => {
|
||||
match $filter {
|
||||
OrderingComparator::Less(value) => $column.lt(value),
|
||||
OrderingComparator::Equal(value) => $column.eq(value),
|
||||
OrderingComparator::Greater(value) => $column.gt(value),
|
||||
OrderingComparator::Between((min_value, max_value)) => {
|
||||
$column.between(min_value, max_value)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum FilterProperty {
|
||||
TagId(NegatableComparator<i64>),
|
||||
TagWildcardIds(NegatableComparator<Vec<i64>>),
|
||||
ContentDescriptor(NegatableComparator<Vec<u8>>),
|
||||
TagCount(OrderingComparator<i64>),
|
||||
FileProperty(FilterFileProperty),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum FilterFileProperty {
|
||||
Id(NegatableComparator<i64>),
|
||||
Status(NegatableComparator<i64>),
|
||||
FileSize(OrderingComparator<i64>),
|
||||
ImportedTime(OrderingComparator<NaiveDateTime>),
|
||||
ChangedTime(OrderingComparator<NaiveDateTime>),
|
||||
CreatedTime(OrderingComparator<NaiveDateTime>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum OrderingComparator<T> {
|
||||
Less(T),
|
||||
Equal(T),
|
||||
Greater(T),
|
||||
Between((T, T)),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum NegatableComparator<T> {
|
||||
Is(T),
|
||||
IsNot(T),
|
||||
}
|
||||
|
||||
pub fn build_find_filter_conditions(filters: Vec<Vec<FilterProperty>>) -> Condition {
|
||||
filters
|
||||
.into_iter()
|
||||
.fold(Condition::all(), |all_cond, mut expression| {
|
||||
if expression.len() == 1 {
|
||||
let property = expression.pop().unwrap();
|
||||
|
||||
all_cond.add(build_single_filter(property))
|
||||
} else if !expression.is_empty() {
|
||||
let sub_condition = expression.into_iter().fold(Condition::any(), |cond, prop| {
|
||||
cond.add(build_single_filter(prop))
|
||||
});
|
||||
|
||||
all_cond.add(sub_condition)
|
||||
} else {
|
||||
all_cond
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn build_single_filter(property: FilterProperty) -> SimpleExpr {
|
||||
match property {
|
||||
FilterProperty::TagId(tag_filter) => build_tag_id_filter(tag_filter),
|
||||
FilterProperty::TagWildcardIds(wildcard_filter) => {
|
||||
build_tag_wildcard_ids_filter(wildcard_filter)
|
||||
}
|
||||
FilterProperty::ContentDescriptor(cd_filter) => build_content_descriptor_filter(cd_filter),
|
||||
FilterProperty::TagCount(count_filter) => build_tag_count_filter(count_filter),
|
||||
FilterProperty::FileProperty(property_filter) => {
|
||||
build_file_property_filter(property_filter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_tag_id_filter(filter: NegatableComparator<i64>) -> SimpleExpr {
|
||||
match filter {
|
||||
NegatableComparator::Is(tag_id) => content_descriptor::Column::Id.in_subquery(
|
||||
Query::select()
|
||||
.expr(Expr::col(content_descriptor_tag::Column::CdId))
|
||||
.from(content_descriptor_tag::Entity)
|
||||
.cond_where(content_descriptor_tag::Column::TagId.eq(tag_id))
|
||||
.to_owned(),
|
||||
),
|
||||
NegatableComparator::IsNot(tag_id) => content_descriptor::Column::Id.not_in_subquery(
|
||||
Query::select()
|
||||
.expr(Expr::col(content_descriptor_tag::Column::CdId))
|
||||
.from(content_descriptor_tag::Entity)
|
||||
.cond_where(content_descriptor_tag::Column::TagId.eq(tag_id))
|
||||
.to_owned(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_tag_wildcard_ids_filter(filter: NegatableComparator<Vec<i64>>) -> SimpleExpr {
|
||||
match filter {
|
||||
NegatableComparator::Is(tag_ids) => content_descriptor::Column::Id.in_subquery(
|
||||
Query::select()
|
||||
.expr(Expr::col(content_descriptor_tag::Column::CdId))
|
||||
.from(content_descriptor_tag::Entity)
|
||||
.cond_where(content_descriptor_tag::Column::TagId.is_in(tag_ids))
|
||||
.to_owned(),
|
||||
),
|
||||
NegatableComparator::IsNot(tag_ids) => content_descriptor::Column::Id.not_in_subquery(
|
||||
Query::select()
|
||||
.expr(Expr::col(content_descriptor_tag::Column::CdId))
|
||||
.from(content_descriptor_tag::Entity)
|
||||
.cond_where(content_descriptor_tag::Column::TagId.is_in(tag_ids))
|
||||
.to_owned(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_content_descriptor_filter(filter: NegatableComparator<Vec<u8>>) -> SimpleExpr {
|
||||
match filter {
|
||||
NegatableComparator::Is(cd) => content_descriptor::Column::Descriptor.eq(cd),
|
||||
NegatableComparator::IsNot(cd) => content_descriptor::Column::Descriptor.ne(cd),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_tag_count_filter(filter: OrderingComparator<i64>) -> SimpleExpr {
|
||||
let count_subquery = Query::select()
|
||||
.expr(content_descriptor_tag::Column::TagId.count())
|
||||
.from(content_descriptor_tag::Entity)
|
||||
.group_by_col(content_descriptor_tag::Column::CdId)
|
||||
.to_owned();
|
||||
let count_column = Alias::new("count").into_column_ref();
|
||||
|
||||
let count_expression = match filter {
|
||||
OrderingComparator::Less(count) => Expr::col(count_column).lt(count),
|
||||
OrderingComparator::Equal(count) => Expr::col(count_column).eq(count),
|
||||
OrderingComparator::Greater(count) => Expr::col(count_column).gt(count),
|
||||
OrderingComparator::Between((min_count, max_count)) => {
|
||||
Expr::col(count_column).between(min_count, max_count)
|
||||
}
|
||||
};
|
||||
|
||||
content_descriptor::Column::Id.in_subquery(
|
||||
Query::select()
|
||||
.expr(Expr::col(content_descriptor_tag::Column::CdId))
|
||||
.from_subquery(count_subquery, Alias::new("tag_counts"))
|
||||
.cond_where(count_expression)
|
||||
.to_owned(),
|
||||
)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn build_file_property_filter(property: FilterFileProperty) -> SimpleExpr {
|
||||
match property {
|
||||
FilterFileProperty::Id(id_filter) => build_file_id_filter(id_filter),
|
||||
FilterFileProperty::Status(status_filter) => build_file_status_filter(status_filter),
|
||||
FilterFileProperty::FileSize(size_filter) => {
|
||||
build_file_metadata_filter(build_file_size_filter(size_filter))
|
||||
}
|
||||
FilterFileProperty::ImportedTime(time_filter) => {
|
||||
build_file_metadata_filter(build_file_import_time_filter(time_filter))
|
||||
}
|
||||
FilterFileProperty::ChangedTime(time_filter) => {
|
||||
build_file_metadata_filter(build_file_changed_time_filter(time_filter))
|
||||
}
|
||||
FilterFileProperty::CreatedTime(time_filter) => {
|
||||
build_file_metadata_filter(build_file_created_time_filter(time_filter))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_file_id_filter(filter: NegatableComparator<i64>) -> SimpleExpr {
|
||||
match filter {
|
||||
NegatableComparator::Is(id) => file::Column::Id.eq(id),
|
||||
NegatableComparator::IsNot(id) => file::Column::Id.ne(id),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_file_status_filter(filter: NegatableComparator<i64>) -> SimpleExpr {
|
||||
match filter {
|
||||
NegatableComparator::Is(status) => file::Column::Status.eq(status),
|
||||
NegatableComparator::IsNot(status) => file::Column::Status.ne(status),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_file_metadata_filter(property_condition: SimpleExpr) -> SimpleExpr {
|
||||
file::Column::Id.in_subquery(
|
||||
Query::select()
|
||||
.expr(Expr::col(file_metadata::Column::FileId))
|
||||
.from(file_metadata::Entity)
|
||||
.cond_where(property_condition)
|
||||
.to_owned(),
|
||||
)
|
||||
}
|
||||
|
||||
fn build_file_size_filter(filter: OrderingComparator<i64>) -> SimpleExpr {
|
||||
apply_ordering_comparator!(file_metadata::Column::Size, filter)
|
||||
}
|
||||
|
||||
fn build_file_import_time_filter(filter: OrderingComparator<NaiveDateTime>) -> SimpleExpr {
|
||||
apply_ordering_comparator!(file_metadata::Column::ImportTime, filter)
|
||||
}
|
||||
|
||||
fn build_file_changed_time_filter(filter: OrderingComparator<NaiveDateTime>) -> SimpleExpr {
|
||||
apply_ordering_comparator!(file_metadata::Column::ChangeTime, filter)
|
||||
}
|
||||
|
||||
fn build_file_created_time_filter(filter: OrderingComparator<NaiveDateTime>) -> SimpleExpr {
|
||||
apply_ordering_comparator!(file_metadata::Column::CreationTime, filter)
|
||||
}
|
@ -0,0 +1,182 @@
|
||||
use mediarepo_core::content_descriptor::decode_content_descriptor;
|
||||
use mediarepo_core::error::RepoResult;
|
||||
use mediarepo_core::mediarepo_api::types::files::FileStatus as ApiFileStatus;
|
||||
use mediarepo_core::mediarepo_api::types::filtering::{
|
||||
FilterExpression, FilterQuery, PropertyQuery, TagQuery, ValueComparator,
|
||||
};
|
||||
use mediarepo_model::file::filter::NegatableComparator::{Is, IsNot};
|
||||
use mediarepo_model::file::filter::{FilterFileProperty, FilterProperty, OrderingComparator};
|
||||
use mediarepo_model::file::{File, FileStatus};
|
||||
use mediarepo_model::repo::Repo;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub async fn find_files_for_filters(
|
||||
repo: &Repo,
|
||||
expressions: Vec<FilterExpression>,
|
||||
) -> RepoResult<Vec<File>> {
|
||||
let tag_names = get_tag_names_from_expressions(&expressions);
|
||||
let tag_id_map = repo.tag_names_to_ids(tag_names).await?;
|
||||
let filters = build_filters_from_expressions(expressions, &tag_id_map);
|
||||
|
||||
repo.find_files_by_filters(filters).await
|
||||
}
|
||||
|
||||
fn get_tag_names_from_expressions(expressions: &Vec<FilterExpression>) -> Vec<String> {
|
||||
expressions
|
||||
.iter()
|
||||
.flat_map(|f| match f {
|
||||
FilterExpression::OrExpression(queries) => queries
|
||||
.iter()
|
||||
.filter_map(|q| match q {
|
||||
FilterQuery::Tag(tag) => Some(tag.tag.to_owned()),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<String>>(),
|
||||
FilterExpression::Query(q) => match q {
|
||||
FilterQuery::Tag(tag) => {
|
||||
vec![tag.tag.to_owned()]
|
||||
}
|
||||
FilterQuery::Property(_) => {
|
||||
vec![]
|
||||
}
|
||||
},
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
}
|
||||
|
||||
fn build_filters_from_expressions(
|
||||
expressions: Vec<FilterExpression>,
|
||||
tag_id_map: &HashMap<String, i64>,
|
||||
) -> Vec<Vec<FilterProperty>> {
|
||||
expressions
|
||||
.into_iter()
|
||||
.filter_map(|e| {
|
||||
let filters = match e {
|
||||
FilterExpression::OrExpression(queries) => queries
|
||||
.into_iter()
|
||||
.filter_map(|q| map_query_to_filter(q, tag_id_map))
|
||||
.collect(),
|
||||
FilterExpression::Query(q) => {
|
||||
if let Some(filter) = map_query_to_filter(q, tag_id_map) {
|
||||
vec![filter]
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
};
|
||||
if filters.len() > 0 {
|
||||
Some(filters)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn map_query_to_filter(
|
||||
query: FilterQuery,
|
||||
tag_id_map: &HashMap<String, i64>,
|
||||
) -> Option<FilterProperty> {
|
||||
match query {
|
||||
FilterQuery::Tag(tag_query) => map_tag_query_to_filter(tag_query, tag_id_map),
|
||||
FilterQuery::Property(property) => map_property_query_to_filter(property),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_tag_query_to_filter(
|
||||
query: TagQuery,
|
||||
tag_id_map: &HashMap<String, i64>,
|
||||
) -> Option<FilterProperty> {
|
||||
if query.tag.ends_with("*") {
|
||||
map_wildcard_tag_to_filter(query, tag_id_map)
|
||||
} else {
|
||||
map_tag_to_filter(query, tag_id_map)
|
||||
}
|
||||
}
|
||||
|
||||
fn map_wildcard_tag_to_filter(
|
||||
query: TagQuery,
|
||||
tag_id_map: &HashMap<String, i64>,
|
||||
) -> Option<FilterProperty> {
|
||||
let filter_tag = query.tag.trim_end_matches("*");
|
||||
let relevant_ids = tag_id_map
|
||||
.iter()
|
||||
.filter_map(|(name, id)| {
|
||||
if name.starts_with(filter_tag) {
|
||||
Some(*id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<i64>>();
|
||||
|
||||
if relevant_ids.len() > 0 {
|
||||
let comparator = if query.negate {
|
||||
IsNot(relevant_ids)
|
||||
} else {
|
||||
Is(relevant_ids)
|
||||
};
|
||||
Some(FilterProperty::TagWildcardIds(comparator))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn map_tag_to_filter(query: TagQuery, tag_id_map: &HashMap<String, i64>) -> Option<FilterProperty> {
|
||||
tag_id_map.get(&query.tag).map(|id| {
|
||||
let comparator = if query.negate { IsNot(*id) } else { Is(*id) };
|
||||
FilterProperty::TagId(comparator)
|
||||
})
|
||||
}
|
||||
|
||||
fn map_property_query_to_filter(query: PropertyQuery) -> Option<FilterProperty> {
|
||||
match query {
|
||||
PropertyQuery::Status(s) => Some(FilterProperty::FileProperty(FilterFileProperty::Status(
|
||||
Is(file_status_to_number(s)),
|
||||
))),
|
||||
PropertyQuery::FileSize(s) => Some(FilterProperty::FileProperty(
|
||||
FilterFileProperty::FileSize(val_comparator_to_order(s, |v| v as i64)),
|
||||
)),
|
||||
PropertyQuery::ImportedTime(t) => Some(FilterProperty::FileProperty(
|
||||
FilterFileProperty::ImportedTime(val_comparator_to_order(t, |t| t)),
|
||||
)),
|
||||
PropertyQuery::ChangedTime(t) => Some(FilterProperty::FileProperty(
|
||||
FilterFileProperty::ChangedTime(val_comparator_to_order(t, |t| t)),
|
||||
)),
|
||||
PropertyQuery::CreatedTime(t) => Some(FilterProperty::FileProperty(
|
||||
FilterFileProperty::CreatedTime(val_comparator_to_order(t, |t| t)),
|
||||
)),
|
||||
PropertyQuery::TagCount(c) => {
|
||||
Some(FilterProperty::TagCount(val_comparator_to_order(c, |v| {
|
||||
v as i64
|
||||
})))
|
||||
}
|
||||
PropertyQuery::Cd(cd) => decode_content_descriptor(cd)
|
||||
.ok()
|
||||
.map(|cd| FilterProperty::ContentDescriptor(Is(cd))),
|
||||
PropertyQuery::Id(id) => Some(FilterProperty::FileProperty(FilterFileProperty::Id(Is(id)))),
|
||||
}
|
||||
}
|
||||
|
||||
fn file_status_to_number(status: ApiFileStatus) -> i64 {
|
||||
match status {
|
||||
ApiFileStatus::Imported => FileStatus::Imported as i64,
|
||||
ApiFileStatus::Archived => FileStatus::Archived as i64,
|
||||
ApiFileStatus::Deleted => FileStatus::Deleted as i64,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn val_comparator_to_order<T1, T2, F: Fn(T1) -> T2>(
|
||||
comp: ValueComparator<T1>,
|
||||
conv_fn: F,
|
||||
) -> OrderingComparator<T2> {
|
||||
match comp {
|
||||
ValueComparator::Less(v) => OrderingComparator::Less(conv_fn(v)),
|
||||
ValueComparator::Equal(v) => OrderingComparator::Equal(conv_fn(v)),
|
||||
ValueComparator::Greater(v) => OrderingComparator::Greater(conv_fn(v)),
|
||||
ValueComparator::Between((v1, v2)) => {
|
||||
OrderingComparator::Between((conv_fn(v1), conv_fn(v2)))
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue