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