Merge pull request 'Add the count of tagged files to All tags endpoint' (#21) from feature/include-tag-counts into master
Reviewed-on: #21
This commit was merged in pull request #21.
This commit is contained in:
1407
Cargo.lock
generated
1407
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
18
Cargo.toml
18
Cargo.toml
@@ -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"
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
70
src/tags.rs
70
src/tags.rs
@@ -3,16 +3,18 @@ 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
|
||||||
T: ServiceFactory<ServiceRequest, Config = (), Error = actix_web::Error, InitError = ()>,
|
T: ServiceFactory<ServiceRequest, Config=(), Error=actix_web::Error, InitError=()>,
|
||||||
{
|
{
|
||||||
app.service(
|
app.service(
|
||||||
web::resource("image/tags")
|
web::resource("image/tags")
|
||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user