Add the count of tagged files to All tags endpoint #21

Merged
cameron merged 4 commits from feature/include-tag-counts into master 2024-01-18 03:54:39 +00:00
4 changed files with 730 additions and 780 deletions

1407
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,30 +10,30 @@ edition = "2018"
lto = true lto = true
[dependencies] [dependencies]
actix = "0.12" actix = "0.13.1"
actix-web = "4" actix-web = "4"
actix-rt = "2.6" actix-rt = "2.6"
actix-files = "0.6" actix-files = "0.6"
actix-multipart = "0.4.0" actix-multipart = "0.6.1"
futures = "0.3.5" futures = "0.3.5"
jsonwebtoken = "7.2.0" jsonwebtoken = "9.2.0"
serde = "1" serde = "1"
serde_json = "1" serde_json = "1"
diesel = { version = "2.0.2", features = ["sqlite"] } diesel = { version = "2.0.2", features = ["sqlite"] }
diesel_migrations = "2.0.0" diesel_migrations = "2.0.0"
hmac = "0.11" hmac = "0.12.1"
sha2 = "0.9" sha2 = "0.10.8"
chrono = "0.4" chrono = "0.4"
dotenv = "0.15" dotenv = "0.15"
bcrypt = "0.9" bcrypt = "0.15.0"
image = { version = "0.23", default-features = false, features = ["jpeg", "png", "jpeg_rayon"] } image = { version = "0.24.7", default-features = false, features = ["jpeg", "png", "jpeg_rayon"] }
walkdir = "2.4.0" walkdir = "2.4.0"
rayon = "1.5" rayon = "1.5"
notify = "6.1.1" notify = "6.1.1"
path-absolutize = "3.0" path-absolutize = "3.0"
log="0.4" log="0.4"
env_logger="0.8" env_logger= "0.10.1"
actix-web-prom = "0.6" actix-web-prom = "0.7.0"
prometheus = "0.13" prometheus = "0.13"
lazy_static = "1.1" lazy_static = "1.1"
anyhow = "1.0" anyhow = "1.0"

View File

