Fix Recursive searching with tags including Any and All filter modes

This commit is contained in:
Cameron Cordes
2024-03-09 13:11:55 -05:00
parent b2c8ebe558
commit 05a56ba0bd
5 changed files with 63 additions and 31 deletions

View File

@@ -5,6 +5,7 @@ Upon first run it will generate thumbnails for all images and videos at `BASE_PA
## Environment ## Environment
There are a handful of required environment variables to have the API run. There are a handful of required environment variables to have the API run.
They should be defined where the binary is located or above it in an `.env` file. They should be defined where the binary is located or above it in an `.env` file.
You must have `ffmpeg` installed for streaming video and generating video thumbnails.
- `DATABASE_URL` is a path or url to a database (currently only SQLite is tested) - `DATABASE_URL` is a path or url to a database (currently only SQLite is tested)
- `BASE_PATH` is the root from which you want to serve images and videos - `BASE_PATH` is the root from which you want to serve images and videos

View File

@@ -108,7 +108,7 @@ pub struct FilesRequest {
pub recursive: Option<bool>, pub recursive: Option<bool>,
} }
#[derive(Copy, Clone, Deserialize, PartialEq)] #[derive(Copy, Clone, Deserialize, PartialEq, Debug)]
pub enum FilterMode { pub enum FilterMode {
Any, Any,
All, All,

View File

@@ -14,16 +14,16 @@ use actix_web::{
web::{self, Query}, web::{self, Query},
HttpResponse, HttpResponse,
}; };
use log::{debug, error, info}; use log::{debug, error, info, trace};
use crate::data::{Claims, FilesRequest, FilterMode, PhotosResponse}; use crate::data::{Claims, FilesRequest, FilterMode, PhotosResponse};
use crate::{AppState, create_thumbnails}; use crate::{create_thumbnails, AppState};
use crate::error::IntoHttpError; use crate::error::IntoHttpError;
use crate::tags::TagDao; use crate::tags::TagDao;
use crate::video::StreamActor;
use path_absolutize::*; use path_absolutize::*;
use serde::Deserialize; use serde::Deserialize;
use crate::video::StreamActor;
pub async fn list_photos<TagD: TagDao, FS: FileSystemAccess>( pub async fn list_photos<TagD: TagDao, FS: FileSystemAccess>(
_: Claims, _: Claims,
@@ -34,10 +34,15 @@ pub async fn list_photos<TagD: TagDao, FS: FileSystemAccess>(
) -> HttpResponse { ) -> HttpResponse {
let path = &req.path; let path = &req.path;
// Do we need this? let search_recursively = req.recursive.unwrap_or(false);
if let (Some(tag_ids), Some(filter_mode)) = (&req.tag_ids, &req.tag_filter_mode) { if let Some(tag_ids) = &req.tag_ids {
let search_recursively = &req.recursive.unwrap_or(false);
if search_recursively { if search_recursively {
let filter_mode = req.tag_filter_mode.unwrap_or(FilterMode::Any);
debug!(
"Searching for tags: {}. With path: '{}' and filter mode: {:?}",
tag_ids, path, filter_mode
);
let mut dao = tag_dao.lock().expect("Unable to get TagDao"); let mut dao = tag_dao.lock().expect("Unable to get TagDao");
let tag_ids = tag_ids let tag_ids = tag_ids
.split(',') .split(',')
@@ -45,9 +50,24 @@ pub async fn list_photos<TagD: TagDao, FS: FileSystemAccess>(
.collect::<Vec<i32>>(); .collect::<Vec<i32>>();
return dao return dao
.get_files_with_tag_ids(tag_ids.clone()) .get_files_with_any_tag_ids(tag_ids.clone())
.context(format!("Failed to get files with tag_ids: {:?}", tag_ids)) .context(format!("Failed to get files with tag_ids: {:?}", tag_ids))
.map(|tagged_files| match filter_mode {
FilterMode::Any => tagged_files,
FilterMode::All => tagged_files
.iter()
.filter(|&file_path| {
let file_tags = dao.get_tags_for_path(file_path).unwrap_or_default();
tag_ids
.iter()
.all(|id| file_tags.iter().any(|tag| &tag.id == id))
})
.cloned()
.collect::<Vec<String>>(),
})
.map(|tagged_files| { .map(|tagged_files| {
trace!("Found tagged files: {:?}", tagged_files);
HttpResponse::Ok().json(PhotosResponse { HttpResponse::Ok().json(PhotosResponse {
photos: tagged_files, photos: tagged_files,
dirs: vec![], dirs: vec![],
@@ -59,7 +79,7 @@ pub async fn list_photos<TagD: TagDao, FS: FileSystemAccess>(
} }
if let Ok(files) = file_system.get_files_for_path(path) { if let Ok(files) = file_system.get_files_for_path(path) {
debug!("Valid path: {:?}", path); debug!("Valid search path: {:?}", path);
let photos = files let photos = files
.iter() .iter()
@@ -78,12 +98,6 @@ pub async fn list_photos<TagD: TagDao, FS: FileSystemAccess>(
}) })
.map(|f| f.to_str().unwrap().to_string()) .map(|f| f.to_str().unwrap().to_string())
.filter(|file_path| { .filter(|file_path| {
let recursive = req.recursive.unwrap_or(false);
let file_slash_count = file_path.split("/").collect::<Vec<&str>>().len();
let filter_path_slash_count = path.split("/").collect::<Vec<&str>>().len();
// Skip if this path is below/deeper than the search path
if !recursive && file_slash_count > filter_path_slash_count { return false; }
if let (Some(tag_ids), Ok(mut tag_dao)) = (&req.tag_ids, tag_dao.lock()) { if let (Some(tag_ids), Ok(mut tag_dao)) = (&req.tag_ids, tag_dao.lock()) {
let tag_ids = tag_ids let tag_ids = tag_ids
.split(',') .split(',')
@@ -91,7 +105,6 @@ pub async fn list_photos<TagD: TagDao, FS: FileSystemAccess>(
.collect::<Vec<i32>>(); .collect::<Vec<i32>>();
let filter_mode = &req.tag_filter_mode.unwrap_or(FilterMode::Any); let filter_mode = &req.tag_filter_mode.unwrap_or(FilterMode::Any);
let file_tags = tag_dao.get_tags_for_path(file_path).unwrap_or_default(); let file_tags = tag_dao.get_tags_for_path(file_path).unwrap_or_default();
return match filter_mode { return match filter_mode {
@@ -152,7 +165,7 @@ pub fn is_valid_full_path<P: AsRef<Path> + Debug + AsRef<std::ffi::OsStr>>(
path: &P, path: &P,
new_file: bool, new_file: bool,
) -> Option<PathBuf> { ) -> Option<PathBuf> {
debug!("Base: {:?}. Path: {:?}", base, path); trace!("is_valid_full_path => Base: {:?}. Path: {:?}", base, path);
let path = PathBuf::from(&path); let path = PathBuf::from(&path);
let mut path = if path.is_relative() { let mut path = if path.is_relative() {

View File

@@ -31,12 +31,14 @@ use diesel::sqlite::Sqlite;
use notify::{Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; use notify::{Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use rayon::prelude::*; use rayon::prelude::*;
use log::{debug, error, info, warn}; use log::{debug, error, info, trace, warn};
use crate::auth::login; use crate::auth::login;
use crate::data::*; use crate::data::*;
use crate::database::*; use crate::database::*;
use crate::files::{is_image_or_video, is_valid_full_path, move_file, RealFileSystem, RefreshThumbnailsMessage}; use crate::files::{
is_image_or_video, is_valid_full_path, move_file, RealFileSystem, RefreshThumbnailsMessage,
};
use crate::service::ServiceBuilder; use crate::service::ServiceBuilder;
use crate::state::AppState; use crate::state::AppState;
use crate::tags::*; use crate::tags::*;
@@ -86,7 +88,7 @@ async fn get_image(
let thumbs = &app_state.thumbnail_path; let thumbs = &app_state.thumbnail_path;
let thumb_path = Path::new(&thumbs).join(relative_path); let thumb_path = Path::new(&thumbs).join(relative_path);
debug!("{:?}", thumb_path); trace!("Thumbnail path: {:?}", thumb_path);
if let Ok(file) = NamedFile::open(&thumb_path) { if let Ok(file) = NamedFile::open(&thumb_path) {
file.into_response(&request) file.into_response(&request)
} else { } else {

View File

@@ -6,7 +6,7 @@ use anyhow::Context;
use chrono::Utc; use chrono::Utc;
use diesel::dsl::count_star; use diesel::dsl::count_star;
use diesel::prelude::*; use diesel::prelude::*;
use log::{debug, info}; use log::{debug, info, trace};
use schema::{tagged_photo, tags}; use schema::{tagged_photo, tags};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::borrow::BorrowMut; use std::borrow::BorrowMut;
@@ -198,7 +198,8 @@ pub trait TagDao {
fn create_tag(&mut self, name: &str) -> anyhow::Result<Tag>; fn create_tag(&mut self, name: &str) -> anyhow::Result<Tag>;
fn remove_tag(&mut self, tag_name: &str, path: &str) -> anyhow::Result<Option<()>>; fn remove_tag(&mut self, tag_name: &str, path: &str) -> anyhow::Result<Option<()>>;
fn tag_file(&mut self, path: &str, tag_id: i32) -> anyhow::Result<TaggedPhoto>; fn tag_file(&mut self, path: &str, tag_id: i32) -> anyhow::Result<TaggedPhoto>;
fn get_files_with_tag_ids(&mut self, tag_ids: Vec<i32>) -> anyhow::Result<Vec<String>>; fn get_files_with_all_tag_ids(&mut self, tag_ids: Vec<i32>) -> anyhow::Result<Vec<String>>;
fn get_files_with_any_tag_ids(&mut self, tag_ids: Vec<i32>) -> anyhow::Result<Vec<String>>;
} }
pub struct SqliteTagDao { pub struct SqliteTagDao {
@@ -248,7 +249,7 @@ impl TagDao for SqliteTagDao {
} }
fn get_tags_for_path(&mut self, path: &str) -> anyhow::Result<Vec<Tag>> { fn get_tags_for_path(&mut self, path: &str) -> anyhow::Result<Vec<Tag>> {
debug!("Getting Tags for path: {:?}", path); trace!("Getting Tags for path: {:?}", path);
tags::table tags::table
.left_join(tagged_photo::table) .left_join(tagged_photo::table)
.filter(tagged_photo::photo_name.eq(&path)) .filter(tagged_photo::photo_name.eq(&path))
@@ -340,7 +341,7 @@ impl TagDao for SqliteTagDao {
}) })
} }
fn get_files_with_tag_ids(&mut self, tag_ids: Vec<i32>) -> anyhow::Result<Vec<String>> { fn get_files_with_all_tag_ids(&mut self, tag_ids: Vec<i32>) -> anyhow::Result<Vec<String>> {
use diesel::dsl::*; use diesel::dsl::*;
tagged_photo::table tagged_photo::table
@@ -352,6 +353,18 @@ impl TagDao for SqliteTagDao {
.get_results::<String>(&mut self.connection) .get_results::<String>(&mut self.connection)
.with_context(|| format!("Unable to get Tagged photos with ids: {:?}", tag_ids)) .with_context(|| format!("Unable to get Tagged photos with ids: {:?}", tag_ids))
} }
fn get_files_with_any_tag_ids(&mut self, tag_ids: Vec<i32>) -> anyhow::Result<Vec<String>> {
use diesel::dsl::*;
tagged_photo::table
.filter(tagged_photo::tag_id.eq_any(tag_ids.clone()))
.group_by(tagged_photo::photo_name)
.select((tagged_photo::photo_name, count(tagged_photo::tag_id)))
.select(tagged_photo::photo_name)
.get_results::<String>(&mut self.connection)
.with_context(|| format!("Unable to get Tagged photos with ids: {:?}", tag_ids))
}
} }
#[cfg(test)] #[cfg(test)]
@@ -472,7 +485,10 @@ mod tests {
} }
} }
fn get_files_with_tag_ids(&mut self, _tag_ids: Vec<i32>) -> anyhow::Result<Vec<String>> { fn get_files_with_all_tag_ids(
&mut self,
_tag_ids: Vec<i32>,
) -> anyhow::Result<Vec<String>> {
todo!() todo!()
} }
} }