From b586c3df6cf6e07636077fbd85e8ff41f55ee591 Mon Sep 17 00:00:00 2001 From: Rasmus Kaj Date: Sat, 11 Feb 2023 17:14:10 +0100 Subject: [PATCH] Use async database access. - Use `diesel-async` with deadpool feature for database access. - A bunch of previously synchronous handlers are now async. - Some `.map` and simliar replaced with `if` blocks or `for` loops. --- CHANGELOG.md | 6 + Cargo.toml | 8 +- src/adm/findphotos.rs | 58 ++++-- src/adm/makepublic.rs | 24 +-- src/adm/precache.rs | 4 +- src/adm/stats.rs | 47 ++--- src/adm/users.rs | 28 +-- src/dbopt.rs | 27 +-- src/fetch_places.rs | 90 ++++----- src/main.rs | 12 +- src/models.rs | 124 +++++++----- src/photosdir.rs | 32 +-- src/server/admin.rs | 58 +++--- src/server/api.rs | 39 ++-- src/server/autocomplete.rs | 62 ++++-- src/server/context.rs | 13 +- src/server/error.rs | 11 +- src/server/image.rs | 6 +- src/server/login.rs | 16 +- src/server/mod.rs | 24 +-- src/server/search.rs | 41 ++-- src/server/splitlist.rs | 51 +++-- src/server/views_by_category.rs | 54 +++--- src/server/views_by_date.rs | 332 ++++++++++++++++---------------- 24 files changed, 660 insertions(+), 507 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d52b88a..fec7dec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ The format is based on ## Unreleased +* Database access is now async (PR #10). + - Use `diesel-async` with deadpool feature for database access. + - A bunch of previously synchronous handlers are now async. + - Some `.map` and simliar replaced with `if` blocks or `for` loops. + + ## Release 0.11.10 (2023-02-11) * Bugfix in month start calculation. diff --git a/Cargo.toml b/Cargo.toml index b363da4..b499f33 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,16 +10,20 @@ build = "src/build.rs" ructe = { version = "0.16.0", features = ["sass", "warp03"] } [dependencies] +async-trait = "0.1.64" +async-walkdir = "0.2.0" brotli = "3.3.0" chrono = "0.4.19" # Must match version used by diesel clap = { version = "4.0.18", features = ["derive", "wrap_help", "env"] } +diesel-async = { version = "0.2.0", features = ["deadpool", "postgres"] } dotenv = "0.15" flate2 = "1.0.14" +futures-lite = "1.12.0" image = "0.24.0" -medallion = "2.3.1" kamadak-exif = "0.5.0" lazy-regex = "2.2.2" libc = "0.2.68" +medallion = "2.3.1" mime = "0.3.0" r2d2-memcache = "0.6" rand = "0.8" @@ -38,7 +42,7 @@ version = "1.1.1" [dependencies.diesel] default-features = false -features = ["r2d2", "chrono", "postgres"] +features = ["chrono", "postgres"] version = "2.0.0" [dependencies.warp] diff --git a/src/adm/findphotos.rs b/src/adm/findphotos.rs index ff33d98..fe7476a 100644 --- a/src/adm/findphotos.rs +++ b/src/adm/findphotos.rs @@ -1,11 +1,11 @@ use super::result::Error; use crate::models::{Camera, Modification, Photo}; use crate::myexif::ExifData; -use crate::photosdir::PhotosDir; +use crate::photosdir::{load_meta, PhotosDir}; use crate::{DbOpt, DirOpt}; use diesel::insert_into; -use diesel::pg::PgConnection; use diesel::prelude::*; +use diesel_async::{AsyncPgConnection, RunQueryDsl}; use std::path::Path; use tracing::{debug, info, warn}; @@ -21,46 +21,60 @@ pub struct Findphotos { } impl Findphotos { - pub fn run(&self) -> Result<(), Error> { + pub async fn run(&self) -> Result<(), Error> { let pd = PhotosDir::new(&self.photos.photos_dir); - let mut db = self.db.connect()?; + let mut db = self.db.connect().await?; if !self.base.is_empty() { for base in &self.base { - crawl(&mut db, &pd, Path::new(base)).map_err(|e| { + crawl(&mut db, &pd, Path::new(base)).await.map_err(|e| { Error::Other(format!("Failed to crawl {base}: {e}")) })?; } } else { crawl(&mut db, &pd, Path::new("")) + .await .map_err(|e| Error::Other(format!("Failed to crawl: {e}")))?; } Ok(()) } } -fn crawl( - db: &mut PgConnection, +async fn crawl( + db: &mut AsyncPgConnection, photos: &PhotosDir, only_in: &Path, ) -> Result<(), Error> { - photos.find_files(only_in, &mut |path, exif| match save_photo( - db, path, exif, - ) { - Ok(()) => debug!("Saved photo {}", path), - Err(e) => warn!("Failed to save photo {}: {:?}", path, e), - })?; + use futures_lite::stream::StreamExt; + let mut entries = photos.walk_dir(only_in); + loop { + match entries.next().await { + None => break, + Some(Err(e)) => return Err(e.into()), + Some(Ok(entry)) => { + if entry.file_type().await?.is_file() { + let path = entry.path(); + if let Some(exif) = load_meta(&path) { + let sp = photos.subpath(&path)?; + save_photo(db, sp, &exif).await?; + } else { + debug!("Not an image: {path:?}"); + } + } + } + } + } Ok(()) } -fn save_photo( - db: &mut PgConnection, +async fn save_photo( + db: &mut AsyncPgConnection, file_path: &str, exif: &ExifData, ) -> Result<(), Error> { let width = exif.width.ok_or(Error::MissingWidth)?; let height = exif.height.ok_or(Error::MissingHeight)?; let rot = exif.rotation()?; - let cam = find_camera(db, exif)?; + let cam = find_camera(db, exif).await?; let photo = match Photo::create_or_set_basics( db, file_path, @@ -69,7 +83,9 @@ fn save_photo( exif.date(), rot, cam, - )? { + ) + .await? + { Modification::Created(photo) => { info!("Created #{}, {}", photo.id, photo.path); photo @@ -90,6 +106,7 @@ fn save_photo( .filter(photo_id.eq(photo.id)) .select((latitude, longitude)) .first::<(i32, i32)>(db) + .await { let lat = (lat * 1e6) as i32; let long = (long * 1e6) as i32; @@ -109,18 +126,19 @@ fn save_photo( longitude.eq((long * 1e6) as i32), )) .execute(db) + .await .expect("Insert image position"); } } Ok(()) } -fn find_camera( - db: &mut PgConnection, +async fn find_camera( + db: &mut AsyncPgConnection, exif: &ExifData, ) -> Result, Error> { if let Some((make, model)) = exif.camera() { - let cam = Camera::get_or_create(db, make, model)?; + let cam = Camera::get_or_create(db, make, model).await?; return Ok(Some(cam)); } Ok(None) diff --git a/src/adm/makepublic.rs b/src/adm/makepublic.rs index b694f08..5b8b092 100644 --- a/src/adm/makepublic.rs +++ b/src/adm/makepublic.rs @@ -1,10 +1,10 @@ use super::result::Error; use crate::models::Photo; use crate::DbOpt; -use diesel::pg::PgConnection; use diesel::prelude::*; use diesel::result::Error as DieselError; use diesel::update; +use diesel_async::{AsyncPgConnection, RunQueryDsl}; use std::fs::File; use std::io::prelude::*; use std::io::{self, BufReader}; @@ -27,8 +27,8 @@ pub struct Makepublic { } impl Makepublic { - pub fn run(&self) -> Result<(), Error> { - let mut db = self.db.connect()?; + pub async fn run(&self) -> Result<(), Error> { + let mut db = self.db.connect().await?; match ( self.list.as_ref().map(AsRef::as_ref), &self.tag, @@ -36,12 +36,12 @@ impl Makepublic { ) { (Some("-"), None, None) => { let list = io::stdin(); - by_file_list(&mut db, list.lock())?; + by_file_list(&mut db, list.lock()).await?; Ok(()) } (Some(list), None, None) => { let list = BufReader::new(File::open(list)?); - by_file_list(&mut db, list) + by_file_list(&mut db, list).await } (None, Some(tag), None) => { use crate::schema::photo_tags::dsl as pt; @@ -58,11 +58,12 @@ impl Makepublic { ), ) .set(p::is_public.eq(true)) - .execute(&mut db)?; + .execute(&mut db) + .await?; println!("Made {n} images public."); Ok(()) } - (None, None, Some(image)) => one(&mut db, image), + (None, None, Some(image)) => one(&mut db, image).await, (None, None, None) => Err(Error::Other( "No images specified to make public".to_string(), )), @@ -71,11 +72,12 @@ impl Makepublic { } } -pub fn one(db: &mut PgConnection, tpath: &str) -> Result<(), Error> { +async fn one(db: &mut AsyncPgConnection, tpath: &str) -> Result<(), Error> { use crate::schema::photos::dsl::*; match update(photos.filter(path.eq(&tpath))) .set(is_public.eq(true)) .get_result::(db) + .await { Ok(photo) => { println!("Made {tpath} public: {photo:?}"); @@ -88,12 +90,12 @@ pub fn one(db: &mut PgConnection, tpath: &str) -> Result<(), Error> { } } -pub fn by_file_list( - db: &mut PgConnection, +async fn by_file_list( + db: &mut AsyncPgConnection, list: In, ) -> Result<(), Error> { for line in list.lines() { - one(db, &line?)?; + one(db, &line?).await?; } Ok(()) } diff --git a/src/adm/precache.rs b/src/adm/precache.rs index 4de436a..2706827 100644 --- a/src/adm/precache.rs +++ b/src/adm/precache.rs @@ -4,6 +4,7 @@ use crate::photosdir::{get_scaled_jpeg, PhotosDir}; use crate::schema::photos::dsl::{date, is_public}; use crate::{CacheOpt, DbOpt, DirOpt}; use diesel::prelude::*; +use diesel_async::RunQueryDsl; use r2d2_memcache::memcache::Client; use std::time::{Duration, Instant}; use tracing::{debug, info}; @@ -37,7 +38,8 @@ impl Args { let (mut n, mut n_stored) = (0, 0); let photos = Photo::query(true) .order((is_public.desc(), date.desc().nulls_last())) - .load::(&mut self.db.connect()?)?; + .load::(&mut self.db.connect().await?) + .await?; let no_expire = 0; let pd = PhotosDir::new(&self.photos.photos_dir); for photo in photos { diff --git a/src/adm/stats.rs b/src/adm/stats.rs index 992fdf6..dff0af1 100644 --- a/src/adm/stats.rs +++ b/src/adm/stats.rs @@ -1,47 +1,42 @@ use super::result::Error; use crate::schema::people::dsl::people; -use crate::schema::photos::dsl::photos; +use crate::schema::photos::dsl::{self as p, photos}; use crate::schema::places::dsl::places; use crate::schema::tags::dsl::tags; -use diesel::dsl::{count_star, sql}; -use diesel::pg::PgConnection; +use diesel::dsl::count_star; use diesel::prelude::*; -use diesel::sql_types::{BigInt, Double, Nullable}; +use diesel::sql_types::{Nullable, Timestamp}; +use diesel_async::{AsyncPgConnection, RunQueryDsl}; -pub fn show_stats(db: &mut PgConnection) -> Result<(), Error> { +sql_function! { + #[aggregate] + fn year_of_timestamp(date: Nullable) -> Nullable +} + +pub async fn show_stats(db: &mut AsyncPgConnection) -> Result<(), Error> { println!( "There are {} photos in total.", - photos.select(count_star()).first::(db)?, + photos.select(count_star()).first::(db).await?, ); println!( "There are {} persons, {} places, and {} tags mentioned.", - people.select(count_star()).first::(db)?, - places.select(count_star()).first::(db)?, - tags.select(count_star()).first::(db)?, + people.select(count_star()).first::(db).await?, + places.select(count_star()).first::(db).await?, + tags.select(count_star()).first::(db).await?, ); - // Something like this should be possible, I guess? - // - // use schema::photos::dsl::date; - // let year = date_part("year", date).aliased("y"); - // println!("Count per year: {:?}", - // photos.select((year, count_star())) - // .group_by(year) - // .limit(10) - // .load::<(Option, i64)>(db)); - + let y = year_of_timestamp(p::date); println!( "Count per year: {:?}", photos - .select(sql::<(Nullable, BigInt)>( - "extract(year from date) y, count(*)" - )) - .group_by(sql::>("y")) - .order(sql::>("y").desc().nulls_last()) - .load::<(Option, i64)>(db)? + .select((y, count_star())) + .group_by(y) + .order(y.desc().nulls_last()) + .load::<(Option, i64)>(db) + .await? .iter() - .map(|&(y, n)| format!("{}: {}", y.unwrap_or(0.0), n)) + .map(|&(y, n)| format!("{}: {}", y.unwrap_or(0), n)) .collect::>(), ); diff --git a/src/adm/users.rs b/src/adm/users.rs index 63b1dc4..1016ce2 100644 --- a/src/adm/users.rs +++ b/src/adm/users.rs @@ -1,35 +1,39 @@ use super::result::Error; -use diesel::pg::PgConnection; +use crate::schema::users::dsl as u; use diesel::prelude::*; use diesel::{insert_into, update}; +use diesel_async::{AsyncPgConnection, RunQueryDsl}; use djangohashers::make_password; use rand::{thread_rng, Rng}; use std::iter::Iterator; -pub fn list(db: &mut PgConnection) -> Result<(), Error> { - use crate::schema::users::dsl::*; +pub async fn list(db: &mut AsyncPgConnection) -> Result<(), Error> { println!( "Existing users: {:?}.", - users.select(username).load::(db)?, + u::users.select(u::username).load::(db).await?, ); Ok(()) } -pub fn passwd(db: &mut PgConnection, uname: &str) -> Result<(), Error> { +pub async fn passwd( + db: &mut AsyncPgConnection, + uname: &str, +) -> Result<(), Error> { let pword = random_password(14); let hashword = make_password(&pword); - use crate::schema::users::dsl::*; - match update(users.filter(username.eq(&uname))) - .set(password.eq(&hashword)) - .execute(db)? + match update(u::users.filter(u::username.eq(&uname))) + .set(u::password.eq(&hashword)) + .execute(db) + .await? { 1 => { println!("Updated password for {uname:?} to {pword:?}"); } 0 => { - insert_into(users) - .values((username.eq(uname), password.eq(&hashword))) - .execute(db)?; + insert_into(u::users) + .values((u::username.eq(uname), u::password.eq(&hashword))) + .execute(db) + .await?; println!("Created user {uname:?} with password {pword:?}"); } n => { diff --git a/src/dbopt.rs b/src/dbopt.rs index 45d9b15..16e705b 100644 --- a/src/dbopt.rs +++ b/src/dbopt.rs @@ -1,12 +1,14 @@ use crate::Error; -use diesel::pg::PgConnection; -use diesel::r2d2::{ConnectionManager, Pool, PooledConnection}; -use diesel::{Connection, ConnectionError}; -use std::time::{Duration, Instant}; +use diesel::ConnectionError; +use diesel_async::pooled_connection::deadpool; +use diesel_async::pooled_connection::AsyncDieselConnectionManager; +use diesel_async::{AsyncConnection, AsyncPgConnection}; +use std::time::Instant; use tracing::debug; -pub type PgPool = Pool>; -pub type PooledPg = PooledConnection>; +/// An asynchronous postgres database connection pool. +pub type PgPool = deadpool::Pool; +pub type PooledPg = deadpool::Object; #[derive(clap::Parser)] pub struct DbOpt { @@ -16,19 +18,18 @@ pub struct DbOpt { } impl DbOpt { - pub fn connect(&self) -> Result { + pub async fn connect(&self) -> Result { let time = Instant::now(); - let db = PgConnection::establish(&self.db_url)?; + let db = AsyncPgConnection::establish(&self.db_url).await?; debug!("Got db connection in {:?}", time.elapsed()); Ok(db) } pub fn create_pool(&self) -> Result { let time = Instant::now(); - let pool = Pool::builder() - .min_idle(Some(2)) - .test_on_check_out(false) - .connection_timeout(Duration::from_millis(500)) - .build(ConnectionManager::new(&self.db_url))?; + let config = AsyncDieselConnectionManager::new(&self.db_url); + let pool = PgPool::builder(config) + .build() + .map_err(|e| Error::Other(format!("Pool creating error: {e}")))?; debug!("Created pool in {:?}", time.elapsed()); Ok(pool) } diff --git a/src/fetch_places.rs b/src/fetch_places.rs index e1d45be..ac82fed 100644 --- a/src/fetch_places.rs +++ b/src/fetch_places.rs @@ -1,7 +1,7 @@ -use crate::dbopt::PgPool; use crate::models::{Coord, Place}; use crate::DbOpt; use diesel::prelude::*; +use diesel_async::{AsyncPgConnection, RunQueryDsl}; use reqwest::{self, Client, Response}; use serde_json::Value; use slug::slugify; @@ -26,7 +26,7 @@ pub struct Fetchplaces { impl Fetchplaces { pub async fn run(&self) -> Result<(), super::adm::result::Error> { - let db = self.db.create_pool()?; + let mut db = self.db.connect().await?; if self.auto { println!("Should find {} photos to fetch places for", self.limit); use crate::schema::photo_places::dsl as place; @@ -38,14 +38,15 @@ impl Fetchplaces { )) .order(pos::photo_id.desc()) .limit(self.limit) - .load::<(i32, Coord)>(&mut db.get()?)?; + .load::<(i32, Coord)>(&mut db) + .await?; for (photo_id, coord) in result { println!("Find places for #{photo_id}, {coord:?}"); - self.overpass.update_image_places(&db, photo_id).await?; + self.overpass.update_image_places(&mut db, photo_id).await?; } } else { for photo in &self.photos { - self.overpass.update_image_places(&db, *photo).await?; + self.overpass.update_image_places(&mut db, *photo).await?; } } Ok(()) @@ -66,18 +67,15 @@ impl OverpassOpt { #[instrument(skip(self, db))] pub async fn update_image_places( &self, - db: &PgPool, + db: &mut AsyncPgConnection, image: i32, ) -> Result<(), Error> { use crate::schema::positions::dsl::*; let coord = positions .filter(photo_id.eq(image)) .select((latitude, longitude)) - .first::( - &mut db - .get() - .map_err(|e| Error::Pool(image, e.to_string()))?, - ) + .first::(db) + .await .optional() .map_err(|e| Error::Db(image, e))? .ok_or(Error::NoPosition(image))?; @@ -98,16 +96,14 @@ impl OverpassOpt { .and_then(|o| o.get("elements")) .and_then(Value::as_array) { - let mut c = - db.get().map_err(|e| Error::Pool(image, e.to_string()))?; for obj in elements { if let (Some(t_osm_id), Some((name, level))) = (osm_id(obj), name_and_level(obj)) { debug!("{}: {} (level {})", t_osm_id, name, level); - let place = - get_or_create_place(&mut c, t_osm_id, name, level) - .map_err(|e| Error::Db(image, e))?; + let place = get_or_create_place(db, t_osm_id, name, level) + .await + .map_err(|e| Error::Db(image, e))?; if place.osm_id.is_none() { debug!("Matched {:?} by name, update osm info", place); use crate::schema::places::dsl::*; @@ -117,7 +113,8 @@ impl OverpassOpt { osm_id.eq(Some(t_osm_id)), osm_level.eq(level), )) - .execute(&mut c) + .execute(db) + .await .map_err(|e| Error::Db(image, e))?; } use crate::models::PhotoPlace; @@ -125,7 +122,7 @@ impl OverpassOpt { let q = photo_places .filter(photo_id.eq(image)) .filter(place_id.eq(place.id)); - if q.first::(&mut c).is_ok() { + if q.first::(db).await.is_ok() { debug!( "Photo #{} already has {} ({})", image, place.id, place.place_name @@ -136,7 +133,8 @@ impl OverpassOpt { photo_id.eq(image), place_id.eq(place.id), )) - .execute(&mut c) + .execute(db) + .await .map_err(|e| Error::Db(image, e))?; } } else { @@ -294,45 +292,51 @@ fn tag_str<'a>(tags: &'a Value, name: &str) -> Option<&'a str> { tags.get(name).and_then(Value::as_str) } -fn get_or_create_place( - c: &mut PgConnection, +async fn get_or_create_place( + c: &mut AsyncPgConnection, t_osm_id: i64, name: &str, level: i16, ) -> Result { use crate::schema::places::dsl::*; - places + let place = places .filter( osm_id .eq(Some(t_osm_id)) .or(place_name.eq(name).and(osm_id.is_null())), ) .first::(c) - .or_else(|_| { - let mut result = diesel::insert_into(places) + .await + .optional()?; + if let Some(place) = place { + Ok(place) + } else { + let mut result = diesel::insert_into(places) + .values(( + place_name.eq(name), + slug.eq(slugify(name)), + osm_id.eq(Some(t_osm_id)), + osm_level.eq(Some(level)), + )) + .get_result::(c) + .await; + let mut attempt = 1; + while is_duplicate(&result) && attempt < 25 { + info!("Attempt #{} got {:?}, trying again", attempt, result); + attempt += 1; + let name = format!("{name} ({attempt})"); + result = diesel::insert_into(places) .values(( - place_name.eq(name), - slug.eq(slugify(name)), + place_name.eq(&name), + slug.eq(&slugify(&name)), osm_id.eq(Some(t_osm_id)), osm_level.eq(Some(level)), )) - .get_result::(c); - let mut attempt = 1; - while is_duplicate(&result) && attempt < 25 { - info!("Attempt #{} got {:?}, trying again", attempt, result); - attempt += 1; - let name = format!("{name} ({attempt})"); - result = diesel::insert_into(places) - .values(( - place_name.eq(&name), - slug.eq(&slugify(&name)), - osm_id.eq(Some(t_osm_id)), - osm_level.eq(Some(level)), - )) - .get_result::(c); - } - result - }) + .get_result::(c) + .await; + } + result + } } fn is_duplicate(r: &Result) -> bool { diff --git a/src/main.rs b/src/main.rs index 7e9cbf3..19e9e5b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -100,12 +100,14 @@ async fn main() { async fn run(args: &RPhotos) -> Result<(), Error> { match args { - RPhotos::Findphotos(cmd) => cmd.run(), - RPhotos::Makepublic(cmd) => cmd.run(), - RPhotos::Stats(db) => show_stats(&mut db.connect()?), - RPhotos::Userlist { db } => users::list(&mut db.connect()?), + RPhotos::Findphotos(cmd) => cmd.run().await, + RPhotos::Makepublic(cmd) => cmd.run().await, + RPhotos::Stats(db) => show_stats(&mut db.connect().await?).await, + RPhotos::Userlist { db } => { + users::list(&mut db.connect().await?).await + } RPhotos::Userpass { db, user } => { - users::passwd(&mut db.connect()?, user) + users::passwd(&mut db.connect().await?, user).await } RPhotos::Fetchplaces(cmd) => cmd.run().await, RPhotos::Precache(cmd) => cmd.run().await, diff --git a/src/models.rs b/src/models.rs index 9063185..4072436 100644 --- a/src/models.rs +++ b/src/models.rs @@ -10,11 +10,13 @@ use crate::schema::photos::dsl as p; use crate::schema::places::dsl as l; use crate::schema::positions::dsl as pos; use crate::schema::tags::dsl as t; +use async_trait::async_trait; use chrono::naive::NaiveDateTime; -use diesel::pg::{Pg, PgConnection}; +use diesel::pg::Pg; use diesel::prelude::*; use diesel::result::Error; use diesel::sql_types::Integer; +use diesel_async::{AsyncPgConnection, RunQueryDsl}; use slug::slugify; use std::cmp::max; @@ -28,17 +30,22 @@ pub struct PhotoDetails { pub camera: Option, } impl PhotoDetails { - pub fn load(id: i32, db: &mut PgConnection) -> Result { + pub async fn load( + id: i32, + db: &mut AsyncPgConnection, + ) -> Result { use crate::schema::photos::dsl::photos; - let photo = photos.find(id).first::(db)?; - let attribution = photo - .attribution_id - .map(|i| a::attributions.find(i).select(a::name).first(db)) - .transpose()?; - let camera = photo - .camera_id - .map(|i| c::cameras.find(i).first(db)) - .transpose()?; + let photo = photos.find(id).first::(db).await?; + let attribution = if let Some(id) = photo.attribution_id { + Some(a::attributions.find(id).select(a::name).first(db).await?) + } else { + None + }; + let camera = if let Some(id) = photo.camera_id { + Some(c::cameras.find(id).first(db).await?) + } else { + None + }; Ok(PhotoDetails { photo, @@ -50,7 +57,8 @@ impl PhotoDetails { .filter(ph::photo_id.eq(id)), ), ) - .load(db)?, + .load(db) + .await?, places: l::places .filter( l::id.eq_any( @@ -60,7 +68,8 @@ impl PhotoDetails { ), ) .order(l::osm_level.desc().nulls_first()) - .load(db)?, + .load(db) + .await?, tags: t::tags .filter( t::id.eq_any( @@ -69,11 +78,13 @@ impl PhotoDetails { .filter(pt::photo_id.eq(id)), ), ) - .load(db)?, + .load(db) + .await?, pos: pos::positions .filter(pos::photo_id.eq(id)) .select((pos::latitude, pos::longitude)) .first(db) + .await .optional()?, attribution, camera, @@ -132,8 +143,8 @@ impl Photo { } } - pub fn update_by_path( - db: &mut PgConnection, + pub async fn update_by_path( + db: &mut AsyncPgConnection, file_path: &str, newwidth: i32, newheight: i32, @@ -143,6 +154,7 @@ impl Photo { if let Some(mut pic) = p::photos .filter(p::path.eq(&file_path.to_string())) .first::(db) + .await .optional()? { let mut change = false; @@ -151,20 +163,23 @@ impl Photo { change = true; pic = diesel::update(p::photos.find(pic.id)) .set((p::width.eq(newwidth), p::height.eq(newheight))) - .get_result::(db)?; + .get_result::(db) + .await?; } if exifdate.is_some() && exifdate != pic.date { change = true; pic = diesel::update(p::photos.find(pic.id)) .set(p::date.eq(exifdate)) - .get_result::(db)?; + .get_result::(db) + .await?; } if let Some(ref camera) = *camera { if pic.camera_id != Some(camera.id) { change = true; pic = diesel::update(p::photos.find(pic.id)) .set(p::camera_id.eq(camera.id)) - .get_result::(db)?; + .get_result::(db) + .await?; } } Ok(Some(if change { @@ -177,8 +192,8 @@ impl Photo { } } - pub fn create_or_set_basics( - db: &mut PgConnection, + pub async fn create_or_set_basics( + db: &mut AsyncPgConnection, file_path: &str, newwidth: i32, newheight: i32, @@ -188,7 +203,9 @@ impl Photo { ) -> Result, Error> { if let Some(result) = Self::update_by_path( db, file_path, newwidth, newheight, exifdate, &camera, - )? { + ) + .await? + { Ok(result) } else { let pic = diesel::insert_into(p::photos) @@ -200,7 +217,8 @@ impl Photo { p::height.eq(newheight), p::camera_id.eq(camera.map(|c| c.id)), )) - .get_result::(db)?; + .get_result::(db) + .await?; Ok(Modification::Created(pic)) } } @@ -237,8 +255,12 @@ impl Photo { } } +#[async_trait] pub trait Facet { - fn by_slug(slug: &str, db: &mut PgConnection) -> Result + async fn by_slug( + slug: &str, + db: &mut AsyncPgConnection, + ) -> Result where Self: Sized; } @@ -250,9 +272,13 @@ pub struct Tag { pub tag_name: String, } +#[async_trait] impl Facet for Tag { - fn by_slug(slug: &str, db: &mut PgConnection) -> Result { - t::tags.filter(t::slug.eq(slug)).first(db) + async fn by_slug( + slug: &str, + db: &mut AsyncPgConnection, + ) -> Result { + t::tags.filter(t::slug.eq(slug)).first(db).await } } @@ -271,27 +297,33 @@ pub struct Person { } impl Person { - pub fn get_or_create_name( - db: &mut PgConnection, + pub async fn get_or_create_name( + db: &mut AsyncPgConnection, name: &str, ) -> Result { - h::people + if let Some(name) = h::people .filter(h::person_name.ilike(name)) .first(db) - .or_else(|_| { - diesel::insert_into(h::people) - .values(( - h::person_name.eq(name), - h::slug.eq(&slugify(name)), - )) - .get_result(db) - }) + .await + .optional()? + { + Ok(name) + } else { + diesel::insert_into(h::people) + .values((h::person_name.eq(name), h::slug.eq(&slugify(name)))) + .get_result(db) + .await + } } } +#[async_trait] impl Facet for Person { - fn by_slug(slug: &str, db: &mut PgConnection) -> Result { - h::people.filter(h::slug.eq(slug)).first(db) + async fn by_slug( + slug: &str, + db: &mut AsyncPgConnection, + ) -> Result { + h::people.filter(h::slug.eq(slug)).first(db).await } } @@ -311,9 +343,13 @@ pub struct Place { pub osm_level: Option, } +#[async_trait] impl Facet for Place { - fn by_slug(slug: &str, db: &mut PgConnection) -> Result { - l::places.filter(l::slug.eq(slug)).first(db) + async fn by_slug( + slug: &str, + db: &mut AsyncPgConnection, + ) -> Result { + l::places.filter(l::slug.eq(slug)).first(db).await } } @@ -332,8 +368,8 @@ pub struct Camera { } impl Camera { - pub fn get_or_create( - db: &mut PgConnection, + pub async fn get_or_create( + db: &mut AsyncPgConnection, make: &str, modl: &str, ) -> Result { @@ -341,6 +377,7 @@ impl Camera { .filter(c::manufacturer.eq(make)) .filter(c::model.eq(modl)) .first::(db) + .await .optional()? { Ok(camera) @@ -348,6 +385,7 @@ impl Camera { diesel::insert_into(c::cameras) .values((c::manufacturer.eq(make), c::model.eq(modl))) .get_result(db) + .await } } } diff --git a/src/photosdir.rs b/src/photosdir.rs index a5fc4dc..67b9779 100644 --- a/src/photosdir.rs +++ b/src/photosdir.rs @@ -1,11 +1,12 @@ use crate::models::Photo; use crate::myexif::ExifData; +use async_walkdir::WalkDir; use image::imageops::FilterType; use image::{self, DynamicImage, ImageError, ImageFormat}; use std::ffi::OsStr; +use std::io; use std::path::{Path, PathBuf}; use std::time::Instant; -use std::{fs, io}; use tokio::task::{spawn_blocking, JoinError}; use tracing::{debug, info, warn}; @@ -28,29 +29,14 @@ impl PhotosDir { self.basedir.join(Path::new(path)).is_file() } - pub fn find_files( - &self, - dir: &Path, - cb: &mut dyn FnMut(&str, &ExifData), - ) -> io::Result<()> { - let absdir = self.basedir.join(dir); - if fs::metadata(&absdir)?.is_dir() { - debug!("Should look in {:?}", absdir); - for entry in fs::read_dir(absdir)? { - let path = entry?.path(); - if fs::metadata(&path)?.is_dir() { - self.find_files(&path, cb)?; - } else if let Some(exif) = load_meta(&path) { - cb(self.subpath(&path)?, &exif); - } else { - debug!("{:?} is no pic.", path) - } - } - } - Ok(()) + pub fn walk_dir(&self, dir: &Path) -> WalkDir { + WalkDir::new(self.basedir.join(dir)) } - fn subpath<'a>(&self, fullpath: &'a Path) -> Result<&'a str, io::Error> { + pub fn subpath<'a>( + &self, + fullpath: &'a Path, + ) -> Result<&'a str, io::Error> { let path = fullpath .strip_prefix(&self.basedir) .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?; @@ -63,7 +49,7 @@ impl PhotosDir { } } -fn load_meta(path: &Path) -> Option { +pub fn load_meta(path: &Path) -> Option { if let Ok(mut exif) = ExifData::read_from(path) { if exif.width.is_none() || exif.height.is_none() { if let Ok((width, height)) = actual_image_size(path) { diff --git a/src/server/admin.rs b/src/server/admin.rs index cebefba..82508fa 100644 --- a/src/server/admin.rs +++ b/src/server/admin.rs @@ -3,6 +3,7 @@ use super::error::ViewResult; use super::{redirect_to_img, wrap, Context, Result, ViewError}; use crate::models::{Coord, Photo, SizeTag}; use diesel::{self, prelude::*}; +use diesel_async::{AsyncPgConnection, RunQueryDsl, SaveChangesDsl}; use serde::Deserialize; use slug::slugify; use tracing::{info, warn}; @@ -21,7 +22,7 @@ pub fn routes(s: BoxedFilter<(Context,)>) -> BoxedFilter<(Response,)> { .unify() .or(path("person").and(s.clone()).and(form()).then(set_person)) .unify() - .or(path("rotate").and(s.clone()).and(form()).map(rotate)) + .or(path("rotate").and(s.clone()).and(form()).then(rotate)) .unify() .or(path("tag").and(s).and(form()).then(set_tag)) .unify() @@ -29,20 +30,20 @@ pub fn routes(s: BoxedFilter<(Context,)>) -> BoxedFilter<(Response,)> { post().and(route).boxed() } -fn rotate(context: Context, form: RotateForm) -> Result { +async fn rotate(context: Context, form: RotateForm) -> Result { if !context.is_authorized() { return Err(ViewError::PermissionDenied); } info!("Should rotate #{} by {}", form.image, form.angle); use crate::schema::photos::dsl::photos; - let mut c = context.db()?; - let c: &mut PgConnection = &mut c; + let mut c = context.db().await?; + let c: &mut AsyncPgConnection = &mut c; let mut image = - or_404q!(photos.find(form.image).first::(c), context); + or_404q!(photos.find(form.image).first::(c).await, context); let newvalue = (360 + image.rotation + form.angle) % 360; info!("Rotation was {}, setting to {}", image.rotation, newvalue); image.rotation = newvalue; - let image = image.save_changes::(c)?; + let image = image.save_changes::(c).await?; context.clear_cache(&image.cache_key(SizeTag::Small)); context.clear_cache(&image.cache_key(SizeTag::Medium)); Builder::new().body("ok".into()).ise() @@ -58,33 +59,37 @@ async fn set_tag(context: Context, form: TagForm) -> Result { if !context.is_authorized() { return Err(ViewError::PermissionDenied); } - let mut c = context.db()?; + let mut c = context.db().await?; use crate::models::Tag; let tag = { use crate::schema::tags::dsl::*; - tags.filter(tag_name.ilike(&form.tag)) + if let Some(tag) = tags + .filter(tag_name.ilike(&form.tag)) .first::(&mut c) - .or_else(|_| { - diesel::insert_into(tags) - .values(( - tag_name.eq(&form.tag), - slug.eq(&slugify(&form.tag)), - )) - .get_result::(&mut c) - })? + .await + .optional()? + { + tag + } else { + diesel::insert_into(tags) + .values((tag_name.eq(&form.tag), slug.eq(&slugify(&form.tag)))) + .get_result::(&mut c) + .await? + } }; use crate::schema::photo_tags::dsl::*; let q = photo_tags .filter(photo_id.eq(form.image)) .filter(tag_id.eq(tag.id)) .count(); - if q.get_result::(&mut c)? > 0 { + if q.get_result::(&mut c).await? > 0 { info!("Photo #{} already has {:?}", form.image, form.tag); } else { info!("Add {:?} on photo #{}!", form.tag, form.image); diesel::insert_into(photo_tags) .values((photo_id.eq(form.image), tag_id.eq(tag.id))) - .execute(&mut c)?; + .execute(&mut c) + .await?; } Ok(redirect_to_img(form.image)) } @@ -99,20 +104,21 @@ async fn set_person(context: Context, form: PersonForm) -> Result { if !context.is_authorized() { return Err(ViewError::PermissionDenied); } - let mut c = context.db()?; + let mut c = context.db().await?; use crate::models::{Person, PhotoPerson}; - let person = Person::get_or_create_name(&mut c, &form.person)?; + let person = Person::get_or_create_name(&mut c, &form.person).await?; use crate::schema::photo_people::dsl::*; let q = photo_people .filter(photo_id.eq(form.image)) .filter(person_id.eq(person.id)); - if q.first::(&mut c).optional()?.is_some() { + if q.first::(&mut c).await.optional()?.is_some() { info!("Photo #{} already has {:?}", form.image, person); } else { info!("Add {:?} on photo #{}!", person, form.image); diesel::insert_into(photo_people) .values((photo_id.eq(form.image), person_id.eq(person.id))) - .execute(&mut c)?; + .execute(&mut c) + .await?; } Ok(redirect_to_img(form.image)) } @@ -132,7 +138,7 @@ async fn set_grade(context: Context, form: GradeForm) -> Result { use crate::schema::photos::dsl::{grade, photos}; let q = diesel::update(photos.find(form.image)).set(grade.eq(form.grade)); - match q.execute(&mut context.db()?)? { + match q.execute(&mut context.db().await?).await? { 1 => { return Ok(redirect_to_img(form.image)); } @@ -168,15 +174,17 @@ async fn set_location(context: Context, form: CoordForm) -> Result { let (lat, lng) = ((coord.x * 1e6) as i32, (coord.y * 1e6) as i32); use crate::schema::positions::dsl::*; use diesel::insert_into; + let mut db = context.db().await?; insert_into(positions) .values((photo_id.eq(image), latitude.eq(lat), longitude.eq(lng))) .on_conflict(photo_id) .do_update() .set((latitude.eq(lat), longitude.eq(lng))) - .execute(&mut context.db()?)?; + .execute(&mut db) + .await?; match context .overpass() - .update_image_places(&context.db_pool(), form.image) + .update_image_places(&mut db, form.image) .await { Ok(()) => (), diff --git a/src/server/api.rs b/src/server/api.rs index 9ff2c18..c1abef6 100644 --- a/src/server/api.rs +++ b/src/server/api.rs @@ -3,6 +3,7 @@ use super::login::LoginForm; use super::{Context, ViewError}; use crate::models::{Photo, SizeTag}; use diesel::{self, prelude::*, result::Error as DbError, update}; +use diesel_async::{AsyncPgConnection, RunQueryDsl}; use serde::{Deserialize, Serialize}; use warp::filters::BoxedFilter; use warp::http::StatusCode; @@ -21,15 +22,15 @@ pub fn routes(s: BoxedFilter<(Context,)>) -> BoxedFilter<(Response,)> { .and(post()) .and(s.clone()) .and(body::json()) - .map(login) + .then(login) .map(w); - let gimg = end().and(get()).and(s.clone()).and(query()).map(get_img); + let gimg = end().and(get()).and(s.clone()).and(query()).then(get_img); let pimg = path("makepublic") .and(end()) .and(post()) .and(s) .and(body::json()) - .map(make_public); + .then(make_public); login .or(path("image").and(gimg.or(pimg).unify().map(w))) @@ -59,10 +60,11 @@ fn w(result: ApiResult) -> Response { } } -fn login(context: Context, form: LoginForm) -> ApiResult { - let mut db = context.db()?; +async fn login(context: Context, form: LoginForm) -> ApiResult { + let mut db = context.db().await?; let user = form .validate(&mut db) + .await .ok_or_else(|| ApiError::bad_request("login failed"))?; tracing::info!("Api login {user:?} ok"); Ok(LoginOk { @@ -98,31 +100,37 @@ enum ImgIdentifier { } impl ImgIdentifier { - fn load(&self, db: &mut PgConnection) -> Result, DbError> { + async fn get( + &self, + db: &mut AsyncPgConnection, + ) -> Result, DbError> { use crate::schema::photos::dsl as p; match &self { ImgIdentifier::Id(ref id) => { - p::photos.filter(p::id.eq(*id as i32)).first(db) + p::photos.filter(p::id.eq(*id as i32)).first(db).await } ImgIdentifier::Path(path) => { - p::photos.filter(p::path.eq(path)).first(db) + p::photos.filter(p::path.eq(path)).first(db).await } } .optional() } } -fn get_img(context: Context, q: ImgQuery) -> ApiResult { +async fn get_img(context: Context, q: ImgQuery) -> ApiResult { let id = q.validate().map_err(ApiError::bad_request)?; - let mut db = context.db()?; - let img = id.load(&mut db)?.ok_or(NOT_FOUND)?; + let mut db = context.db().await?; + let img = id.get(&mut db).await?.ok_or(NOT_FOUND)?; if !context.is_authorized() && !img.is_public() { return Err(NOT_FOUND); } Ok(GetImgResult::for_img(&img)) } -fn make_public(context: Context, q: ImgQuery) -> ApiResult { +async fn make_public( + context: Context, + q: ImgQuery, +) -> ApiResult { if !context.is_authorized() { return Err(ApiError { code: StatusCode::UNAUTHORIZED, @@ -130,12 +138,13 @@ fn make_public(context: Context, q: ImgQuery) -> ApiResult { }); } let id = q.validate().map_err(ApiError::bad_request)?; - let mut db = context.db()?; - let img = id.load(&mut db)?.ok_or(NOT_FOUND)?; + let mut db = context.db().await?; + let img = id.get(&mut db).await?.ok_or(NOT_FOUND)?; use crate::schema::photos::dsl as p; let img = update(p::photos.find(img.id)) .set(p::is_public.eq(true)) - .get_result(&mut db)?; + .get_result(&mut db) + .await?; Ok(GetImgResult::for_img(&img)) } diff --git a/src/server/autocomplete.rs b/src/server/autocomplete.rs index c390686..88427b8 100644 --- a/src/server/autocomplete.rs +++ b/src/server/autocomplete.rs @@ -8,6 +8,7 @@ use crate::schema::places::dsl as l; use crate::schema::tags::dsl as t; use diesel::prelude::*; use diesel::sql_types::Text; +use diesel_async::RunQueryDsl; use serde::{Deserialize, Serialize}; use std::cmp::Ordering; use std::fmt::Display; @@ -20,23 +21,26 @@ use warp::Filter; pub fn routes(s: BoxedFilter<(Context,)>) -> BoxedFilter<(Response,)> { let egs = end().and(get()).and(s); - let any = egs.clone().and(query()).map(list_any); - let tag = path("tag").and(egs.clone()).and(query()).map(list_tags); - let person = path("person").and(egs).and(query()).map(list_people); + let any = egs.clone().and(query()).then(list_any); + let tag = path("tag").and(egs.clone()).and(query()).then(list_tags); + let person = path("person").and(egs).and(query()).then(list_people); any.or(tag).unify().or(person).unify().map(wrap).boxed() } -fn list_any(context: Context, term: AcQ) -> Result { - let mut tags = select_tags(&context, &term)? +async fn list_any(context: Context, term: AcQ) -> Result { + let mut tags = select_tags(&context, &term) + .await? .into_iter() .map(|(t, s, p)| (SearchTag { k: 't', t, s }, p)) .chain({ - select_people(&context, &term)? + select_people(&context, &term) + .await? .into_iter() .map(|(t, s, p)| (SearchTag { k: 'p', t, s }, p)) }) .chain({ - select_places(&context, &term)? + select_places(&context, &term) + .await? .into_iter() .map(|(t, s, p)| (SearchTag { k: 'l', t, s }, p)) }) @@ -45,12 +49,12 @@ fn list_any(context: Context, term: AcQ) -> Result { Ok(json(&tags.iter().map(|(t, _)| t).collect::>())) } -fn list_tags(context: Context, query: AcQ) -> Result { - Ok(json(&names(select_tags(&context, &query)?))) +async fn list_tags(context: Context, query: AcQ) -> Result { + Ok(json(&names(select_tags(&context, &query).await?))) } -fn list_people(context: Context, query: AcQ) -> Result { - Ok(json(&names(select_people(&context, &query)?))) +async fn list_people(context: Context, query: AcQ) -> Result { + Ok(json(&names(select_people(&context, &query).await?))) } fn names(data: Vec) -> Vec { @@ -147,7 +151,10 @@ sql_function!(fn strpos(string: Text, substring: Text) -> Integer); type NameSlugScore = (String, String, i32); -fn select_tags(context: &Context, term: &AcQ) -> Result> { +async fn select_tags( + context: &Context, + term: &AcQ, +) -> Result> { let tpos = strpos(lower(t::tag_name), &term.q); let query = t::tags .select((t::tag_name, t::slug, tpos)) @@ -160,11 +167,18 @@ fn select_tags(context: &Context, term: &AcQ) -> Result> { tp::photo_id.eq_any(p::photos.select(p::id).filter(p::is_public)), ))) }; - let mut db = context.db()?; - Ok(query.order((tpos, t::tag_name)).limit(10).load(&mut db)?) + let mut db = context.db().await?; + Ok(query + .order((tpos, t::tag_name)) + .limit(10) + .load(&mut db) + .await?) } -fn select_people(context: &Context, term: &AcQ) -> Result> { +async fn select_people( + context: &Context, + term: &AcQ, +) -> Result> { let ppos = strpos(lower(h::person_name), &term.q); let query = h::people .select((h::person_name, h::slug, ppos)) @@ -182,14 +196,18 @@ fn select_people(context: &Context, term: &AcQ) -> Result> { ), ) }; - let mut db = context.db()?; + let mut db = context.db().await?; Ok(query .order((ppos, h::person_name)) .limit(10) - .load(&mut db)?) + .load(&mut db) + .await?) } -fn select_places(context: &Context, term: &AcQ) -> Result> { +async fn select_places( + context: &Context, + term: &AcQ, +) -> Result> { let lpos = strpos(lower(l::place_name), &term.q); let query = l::places .select((l::place_name, l::slug, lpos)) @@ -207,6 +225,10 @@ fn select_places(context: &Context, term: &AcQ) -> Result> { ), ) }; - let mut db = context.db()?; - Ok(query.order((lpos, l::place_name)).limit(10).load(&mut db)?) + let mut db = context.db().await?; + Ok(query + .order((lpos, l::place_name)) + .limit(10) + .load(&mut db) + .await?) } diff --git a/src/server/context.rs b/src/server/context.rs index 3ea7797..d2f31d0 100644 --- a/src/server/context.rs +++ b/src/server/context.rs @@ -3,9 +3,8 @@ use crate::adm::result::Error; use crate::dbopt::{PgPool, PooledPg}; use crate::fetch_places::OverpassOpt; use crate::photosdir::PhotosDir; -use diesel::r2d2::{Pool, PooledConnection}; use medallion::{Header, Payload, Token}; -use r2d2_memcache::MemcacheConnectionManager; +use r2d2_memcache::{r2d2, MemcacheConnectionManager}; use std::future::Future; use std::sync::Arc; use std::time::Duration; @@ -15,8 +14,8 @@ use warp::path::{self, FullPath}; use warp::{self, Filter}; pub type ContextFilter = BoxedFilter<(Context,)>; -type MemcachePool = Pool; -type PooledMemcache = PooledConnection; +type MemcachePool = r2d2::Pool; +type PooledMemcache = r2d2::PooledConnection; pub fn create_session_filter(args: &Args) -> Result { let global = Arc::new(GlobalContext::new(args)?); @@ -61,7 +60,7 @@ impl GlobalContext { Error::Other(format!("Failed to create db pool: {e}")) })?, photosdir: PhotosDir::new(&args.photos.photos_dir), - memcache_pool: Pool::builder() + memcache_pool: r2d2::Pool::builder() .connection_timeout(Duration::from_secs(1)) .build(mc_manager) .map_err(|e| { @@ -121,8 +120,8 @@ pub struct Context { } impl Context { - pub fn db(&self) -> Result { - Ok(self.global.db_pool.get()?) + pub async fn db(&self) -> Result { + Ok(self.global.db_pool.get().await?) } pub fn db_pool(&self) -> PgPool { self.global.db_pool.clone() diff --git a/src/server/error.rs b/src/server/error.rs index 0c861ef..db8fa97 100644 --- a/src/server/error.rs +++ b/src/server/error.rs @@ -1,6 +1,7 @@ use super::Context; use crate::photosdir::ImageLoadFailed; use crate::templates::{self, RenderError, RenderRucte}; +use diesel_async::pooled_connection::deadpool::PoolError; use tracing::{error, warn}; use warp::http::response::Builder; use warp::http::status::StatusCode; @@ -128,8 +129,8 @@ impl From for ViewError { impl From for ViewError { fn from(e: r2d2_memcache::memcache::Error) -> Self { - error!("Pool error: {:?}", e); - ViewError::Err("Pool error") + error!("Memcache pool error: {:?}", e); + ViewError::ServiceUnavailable } } impl From for ViewError { @@ -138,6 +139,12 @@ impl From for ViewError { ViewError::Err("Failed to load image") } } +impl From for ViewError { + fn from(value: PoolError) -> Self { + error!("Database pool error: {value}"); + ViewError::ServiceUnavailable + } +} /// Create custom errors for warp rejections. /// diff --git a/src/server/image.rs b/src/server/image.rs index 3315b5a..5168583 100644 --- a/src/server/image.rs +++ b/src/server/image.rs @@ -3,6 +3,7 @@ use super::{error::ViewResult, Context, Result, ViewError}; use crate::models::{Photo, SizeTag}; use crate::photosdir::{get_scaled_jpeg, ImageLoadFailed}; use diesel::prelude::*; +use diesel_async::RunQueryDsl; use std::str::FromStr; use warp::http::response::Builder; use warp::http::{header, StatusCode}; @@ -10,7 +11,10 @@ use warp::reply::Response; pub async fn show_image(img: ImgName, context: Context) -> Result { use crate::schema::photos::dsl::photos; - let tphoto = photos.find(img.id).first::(&mut context.db()?); + let tphoto = photos + .find(img.id) + .first::(&mut context.db().await?) + .await; if let Ok(tphoto) = tphoto { if context.is_authorized() || tphoto.is_public() { if img.size == SizeTag::Large { diff --git a/src/server/login.rs b/src/server/login.rs index 75ee276..7c84b56 100644 --- a/src/server/login.rs +++ b/src/server/login.rs @@ -1,7 +1,7 @@ use super::{wrap, BuilderExt, Context, ContextFilter, RenderRucte, Result}; use crate::templates; -use diesel::pg::PgConnection; use diesel::prelude::*; +use diesel_async::{AsyncPgConnection, RunQueryDsl}; use lazy_regex::regex_is_match; use serde::Deserialize; use tracing::info; @@ -16,7 +16,7 @@ use warp::{body, get, path, post, Filter}; pub fn routes(s: ContextFilter) -> BoxedFilter<(Response,)> { let s = move || s.clone(); let get_form = get().and(s()).and(query()).map(get_login); - let post_form = post().and(s()).and(body::form()).map(post_login); + let post_form = post().and(s()).and(body::form()).then(post_login); let login = path("login") .and(end()) .and(get_form.or(post_form).unify().map(wrap)); @@ -36,10 +36,10 @@ struct NextQ { next: Option, } -fn post_login(context: Context, form: LoginForm) -> Result { +async fn post_login(context: Context, form: LoginForm) -> Result { let next = sanitize_next(form.next.as_ref().map(AsRef::as_ref)); - let mut db = context.db()?; - if let Some(user) = form.validate(&mut db) { + let mut db = context.db().await?; + if let Some(user) = form.validate(&mut db).await { let token = context.make_token(&user)?; return Ok(Builder::new() .header( @@ -64,12 +64,16 @@ pub struct LoginForm { impl LoginForm { /// Retur user if and only if password is correct for user. - pub fn validate(&self, db: &mut PgConnection) -> Option { + pub async fn validate( + &self, + db: &mut AsyncPgConnection, + ) -> Option { use crate::schema::users::dsl::*; if let Ok(hash) = users .filter(username.eq(&self.user)) .select(password) .first::(db) + .await { if djangohashers::check_password_tolerant(&self.password, &hash) { info!("User {} logged in", self.user); diff --git a/src/server/mod.rs b/src/server/mod.rs index 26c13f0..7e771a4 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -30,6 +30,7 @@ use crate::pidfiles::handle_pid_file; use crate::templates::{self, Html, RenderRucte}; use chrono::Datelike; use diesel::prelude::*; +use diesel_async::RunQueryDsl; use serde::Deserialize; use std::net::SocketAddr; use std::path::PathBuf; @@ -84,7 +85,7 @@ pub async fn run(args: &Args) -> Result<(), Error> { .and(static_routes) .or(login::routes(s())) .or(path("img").and( - param().and(end()).and(get()).and(s()).map(photo_details) + param().and(end()).and(get()).and(s()).then(photo_details) .or(param().and(end()).and(get()).and(s()).then(image::show_image)) .unify() .map(wrap))) @@ -92,9 +93,9 @@ pub async fn run(args: &Args) -> Result<(), Error> { .or(path("person").and(person_routes(s()))) .or(path("place").and(place_routes(s()))) .or(path("tag").and(tag_routes(s()))) - .or(path("random").and(end()).and(get()).and(s()).map(random_image).map(wrap)) + .or(path("random").and(end()).and(get()).and(s()).then(random_image).map(wrap)) .or(path("ac").and(autocomplete::routes(s()))) - .or(path("search").and(end()).and(get()).and(s()).and(query()).map(search).map(wrap)) + .or(path("search").and(end()).and(get()).and(s()).and(query()).then(search).map(wrap)) .or(path("api").and(api::routes(s()))) .or(path("adm").and(admin::routes(s()))) .or(path("robots.txt") @@ -139,23 +140,24 @@ async fn static_file(name: Tail) -> Result { .ise() } -fn random_image(context: Context) -> Result { +async fn random_image(context: Context) -> Result { use crate::schema::photos::dsl::id; - use diesel::dsl::sql; - use diesel::sql_types::Integer; + sql_function! { fn random() -> Integer }; + let photo = Photo::query(context.is_authorized()) .select(id) .limit(1) - .order(sql::("random()")) - .first(&mut context.db()?)?; + .order(random()) + .first(&mut context.db().await?) + .await?; info!("Random: {:?}", photo); Ok(redirect_to_img(photo)) } -fn photo_details(id: i32, context: Context) -> Result { - let mut c = context.db()?; - let photo = or_404q!(PhotoDetails::load(id, &mut c), context); +async fn photo_details(id: i32, context: Context) -> Result { + let mut c = context.db().await?; + let photo = or_404q!(PhotoDetails::load(id, &mut c).await, context); if context.is_authorized() || photo.is_public() { Ok(Builder::new().html(|o| { diff --git a/src/server/search.rs b/src/server/search.rs index 0c98bb6..394b862 100644 --- a/src/server/search.rs +++ b/src/server/search.rs @@ -9,18 +9,18 @@ use crate::schema::photo_tags::dsl as pt; use crate::schema::photos::dsl as p; use crate::templates; use chrono::{NaiveDate, NaiveDateTime, NaiveTime}; -use diesel::pg::PgConnection; use diesel::prelude::*; +use diesel_async::{AsyncPgConnection, RunQueryDsl}; use tracing::warn; use warp::http::response::Builder; use warp::reply::Response; -pub fn search( +pub async fn search( context: Context, query: Vec<(String, String)>, ) -> Result { - let mut db = context.db()?; - let query = SearchQuery::load(query, &mut db)?; + let mut db = context.db().await?; + let query = SearchQuery::load(query, &mut db).await?; let mut photos = Photo::query(context.is_authorized()); if let Some(since) = query.since.as_ref() { @@ -71,10 +71,11 @@ pub fn search( let photos = photos .order((p::date.desc().nulls_last(), p::id.desc())) - .load(&mut db)?; + .load(&mut db) + .await?; let n = photos.len(); - let coords = get_positions(&photos, &mut db)?; + let coords = get_positions(&photos, &mut db).await?; let links = split_to_group_links(&photos, &query.to_base_url(), true); Ok(Builder::new().html(|o| { @@ -104,12 +105,12 @@ pub struct Filter { } impl Filter { - fn load(val: &str, db: &mut PgConnection) -> Option> { + async fn load(val: &str, db: &mut AsyncPgConnection) -> Option> { let (inc, slug) = match val.strip_prefix('!') { Some(val) => (false, val), None => (true, val), }; - match T::by_slug(slug, db) { + match T::by_slug(slug, db).await { Ok(item) => Some(Filter { inc, item }), Err(err) => { warn!("No filter {:?}: {:?}", slug, err); @@ -120,9 +121,9 @@ impl Filter { } impl SearchQuery { - fn load( + async fn load( query: Vec<(String, String)>, - db: &mut PgConnection, + db: &mut AsyncPgConnection, ) -> Result { let mut result = SearchQuery::default(); let (mut s_d, mut s_t, mut u_d, mut u_t) = (None, None, None, None); @@ -148,17 +149,17 @@ impl SearchQuery { result.q = val; } "t" => { - if let Some(f) = Filter::load(&val, db) { + if let Some(f) = Filter::load(&val, db).await { result.t.push(f); } } "p" => { - if let Some(f) = Filter::load(&val, db) { + if let Some(f) = Filter::load(&val, db).await { result.p.push(f); } } "l" => { - if let Some(f) = Filter::load(&val, db) { + if let Some(f) = Filter::load(&val, db).await { result.l.push(f); } } @@ -175,11 +176,13 @@ impl SearchQuery { } "from" => { result.since = - QueryDateTime::from_img(val.parse().req("from")?, db)?; + QueryDateTime::from_img(val.parse().req("from")?, db) + .await?; } "to" => { result.until = - QueryDateTime::from_img(val.parse().req("to")?, db)?; + QueryDateTime::from_img(val.parse().req("to")?, db) + .await?; } _ => (), // ignore unknown query parameters } @@ -222,12 +225,16 @@ impl QueryDateTime { NaiveTime::from_hms_milli_opt(23, 59, 59, 999).unwrap(); QueryDateTime::new(datetime_from_parts(date, time, until_midnight)) } - fn from_img(photo_id: i32, db: &mut PgConnection) -> Result { + async fn from_img( + photo_id: i32, + db: &mut AsyncPgConnection, + ) -> Result { Ok(QueryDateTime::new( p::photos .select(p::date) .filter(p::id.eq(photo_id)) - .first(db)?, + .first(db) + .await?, )) } fn as_ref(&self) -> Option<&NaiveDateTime> { diff --git a/src/server/splitlist.rs b/src/server/splitlist.rs index 0123fab..ea9eb19 100644 --- a/src/server/splitlist.rs +++ b/src/server/splitlist.rs @@ -3,40 +3,48 @@ use super::views_by_date::date_of_img; use super::{Context, ImgRange, PhotoLink, Result}; use crate::models::{Coord, Photo}; use crate::schema::photos; -use diesel::pg::{Pg, PgConnection}; +use chrono::NaiveDateTime; +use diesel::pg::Pg; use diesel::prelude::*; +use diesel_async::{AsyncPgConnection, RunQueryDsl}; use tracing::debug; -pub fn links_by_time( +pub async fn links_by_time( context: &Context, photos: photos::BoxedQuery<'_, Pg>, range: ImgRange, with_date: bool, ) -> Result<(Vec, Vec<(Coord, i32)>)> { - let mut c = context.db()?; + let mut c = context.db().await?; use crate::schema::photos::dsl::{date, id}; - let photos = - if let Some(from_date) = range.from.map(|i| date_of_img(&mut c, i)) { - photos.filter(date.ge(from_date)) - } else { - photos - }; - let photos = - if let Some(to_date) = range.to.map(|i| date_of_img(&mut c, i)) { - photos.filter(date.le(to_date)) - } else { - photos - }; + let photos = if let Some(fr) = date_of_opt_img(&mut c, range.from).await { + photos.filter(date.ge(fr)) + } else { + photos + }; + let photos = if let Some(to) = date_of_opt_img(&mut c, range.to).await { + photos.filter(date.le(to)) + } else { + photos + }; let photos = photos .order((date.desc().nulls_last(), id.desc())) - .load(&mut c)?; + .load(&mut c) + .await?; let baseurl = UrlString::new(context.path_without_query()); Ok(( split_to_group_links(&photos, &baseurl, with_date), - get_positions(&photos, &mut c)?, + get_positions(&photos, &mut c).await?, )) } +async fn date_of_opt_img( + db: &mut AsyncPgConnection, + img: Option, +) -> Option { + date_of_img(db, img?).await +} + pub fn split_to_group_links( photos: &[Photo], path: &UrlString, @@ -57,15 +65,16 @@ pub fn split_to_group_links( } } -pub fn get_positions( +pub async fn get_positions( photos: &[Photo], - c: &mut PgConnection, + c: &mut AsyncPgConnection, ) -> Result> { use crate::schema::positions::dsl::*; Ok(positions .filter(photo_id.eq_any(photos.iter().map(|p| p.id))) .select((photo_id, latitude, longitude)) - .load(c)? + .load(c) + .await? .into_iter() .map(|(p_id, lat, long): (i32, i32, i32)| ((lat, long).into(), p_id)) .collect()) @@ -92,7 +101,7 @@ fn find_largest(groups: &[&[Photo]]) -> usize { let mut found = 0; let mut largest = 0.0; for (i, g) in groups.iter().enumerate() { - let time = 1 + g.first().map(timestamp).unwrap_or(0) + let time = 1 + g.iter().next().map(timestamp).unwrap_or(0) - g.last().map(timestamp).unwrap_or(0); let score = (g.len() as f64).powi(3) * (time as f64); if score > largest { diff --git a/src/server/views_by_category.rs b/src/server/views_by_category.rs index a42d737..dab52b9 100644 --- a/src/server/views_by_category.rs +++ b/src/server/views_by_category.rs @@ -6,6 +6,7 @@ use super::{ use crate::models::{Person, Photo, Place, Tag}; use crate::templates; use diesel::prelude::*; +use diesel_async::RunQueryDsl; use warp::filters::method::get; use warp::filters::BoxedFilter; use warp::http::response::Builder; @@ -15,37 +16,37 @@ use warp::reply::Response; use warp::Filter; pub fn person_routes(s: ContextFilter) -> BoxedFilter<(Response,)> { - let all = end().and(get()).and(s.clone()).map(person_all); + let all = end().and(get()).and(s.clone()).then(person_all); let one = param() .and(end()) .and(get()) .and(query()) .and(s) - .map(person_one); + .then(person_one); all.or(one).unify().map(wrap).boxed() } pub fn place_routes(s: ContextFilter) -> BoxedFilter<(Response,)> { - let all = end().and(s.clone()).and(get()).map(place_all); + let all = end().and(s.clone()).and(get()).then(place_all); let one = param() .and(end()) .and(get()) .and(query()) .and(s) - .map(place_one); + .then(place_one); all.or(one).unify().map(wrap).boxed() } pub fn tag_routes(s: ContextFilter) -> BoxedFilter<(Response,)> { - let all = end().and(s.clone()).and(get()).map(tag_all); + let all = end().and(s.clone()).and(get()).then(tag_all); let one = param() .and(end()) .and(get()) .and(query()) .and(s) - .map(tag_one); + .then(tag_one); all.or(one).unify().map(wrap).boxed() } -fn person_all(context: Context) -> Result { +async fn person_all(context: Context) -> Result { use crate::schema::people::dsl::{id, people, person_name}; let query = people.into_boxed(); let query = if context.is_authorized() { @@ -57,20 +58,23 @@ fn person_all(context: Context) -> Result { pp::photo_id.eq_any(p::photos.select(p::id).filter(p::is_public)), ))) }; - let images = query.order(person_name).load(&mut context.db()?)?; + let images = query + .order(person_name) + .load(&mut context.db().await?) + .await?; Ok(Builder::new() .html(|o| templates::people_html(o, &context, &images))?) } -fn person_one( +async fn person_one( tslug: String, range: ImgRange, context: Context, ) -> Result { use crate::schema::people::dsl::{people, slug}; - let mut c = context.db()?; + let mut c = context.db().await?; let person = or_404q!( - people.filter(slug.eq(tslug)).first::(&mut c), + people.filter(slug.eq(tslug)).first::(&mut c).await, context ); use crate::schema::photo_people::dsl::{ @@ -84,13 +88,13 @@ fn person_one( .filter(person_id.eq(person.id)), ), ); - let (links, coords) = links_by_time(&context, photos, range, true)?; + let (links, coords) = links_by_time(&context, photos, range, true).await?; Ok(Builder::new().html(|o| { templates::person_html(o, &context, &links, &coords, &person) })?) } -fn tag_all(context: Context) -> Result { +async fn tag_all(context: Context) -> Result { use crate::schema::tags::dsl::{id, tag_name, tags}; let query = tags.order(tag_name).into_boxed(); let query = if context.is_authorized() { @@ -102,18 +106,20 @@ fn tag_all(context: Context) -> Result { tp::photo_id.eq_any(p::photos.select(p::id).filter(p::is_public)), ))) }; - let taggs = query.load(&mut context.db()?)?; + let taggs = query.load(&mut context.db().await?).await?; Ok(Builder::new().html(|o| templates::tags_html(o, &context, &taggs))?) } -fn tag_one( +async fn tag_one( tslug: String, range: ImgRange, context: Context, ) -> Result { use crate::schema::tags::dsl::{slug, tags}; let tag = or_404q!( - tags.filter(slug.eq(tslug)).first::(&mut context.db()?), + tags.filter(slug.eq(tslug)) + .first::(&mut context.db().await?) + .await, context ); @@ -122,12 +128,12 @@ fn tag_one( let photos = Photo::query(context.is_authorized()).filter( id.eq_any(photo_tags.select(photo_id).filter(tag_id.eq(tag.id))), ); - let (links, coords) = links_by_time(&context, photos, range, true)?; + let (links, coords) = links_by_time(&context, photos, range, true).await?; Ok(Builder::new() .html(|o| templates::tag_html(o, &context, &links, &coords, &tag))?) } -fn place_all(context: Context) -> Result { +async fn place_all(context: Context) -> Result { use crate::schema::places::dsl::{id, place_name, places}; let query = places.into_boxed(); let query = if context.is_authorized() { @@ -139,14 +145,17 @@ fn place_all(context: Context) -> Result { pp::photo_id.eq_any(p::photos.select(p::id).filter(p::is_public)), ))) }; - let found = query.order(place_name).load(&mut context.db()?)?; + let found = query + .order(place_name) + .load(&mut context.db().await?) + .await?; Ok( Builder::new() .html(|o| templates::places_html(o, &context, &found))?, ) } -fn place_one( +async fn place_one( tslug: String, range: ImgRange, context: Context, @@ -155,7 +164,8 @@ fn place_one( let place = or_404q!( places .filter(slug.eq(tslug)) - .first::(&mut context.db()?), + .first::(&mut context.db().await?) + .await, context ); @@ -164,7 +174,7 @@ fn place_one( let photos = Photo::query(context.is_authorized()).filter( id.eq_any(photo_places.select(photo_id).filter(place_id.eq(place.id))), ); - let (links, coord) = links_by_time(&context, photos, range, true)?; + let (links, coord) = links_by_time(&context, photos, range, true).await?; Ok(Builder::new().html(|o| { templates::place_html(o, &context, &links, &coord, &place) })?) diff --git a/src/server/views_by_date.rs b/src/server/views_by_date.rs index 9edbf74..6fac688 100644 --- a/src/server/views_by_date.rs +++ b/src/server/views_by_date.rs @@ -10,6 +10,7 @@ use chrono::{Datelike, Duration, Local}; use diesel::dsl::count_star; use diesel::prelude::*; use diesel::sql_types::{Bool, Nullable, Timestamp}; +use diesel_async::{AsyncPgConnection, RunQueryDsl}; use serde::Deserialize; use warp::filters::BoxedFilter; use warp::http::response::Builder; @@ -20,15 +21,15 @@ use warp::{get, path, Filter}; pub fn routes(s: ContextFilter) -> BoxedFilter<(Response,)> { let s = move || s.clone(); - let root = end().and(get()).and(s()).map(all_years); - let nodate = path("0").and(end()).and(get()).and(s()).map(all_null_date); - let year = param().and(end()).and(get()).and(s()).map(months_in_year); + let root = end().and(get()).and(s()).then(all_years); + let nodate = path("0").and(end()).and(get()).and(s()).then(all_null_date); + let year = param().and(end()).and(get()).and(s()).then(months_in_year); let month = param() .and(param()) .and(end()) .and(get()) .and(s()) - .map(days_in_month); + .then(days_in_month); let day = param() .and(param()) .and(param()) @@ -36,25 +37,25 @@ pub fn routes(s: ContextFilter) -> BoxedFilter<(Response,)> { .and(query()) .and(get()) .and(s()) - .map(all_for_day); + .then(all_for_day); let this = path("thisday") .and(end()) .and(get()) .and(s()) - .map(on_this_day); + .then(on_this_day); let next = path("next") .and(end()) .and(get()) .and(s()) .and(query()) - .map(next_image); + .then(next_image); let prev = path("prev") .and(end()) .and(get()) .and(s()) .and(query()) - .map(prev_image); + .then(prev_image); root.or(nodate) .unify() @@ -100,56 +101,56 @@ mod filter { fn day_of_timestamp(date: Nullable) -> Nullable } } -fn all_years(context: Context) -> Result { +async fn all_years(context: Context) -> Result { use crate::schema::photos::dsl as p; - let mut db = context.db()?; + let mut db = context.db().await?; let y = year_of_timestamp(p::date); - let groups = p::photos + let groups_in = p::photos .filter(p::path.not_like("%.CR2")) .filter(p::path.not_like("%.dng")) .filter(p::is_public.or::<_, Bool>(context.is_authorized())) .select((y, count_star())) .group_by(y) .order(y.desc().nulls_last()) - .load::<(Option, i64)>(&mut db)? - .iter() - .map(|&(year, count)| { - let year: Option = year.map(|y| y as i32); - let q = Photo::query(context.is_authorized()) - .order((p::grade.desc().nulls_last(), p::date.asc())) - .limit(1); - let photo = if let Some(year) = year { - q.filter(p::date.ge(start_of_year(year))) - .filter(p::date.lt(start_of_year(year + 1))) - } else { - q.filter(p::date.is_null()) - }; - let photo = photo.first::(&mut db)?; - Ok(PhotoLink { - title: Some( - year.map(|y| format!("{y}")) - .unwrap_or_else(|| "-".to_string()), - ), - href: format!("/{}/", year.unwrap_or(0)), - lable: Some(format!("{count} images")), - id: photo.id, - size: photo.get_size(SizeTag::Small), - }) - }) - .collect::>>()?; + .load::<(Option, i64)>(&mut db) + .await?; + let mut groups = Vec::with_capacity(groups_in.len()); + for (year, count) in groups_in { + let year: Option = year.map(Into::into); + let q = Photo::query(context.is_authorized()) + .order((p::grade.desc().nulls_last(), p::date.asc())) + .limit(1); + let photo = if let Some(year) = year { + q.filter(p::date.ge(start_of_year(year))) + .filter(p::date.lt(start_of_year(year + 1))) + } else { + q.filter(p::date.is_null()) + }; + let photo = photo.first::(&mut db).await?; + groups.push(PhotoLink { + title: Some( + year.map(|y| format!("{y}")) + .unwrap_or_else(|| "-".to_string()), + ), + href: format!("/{}/", year.unwrap_or(0)), + lable: Some(format!("{count} images")), + id: photo.id, + size: photo.get_size(SizeTag::Small), + }); + } Ok(Builder::new().html(|o| { templates::index_html(o, &context, "All photos", &[], &groups, &[]) })?) } -fn months_in_year(year: i32, context: Context) -> Result { +async fn months_in_year(year: i32, context: Context) -> Result { use crate::schema::photos::dsl as p; let title: String = format!("Photos from {year}"); - let mut db = context.db()?; + let mut db = context.db().await?; let m = month_of_timestamp(p::date); - let groups = p::photos + let groups_in = p::photos .filter(p::path.not_like("%.CR2")) .filter(p::path.not_like("%.dng")) .filter(p::is_public.or::<_, Bool>(context.is_authorized())) @@ -158,48 +159,47 @@ fn months_in_year(year: i32, context: Context) -> Result { .select((m, count_star())) .group_by(m) .order(m.desc().nulls_last()) - .load::<(Option, i64)>(&mut db)? - .iter() - .map(|&(month, count)| { - let month = month.unwrap() as u32; // cant be null when in range! - let photo = Photo::query(context.is_authorized()) - .filter(p::date.ge(start_of_month(year, month))) - .filter(p::date.lt(start_of_month(year, month + 1))) - .order((p::grade.desc().nulls_last(), p::date.asc())) - .limit(1) - .first::(&mut db)?; - - Ok(PhotoLink { - title: Some(monthname(month).to_string()), - href: format!("/{year}/{month}/"), - lable: Some(format!("{count} pictures")), - id: photo.id, - size: photo.get_size(SizeTag::Small), - }) - }) - .collect::>>()?; - - if groups.is_empty() { - Err(ViewError::NotFound(Some(context))) - } else { - use crate::schema::positions::dsl::{ - latitude, longitude, photo_id, positions, - }; - let pos = Photo::query(context.is_authorized()) - .inner_join(positions) - .filter(p::date.ge(start_of_year(year))) - .filter(p::date.lt(start_of_year(year + 1))) - .select((photo_id, latitude, longitude)) - .load(&mut db)? - .into_iter() - .map(|(p_id, lat, long): (i32, i32, i32)| { - ((lat, long).into(), p_id) - }) - .collect::>(); - Ok(Builder::new().html(|o| { - templates::index_html(o, &context, &title, &[], &groups, &pos) - })?) + .load::<(Option, i64)>(&mut db) + .await?; + if groups_in.is_empty() { + return Err(ViewError::NotFound(Some(context))); } + let mut groups = Vec::with_capacity(groups_in.len()); + for (month, count) in groups_in { + let month = month.unwrap() as u32; // cant be null when in range! + let photo = Photo::query(context.is_authorized()) + .filter(p::date.ge(start_of_month(year, month))) + .filter(p::date.lt(start_of_month(year, month + 1))) + .order((p::grade.desc().nulls_last(), p::date.asc())) + .limit(1) + .first::(&mut db) + .await?; + + groups.push(PhotoLink { + title: Some(monthname(month).to_string()), + href: format!("/{year}/{month}/"), + lable: Some(format!("{count} pictures")), + id: photo.id, + size: photo.get_size(SizeTag::Small), + }) + } + + use crate::schema::positions::dsl::{ + latitude, longitude, photo_id, positions, + }; + let pos = Photo::query(context.is_authorized()) + .inner_join(positions) + .filter(p::date.ge(start_of_year(year))) + .filter(p::date.lt(start_of_year(year + 1))) + .select((photo_id, latitude, longitude)) + .load(&mut db) + .await? + .into_iter() + .map(|(p_id, lat, long): (i32, i32, i32)| ((lat, long).into(), p_id)) + .collect::>(); + Ok(Builder::new().html(|o| { + templates::index_html(o, &context, &title, &[], &groups, &pos) + })?) } fn start_of_year(year: i32) -> NaiveDateTime { @@ -221,14 +221,18 @@ fn start_of_day(year: i32, month: u32, day: u32) -> NaiveDateTime { .unwrap() } -fn days_in_month(year: i32, month: u32, context: Context) -> Result { +async fn days_in_month( + year: i32, + month: u32, + context: Context, +) -> Result { use crate::schema::photos::dsl as p; let d = day_of_timestamp(p::date); let lpath: Vec = vec![Link::year(year)]; let title: String = format!("Photos from {} {}", monthname(month), year); - let mut db = context.db()?; - let groups = p::photos + let mut db = context.db().await?; + let groups_in = p::photos .filter(p::path.not_like("%.CR2")) .filter(p::path.not_like("%.dng")) .filter(p::is_public.or::<_, Bool>(context.is_authorized())) @@ -237,56 +241,56 @@ fn days_in_month(year: i32, month: u32, context: Context) -> Result { .select((d, count_star())) .group_by(d) .order(d.desc().nulls_last()) - .load::<(Option, i64)>(&mut db)? - .iter() - .map(|&(day, count)| { - let day = day.unwrap() as u32; - let fromdate = start_of_day(year, month, day); - let photo = Photo::query(context.is_authorized()) - .filter(p::date.ge(fromdate)) - .filter(p::date.lt(fromdate + Duration::days(1))) - .order((p::grade.desc().nulls_last(), p::date.asc())) - .limit(1) - .first::(&mut db)?; - - Ok(PhotoLink { - title: Some(format!("{day}")), - href: format!("/{year}/{month}/{day}"), - lable: Some(format!("{count} pictures")), - id: photo.id, - size: photo.get_size(SizeTag::Small), - }) - }) - .collect::>>()?; - - if groups.is_empty() { - Err(ViewError::NotFound(Some(context))) - } else { - use crate::schema::positions::dsl as ps; - let pos = Photo::query(context.is_authorized()) - .inner_join(ps::positions) - .filter(p::date.ge(start_of_month(year, month))) - .filter(p::date.lt(start_of_month(year, month + 1))) - .select((ps::photo_id, ps::latitude, ps::longitude)) - .load(&mut db)? - .into_iter() - .map(|(p_id, lat, long): (i32, i32, i32)| { - ((lat, long).into(), p_id) - }) - .collect::>(); - Ok(Builder::new().html(|o| { - templates::index_html(o, &context, &title, &lpath, &groups, &pos) - })?) + .load::<(Option, i64)>(&mut db) + .await?; + if groups_in.is_empty() { + return Err(ViewError::NotFound(Some(context))); } + let mut groups = Vec::with_capacity(groups_in.len()); + for (day, count) in groups_in { + let day = day.unwrap() as u32; + let fromdate = start_of_day(year, month, day); + let photo = Photo::query(context.is_authorized()) + .filter(p::date.ge(fromdate)) + .filter(p::date.lt(fromdate + Duration::days(1))) + .order((p::grade.desc().nulls_last(), p::date.asc())) + .limit(1) + .first::(&mut db) + .await?; + + groups.push(PhotoLink { + title: Some(format!("{day}")), + href: format!("/{year}/{month}/{day}"), + lable: Some(format!("{count} pictures")), + id: photo.id, + size: photo.get_size(SizeTag::Small), + }) + } + + use crate::schema::positions::dsl as ps; + let pos = Photo::query(context.is_authorized()) + .inner_join(ps::positions) + .filter(p::date.ge(start_of_month(year, month))) + .filter(p::date.lt(start_of_month(year, month + 1))) + .select((ps::photo_id, ps::latitude, ps::longitude)) + .load(&mut db) + .await? + .into_iter() + .map(|(p_id, lat, long): (i32, i32, i32)| ((lat, long).into(), p_id)) + .collect::>(); + Ok(Builder::new().html(|o| { + templates::index_html(o, &context, &title, &lpath, &groups, &pos) + })?) } -fn all_null_date(context: Context) -> Result { +async fn all_null_date(context: Context) -> Result { use crate::schema::photos::dsl as p; let images = Photo::query(context.is_authorized()) .filter(p::date.is_null()) .order(p::path.asc()) .limit(500) - .load(&mut context.db()?)? + .load(&mut context.db().await?) + .await? .iter() .map(PhotoLink::no_title) .collect::>(); @@ -302,7 +306,7 @@ fn all_null_date(context: Context) -> Result { })?) } -fn all_for_day( +async fn all_for_day( year: i32, month: u32, day: u32, @@ -315,7 +319,8 @@ fn all_for_day( let photos = Photo::query(context.is_authorized()) .filter(p::date.ge(thedate)) .filter(p::date.lt(thedate + Duration::days(1))); - let (links, coords) = links_by_time(&context, photos, range, false)?; + let (links, coords) = + links_by_time(&context, photos, range, false).await?; if links.is_empty() { Err(ViewError::NotFound(Some(context))) @@ -333,7 +338,7 @@ fn all_for_day( } } -fn on_this_day(context: Context) -> Result { +async fn on_this_day(context: Context) -> Result { use crate::schema::photos::dsl as p; use crate::schema::positions::dsl as ps; @@ -341,19 +346,20 @@ fn on_this_day(context: Context) -> Result { let today = Local::now(); (today.month(), today.day()) }; - let mut db = context.db()?; + let mut db = context.db().await?; let pos = Photo::query(context.is_authorized()) .inner_join(ps::positions) .filter(filter::month_of_timestamp(p::date).eq(month as i16)) .filter(filter::day_of_timestamp(p::date).eq(day as i16)) .select((ps::photo_id, ps::latitude, ps::longitude)) - .load(&mut db)? + .load(&mut db) + .await? .into_iter() .map(|(p_id, lat, long): (i32, i32, i32)| ((lat, long).into(), p_id)) .collect::>(); let y = year_of_timestamp(p::date); - let photos = p::photos + let photos_in = p::photos .filter(p::path.not_like("%.CR2")) .filter(p::path.not_like("%.dng")) .filter(p::is_public.or::<_, Bool>(context.is_authorized())) @@ -362,26 +368,27 @@ fn on_this_day(context: Context) -> Result { .select((y, count_star())) .group_by(y) .order(y.desc()) - .load::<(Option, i64)>(&mut db)? - .iter() - .map(|&(year, count)| { - let year = year.unwrap(); // matching date can't be null - let fromdate = start_of_day(year.into(), month, day); - let photo = Photo::query(context.is_authorized()) - .filter(p::date.ge(fromdate)) - .filter(p::date.lt(fromdate + Duration::days(1))) - .order((p::grade.desc().nulls_last(), p::date.asc())) - .limit(1) - .first::(&mut db)?; - Ok(PhotoLink { - title: Some(format!("{year}")), - href: format!("/{year}/{month}/{day}"), - lable: Some(format!("{count} pictures")), - id: photo.id, - size: photo.get_size(SizeTag::Small), - }) + .load::<(Option, i64)>(&mut db) + .await?; + let mut photos = Vec::with_capacity(photos_in.len()); + for (year, count) in photos_in { + let year = year.unwrap(); // matching date can't be null + let fromdate = start_of_day(year.into(), month, day); + let photo = Photo::query(context.is_authorized()) + .filter(p::date.ge(fromdate)) + .filter(p::date.lt(fromdate + Duration::days(1))) + .order((p::grade.desc().nulls_last(), p::date.asc())) + .limit(1) + .first::(&mut db) + .await?; + photos.push(PhotoLink { + title: Some(format!("{year}")), + href: format!("/{year}/{month}/{day}"), + lable: Some(format!("{count} pictures")), + id: photo.id, + size: photo.get_size(SizeTag::Small), }) - .collect::>>()?; + } Ok(Builder::new().html(|o| { templates::index_html( o, @@ -394,10 +401,10 @@ fn on_this_day(context: Context) -> Result { })?) } -fn next_image(context: Context, param: FromParam) -> Result { +async fn next_image(context: Context, param: FromParam) -> Result { use crate::schema::photos::dsl as p; - let mut db = context.db()?; - let from_date = or_404!(date_of_img(&mut db, param.from), context); + let mut db = context.db().await?; + let from_date = or_404!(date_of_img(&mut db, param.from).await, context); let photo = or_404q!( Photo::query(context.is_authorized()) .select(p::id) @@ -407,16 +414,17 @@ fn next_image(context: Context, param: FromParam) -> Result { .or(p::date.eq(from_date).and(p::id.gt(param.from))), ) .order((p::date, p::id)) - .first::(&mut db), + .first::(&mut db) + .await, context ); Ok(redirect_to_img(photo)) } -fn prev_image(context: Context, param: FromParam) -> Result { +async fn prev_image(context: Context, param: FromParam) -> Result { use crate::schema::photos::dsl as p; - let mut db = context.db()?; - let from_date = or_404!(date_of_img(&mut db, param.from), context); + let mut db = context.db().await?; + let from_date = or_404!(date_of_img(&mut db, param.from).await, context); let photo = or_404q!( Photo::query(context.is_authorized()) .select(p::id) @@ -426,7 +434,8 @@ fn prev_image(context: Context, param: FromParam) -> Result { .or(p::date.eq(from_date).and(p::id.lt(param.from))), ) .order((p::date.desc().nulls_last(), p::id.desc())) - .first::(&mut db), + .first::(&mut db) + .await, context ); Ok(redirect_to_img(photo)) @@ -437,8 +446,8 @@ struct FromParam { from: i32, } -pub fn date_of_img( - db: &mut PgConnection, +pub async fn date_of_img( + db: &mut AsyncPgConnection, photo_id: i32, ) -> Option { use crate::schema::photos::dsl as p; @@ -446,6 +455,7 @@ pub fn date_of_img( .find(photo_id) .select(p::date) .first(db) + .await .unwrap_or(None) }