feature/add-recursive-tag-support #26
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
45
src/files.rs
45
src/files.rs
@@ -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() {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
26
src/tags.rs
26
src/tags.rs
@@ -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!()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user