@@ -166,6 +166,11 @@ pub struct AddTagRequest {
pub tag_name: String, pub tag_name: String,
} }
#[derive(Deserialize)]
pub struct GetTagsRequest {
pub path: Option<String>,
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::Claims; use super::Claims;

View File

@@ -3,12 +3,14 @@ use actix_web::dev::{ServiceFactory, ServiceRequest};
use actix_web::{web, App, HttpResponse, Responder}; use actix_web::{web, App, HttpResponse, Responder};
use anyhow::Context; use anyhow::Context;
use chrono::Utc; use chrono::Utc;
use diesel::dsl::{count_star};
use diesel::prelude::*; use diesel::prelude::*;
use log::{debug, info}; use log::{debug, info};
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;
use std::sync::Mutex; use std::sync::Mutex;
use crate::data::GetTagsRequest;
pub fn add_tag_services<T, TagD: TagDao + 'static>(app: App<T>) -> App<T> pub fn add_tag_services<T, TagD: TagDao + 'static>(app: App<T>) -> App<T>
where where
@@ -34,9 +36,9 @@ async fn add_tag<D: TagDao>(
let mut tag_dao = tag_dao.lock().expect("Unable to get TagDao"); let mut tag_dao = tag_dao.lock().expect("Unable to get TagDao");
tag_dao tag_dao
.get_all_tags() .get_all_tags(None)
.and_then(|tags| { .and_then(|tags| {
if let Some(tag) = tags.iter().find(|t| t.name == tag_name) { if let Some((_, tag)) = tags.iter().find(|t| t.1.name == tag_name) {
Ok(tag.clone()) Ok(tag.clone())
} else { } else {
tag_dao.create_tag(&tag_name) tag_dao.create_tag(&tag_name)
@@ -59,11 +61,16 @@ async fn get_tags<D: TagDao>(
.into_http_internal_err() .into_http_internal_err()
} }
async fn get_all_tags<D: TagDao>(_: Claims, tag_dao: web::Data<Mutex<D>>) -> impl Responder { async fn get_all_tags<D: TagDao>(_: Claims, tag_dao: web::Data<Mutex<D>>, query: web::Query<GetTagsRequest>) -> impl Responder {
let mut tag_dao = tag_dao.lock().expect("Unable to get TagDao"); let mut tag_dao = tag_dao.lock().expect("Unable to get TagDao");
tag_dao tag_dao
.get_all_tags() .get_all_tags(query.path.clone())
.map(|tags| HttpResponse::Ok().json(tags)) .map(|tags| HttpResponse::Ok().json(tags.iter().map(|(tag_count, tag)|
TagWithTagCount {
tag: tag.clone(),
tag_count: *tag_count,
}
).collect::<Vec<TagWithTagCount>>()))
.into_http_internal_err() .into_http_internal_err()
} }
@@ -93,7 +100,7 @@ async fn update_tags<D: TagDao>(
let mut dao = tag_dao.lock().expect("Unable to get TagDao"); let mut dao = tag_dao.lock().expect("Unable to get TagDao");
dao.get_tags_for_path(&request.file_name) dao.get_tags_for_path(&request.file_name)
.and_then(|existing_tags| dao.get_all_tags().map(|all| (existing_tags, all))) .and_then(|existing_tags| dao.get_all_tags(None).map(|all| (existing_tags, all)))
.map(|(existing_tags, all_tags)| { .map(|(existing_tags, all_tags)| {
let tags_to_remove = existing_tags let tags_to_remove = existing_tags
.iter() .iter()
@@ -111,10 +118,10 @@ async fn update_tags<D: TagDao>(
let new_tags = all_tags let new_tags = all_tags
.iter() .iter()
.filter(|&t| !existing_tags.contains(t) && request.tag_ids.contains(&t.id)) .filter(|(_, t)| !existing_tags.contains(t) && request.tag_ids.contains(&t.id))
.collect::<Vec<&Tag>>(); .collect::<Vec<&(i64, Tag)>>();
for new_tag in new_tags { for (_, new_tag) in new_tags {
info!( info!(
"Adding tag {:?} to file: {:?}", "Adding tag {:?} to file: {:?}",
new_tag.name, request.file_name new_tag.name, request.file_name
@@ -142,6 +149,13 @@ pub struct Tag {
pub created_time: i64, pub created_time: i64,
} }
#[derive(Serialize, Debug)]
pub struct TagWithTagCount {
pub tag_count: i64,
pub tag: Tag,
}
#[derive(Insertable, Clone, Debug)] #[derive(Insertable, Clone, Debug)]
#[diesel(table_name = tags)] #[diesel(table_name = tags)]
pub struct InsertTag { pub struct InsertTag {
@@ -172,7 +186,7 @@ pub struct AddTagsRequest {
} }
pub trait TagDao { pub trait TagDao {
fn get_all_tags(&mut self) -> anyhow::Result<Vec<Tag>>; fn get_all_tags(&mut self, path: Option<String>) -> anyhow::Result<Vec<(i64, Tag)>>;
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>>;
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<()>>;
@@ -197,9 +211,29 @@ impl Default for SqliteTagDao {
} }
impl TagDao for SqliteTagDao { impl TagDao for SqliteTagDao {
fn get_all_tags(&mut self) -> anyhow::Result<Vec<Tag>> { fn get_all_tags(&mut self, path: Option<String>) -> anyhow::Result<Vec<(i64, Tag)>> {
// select name, count(*) from tags join tagged_photo ON tags.id = tagged_photo.tag_id GROUP BY tags.name ORDER BY COUNT(*);
let path = path.map(|p| p + "%").unwrap_or("%".to_string());
let (id, name, created_time) = tags::all_columns;
tags::table tags::table
.inner_join(tagged_photo::table)
.group_by(tags::id)
.select((count_star(), id, name, created_time))
.filter(tagged_photo::photo_name.like(path))
.get_results(&mut self.connection) .get_results(&mut self.connection)
.map::<Vec<(i64, Tag)>, _>(|tags_with_count: Vec<(i64, i32, String, i64)>| {
tags_with_count.iter().map(|tup| {
(
tup.0,
Tag {
id: tup.1,
name: tup.2.clone(),
created_time: tup.3,
},
)
}).collect()
})
.with_context(|| "Unable to get all tags") .with_context(|| "Unable to get all tags")
} }
@@ -337,8 +371,8 @@ mod tests {
} }
impl TagDao for TestTagDao { impl TagDao for TestTagDao {
fn get_all_tags(&mut self) -> anyhow::Result<Vec<Tag>> { fn get_all_tags(&mut self, _option: Option<String>) -> anyhow::Result<Vec<(i64, Tag)>> {
Ok(self.tags.borrow().clone()) Ok(self.tags.borrow().iter().map(|t| (1, t.clone())).collect::<Vec<(i64, Tag)>>().clone())
} }
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>> {
@@ -440,9 +474,9 @@ mod tests {
add_tag(claims, web::Json(body), tag_data.clone()).await; add_tag(claims, web::Json(body), tag_data.clone()).await;
let mut tag_dao = tag_data.lock().unwrap(); let mut tag_dao = tag_data.lock().unwrap();
let tags = tag_dao.get_all_tags().unwrap(); let tags = tag_dao.get_all_tags(None).unwrap();
assert_eq!(tags.len(), 1); assert_eq!(tags.len(), 1);
assert_eq!(tags.first().unwrap().name, "test-tag"); assert_eq!(tags.first().unwrap().1.name, "test-tag");
let tagged_photos = tag_dao.tagged_photos.borrow(); let tagged_photos = tag_dao.tagged_photos.borrow();
assert_eq!(tagged_photos["test.png"].len(), 1) assert_eq!(tagged_photos["test.png"].len(), 1)
} }
@@ -466,7 +500,7 @@ mod tests {
remove_tagged_photo(claims, web::Json(remove_request), tag_data.clone()).await; remove_tagged_photo(claims, web::Json(remove_request), tag_data.clone()).await;
let mut tag_dao = tag_data.lock().unwrap(); let mut tag_dao = tag_data.lock().unwrap();
let tags = tag_dao.get_all_tags().unwrap(); let tags = tag_dao.get_all_tags(None).unwrap();
assert!(tags.is_empty()); assert!(tags.is_empty());
let tagged_photos = tag_dao.tagged_photos.borrow(); let tagged_photos = tag_dao.tagged_photos.borrow();
let previously_added_tagged_photo = tagged_photos.get("test.png").unwrap(); let previously_added_tagged_photo = tagged_photos.get("test.png").unwrap();