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.
This commit is contained in:
Rasmus Kaj 2023-02-11 17:14:10 +01:00
parent 3823e9a337
commit b586c3df6c
24 changed files with 660 additions and 507 deletions

View File

@ -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.

View File

@ -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]

View File

@ -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<Option<Camera>, 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)

View File

@ -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::<Photo>(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<In: BufRead + Sized>(
db: &mut PgConnection,
async fn by_file_list<In: BufRead + Sized>(
db: &mut AsyncPgConnection,
list: In,
) -> Result<(), Error> {
for line in list.lines() {
one(db, &line?)?;
one(db, &line?).await?;
}
Ok(())
}

View File

@ -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::<Photo>(&mut self.db.connect()?)?;
.load::<Photo>(&mut self.db.connect().await?)
.await?;
let no_expire = 0;
let pd = PhotosDir::new(&self.photos.photos_dir);
for photo in photos {

View File

@ -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<Timestamp>) -> Nullable<SmallInt>
}
pub async fn show_stats(db: &mut AsyncPgConnection) -> Result<(), Error> {
println!(
"There are {} photos in total.",
photos.select(count_star()).first::<i64>(db)?,
photos.select(count_star()).first::<i64>(db).await?,
);
println!(
"There are {} persons, {} places, and {} tags mentioned.",
people.select(count_star()).first::<i64>(db)?,
places.select(count_star()).first::<i64>(db)?,
tags.select(count_star()).first::<i64>(db)?,
people.select(count_star()).first::<i64>(db).await?,
places.select(count_star()).first::<i64>(db).await?,
tags.select(count_star()).first::<i64>(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<f64>, i64)>(db));
let y = year_of_timestamp(p::date);
println!(
"Count per year: {:?}",
photos
.select(sql::<(Nullable<Double>, BigInt)>(
"extract(year from date) y, count(*)"
))
.group_by(sql::<Nullable<Double>>("y"))
.order(sql::<Nullable<Double>>("y").desc().nulls_last())
.load::<(Option<f64>, i64)>(db)?
.select((y, count_star()))
.group_by(y)
.order(y.desc().nulls_last())
.load::<(Option<i16>, i64)>(db)
.await?
.iter()
.map(|&(y, n)| format!("{}: {}", y.unwrap_or(0.0), n))
.map(|&(y, n)| format!("{}: {}", y.unwrap_or(0), n))
.collect::<Vec<_>>(),
);

View File

@ -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::<String>(db)?,
u::users.select(u::username).load::<String>(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 => {

View File

@ -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<ConnectionManager<PgConnection>>;
pub type PooledPg = PooledConnection<ConnectionManager<PgConnection>>;
/// An asynchronous postgres database connection pool.
pub type PgPool = deadpool::Pool<AsyncPgConnection>;
pub type PooledPg = deadpool::Object<AsyncPgConnection>;
#[derive(clap::Parser)]
pub struct DbOpt {
@ -16,19 +18,18 @@ pub struct DbOpt {
}
impl DbOpt {
pub fn connect(&self) -> Result<PgConnection, ConnectionError> {
pub async fn connect(&self) -> Result<AsyncPgConnection, ConnectionError> {
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<PgPool, Error> {
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)
}

View File

@ -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::<Coord>(
&mut db
.get()
.map_err(|e| Error::Pool(image, e.to_string()))?,
)
.first::<Coord>(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::<PhotoPlace>(&mut c).is_ok() {
if q.first::<PhotoPlace>(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<Place, diesel::result::Error> {
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::<Place>(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::<Place>(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::<Place>(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::<Place>(c);
}
result
})
.get_result::<Place>(c)
.await;
}
result
}
}
fn is_duplicate<T>(r: &Result<T, diesel::result::Error>) -> bool {

View File

@ -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,

View File

@ -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<Camera>,
}
impl PhotoDetails {
pub fn load(id: i32, db: &mut PgConnection) -> Result<Self, Error> {
pub async fn load(
id: i32,
db: &mut AsyncPgConnection,
) -> Result<Self, Error> {
use crate::schema::photos::dsl::photos;
let photo = photos.find(id).first::<Photo>(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::<Photo>(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::<Photo>(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::<Photo>(db)?;
.get_result::<Photo>(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::<Photo>(db)?;
.get_result::<Photo>(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::<Photo>(db)?;
.get_result::<Photo>(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<Modification<Photo>, 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::<Photo>(db)?;
.get_result::<Photo>(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<Self, Error>
async fn by_slug(
slug: &str,
db: &mut AsyncPgConnection,
) -> Result<Self, Error>
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<Tag, Error> {
t::tags.filter(t::slug.eq(slug)).first(db)
async fn by_slug(
slug: &str,
db: &mut AsyncPgConnection,
) -> Result<Tag, Error> {
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<Person, Error> {
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<Person, Error> {
h::people.filter(h::slug.eq(slug)).first(db)
async fn by_slug(
slug: &str,
db: &mut AsyncPgConnection,
) -> Result<Person, Error> {
h::people.filter(h::slug.eq(slug)).first(db).await
}
}
@ -311,9 +343,13 @@ pub struct Place {
pub osm_level: Option<i16>,
}
#[async_trait]
impl Facet for Place {
fn by_slug(slug: &str, db: &mut PgConnection) -> Result<Place, Error> {
l::places.filter(l::slug.eq(slug)).first(db)
async fn by_slug(
slug: &str,
db: &mut AsyncPgConnection,
) -> Result<Place, Error> {
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<Camera, Error> {
@ -341,6 +377,7 @@ impl Camera {
.filter(c::manufacturer.eq(make))
.filter(c::model.eq(modl))
.first::<Camera>(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
}
}
}

View File

@ -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<ExifData> {
pub fn load_meta(path: &Path) -> Option<ExifData> {
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) {

View File

@ -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<Response> {
async fn rotate(context: Context, form: RotateForm) -> Result<Response> {
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::<Photo>(c), context);
or_404q!(photos.find(form.image).first::<Photo>(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::<Photo>(c)?;
let image = image.save_changes::<Photo>(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<Response> {
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::<Tag>(&mut c)
.or_else(|_| {
diesel::insert_into(tags)
.values((
tag_name.eq(&form.tag),
slug.eq(&slugify(&form.tag)),
))
.get_result::<Tag>(&mut c)
})?
.await
.optional()?
{
tag
} else {
diesel::insert_into(tags)
.values((tag_name.eq(&form.tag), slug.eq(&slugify(&form.tag))))
.get_result::<Tag>(&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::<i64>(&mut c)? > 0 {
if q.get_result::<i64>(&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<Response> {
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::<PhotoPerson>(&mut c).optional()?.is_some() {
if q.first::<PhotoPerson>(&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<Response> {
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<Response> {
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(()) => (),

View File

@ -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<T: Serialize>(result: ApiResult<T>) -> Response {
}
}
fn login(context: Context, form: LoginForm) -> ApiResult<LoginOk> {
let mut db = context.db()?;
async fn login(context: Context, form: LoginForm) -> ApiResult<LoginOk> {
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<Option<Photo>, DbError> {
async fn get(
&self,
db: &mut AsyncPgConnection,
) -> Result<Option<Photo>, 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<GetImgResult> {
async fn get_img(context: Context, q: ImgQuery) -> ApiResult<GetImgResult> {
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<GetImgResult> {
async fn make_public(
context: Context,
q: ImgQuery,
) -> ApiResult<GetImgResult> {
if !context.is_authorized() {
return Err(ApiError {
code: StatusCode::UNAUTHORIZED,
@ -130,12 +138,13 @@ fn make_public(context: Context, q: ImgQuery) -> ApiResult<GetImgResult> {
});
}
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))
}

View File

@ -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<Json> {
let mut tags = select_tags(&context, &term)?
async fn list_any(context: Context, term: AcQ) -> Result<Json> {
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<Json> {
Ok(json(&tags.iter().map(|(t, _)| t).collect::<Vec<_>>()))
}
fn list_tags(context: Context, query: AcQ) -> Result<Json> {
Ok(json(&names(select_tags(&context, &query)?)))
async fn list_tags(context: Context, query: AcQ) -> Result<Json> {
Ok(json(&names(select_tags(&context, &query).await?)))
}
fn list_people(context: Context, query: AcQ) -> Result<Json> {
Ok(json(&names(select_people(&context, &query)?)))
async fn list_people(context: Context, query: AcQ) -> Result<Json> {
Ok(json(&names(select_people(&context, &query).await?)))
}
fn names(data: Vec<NameSlugScore>) -> Vec<String> {
@ -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<Vec<NameSlugScore>> {
async fn select_tags(
context: &Context,
term: &AcQ,
) -> Result<Vec<NameSlugScore>> {
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<Vec<NameSlugScore>> {
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<Vec<NameSlugScore>> {
async fn select_people(
context: &Context,
term: &AcQ,
) -> Result<Vec<NameSlugScore>> {
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<Vec<NameSlugScore>> {
),
)
};
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<Vec<NameSlugScore>> {
async fn select_places(
context: &Context,
term: &AcQ,
) -> Result<Vec<NameSlugScore>> {
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<Vec<NameSlugScore>> {
),
)
};
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?)
}

View File

@ -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<MemcacheConnectionManager>;
type PooledMemcache = PooledConnection<MemcacheConnectionManager>;
type MemcachePool = r2d2::Pool<MemcacheConnectionManager>;
type PooledMemcache = r2d2::PooledConnection<MemcacheConnectionManager>;
pub fn create_session_filter(args: &Args) -> Result<ContextFilter, Error> {
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<PooledPg> {
Ok(self.global.db_pool.get()?)
pub async fn db(&self) -> Result<PooledPg> {
Ok(self.global.db_pool.get().await?)
}
pub fn db_pool(&self) -> PgPool {
self.global.db_pool.clone()

View File

@ -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<diesel::result::Error> for ViewError {
impl From<r2d2_memcache::memcache::Error> 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<ImageLoadFailed> for ViewError {
@ -138,6 +139,12 @@ impl From<ImageLoadFailed> for ViewError {
ViewError::Err("Failed to load image")
}
}
impl From<PoolError> for ViewError {
fn from(value: PoolError) -> Self {
error!("Database pool error: {value}");
ViewError::ServiceUnavailable
}
}
/// Create custom errors for warp rejections.
///

View File

@ -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<Response> {
use crate::schema::photos::dsl::photos;
let tphoto = photos.find(img.id).first::<Photo>(&mut context.db()?);
let tphoto = photos
.find(img.id)
.first::<Photo>(&mut context.db().await?)
.await;
if let Ok(tphoto) = tphoto {
if context.is_authorized() || tphoto.is_public() {
if img.size == SizeTag::Large {

View File

@ -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<String>,
}
fn post_login(context: Context, form: LoginForm) -> Result<Response> {
async fn post_login(context: Context, form: LoginForm) -> Result<Response> {
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<String> {
pub async fn validate(
&self,
db: &mut AsyncPgConnection,
) -> Option<String> {
use crate::schema::users::dsl::*;
if let Ok(hash) = users
.filter(username.eq(&self.user))
.select(password)
.first::<String>(db)
.await
{
if djangohashers::check_password_tolerant(&self.password, &hash) {
info!("User {} logged in", self.user);

View File

@ -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<Response> {
.ise()
}
fn random_image(context: Context) -> Result<Response> {
async fn random_image(context: Context) -> Result<Response> {
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::<Integer>("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<Response> {
let mut c = context.db()?;
let photo = or_404q!(PhotoDetails::load(id, &mut c), context);
async fn photo_details(id: i32, context: Context) -> Result<Response> {
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| {

View File

@ -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<Response> {
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<T> {
}
impl<T: Facet> Filter<T> {
fn load(val: &str, db: &mut PgConnection) -> Option<Filter<T>> {
async fn load(val: &str, db: &mut AsyncPgConnection) -> Option<Filter<T>> {
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<T: Facet> Filter<T> {
}
impl SearchQuery {
fn load(
async fn load(
query: Vec<(String, String)>,
db: &mut PgConnection,
db: &mut AsyncPgConnection,
) -> Result<Self> {
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<Self> {
async fn from_img(
photo_id: i32,
db: &mut AsyncPgConnection,
) -> Result<Self> {
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> {

View File

@ -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<PhotoLink>, 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<i32>,
) -> Option<NaiveDateTime> {
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<Vec<(Coord, i32)>> {
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 {

View File

@ -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<Response> {
async fn person_all(context: Context) -> Result<Response> {
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<Response> {
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<Response> {
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::<Person>(&mut c),
people.filter(slug.eq(tslug)).first::<Person>(&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<Response> {
async fn tag_all(context: Context) -> Result<Response> {
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<Response> {
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<Response> {
use crate::schema::tags::dsl::{slug, tags};
let tag = or_404q!(
tags.filter(slug.eq(tslug)).first::<Tag>(&mut context.db()?),
tags.filter(slug.eq(tslug))
.first::<Tag>(&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<Response> {
async fn place_all(context: Context) -> Result<Response> {
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<Response> {
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::<Place>(&mut context.db()?),
.first::<Place>(&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)
})?)

View File

@ -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<Timestamp>) -> Nullable<SmallInt>
}
}
fn all_years(context: Context) -> Result<Response> {
async fn all_years(context: Context) -> Result<Response> {
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<i16>, i64)>(&mut db)?
.iter()
.map(|&(year, count)| {
let year: Option<i32> = 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::<Photo>(&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::<Result<Vec<_>>>()?;
.load::<(Option<i16>, i64)>(&mut db)
.await?;
let mut groups = Vec::with_capacity(groups_in.len());
for (year, count) in groups_in {
let year: Option<i32> = 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::<Photo>(&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<Response> {
async fn months_in_year(year: i32, context: Context) -> Result<Response> {
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<Response> {
.select((m, count_star()))
.group_by(m)
.order(m.desc().nulls_last())
.load::<(Option<i16>, 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::<Photo>(&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::<Result<Vec<_>>>()?;
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::<Vec<_>>();
Ok(Builder::new().html(|o| {
templates::index_html(o, &context, &title, &[], &groups, &pos)
})?)
.load::<(Option<i16>, 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::<Photo>(&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::<Vec<_>>();
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<Response> {
async fn days_in_month(
year: i32,
month: u32,
context: Context,
) -> Result<Response> {
use crate::schema::photos::dsl as p;
let d = day_of_timestamp(p::date);
let lpath: Vec<Link> = 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<Response> {
.select((d, count_star()))
.group_by(d)
.order(d.desc().nulls_last())
.load::<(Option<i16>, 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::<Photo>(&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::<Result<Vec<_>>>()?;
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::<Vec<_>>();
Ok(Builder::new().html(|o| {
templates::index_html(o, &context, &title, &lpath, &groups, &pos)
})?)
.load::<(Option<i16>, 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::<Photo>(&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::<Vec<_>>();
Ok(Builder::new().html(|o| {
templates::index_html(o, &context, &title, &lpath, &groups, &pos)
})?)
}
fn all_null_date(context: Context) -> Result<Response> {
async fn all_null_date(context: Context) -> Result<Response> {
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::<Vec<_>>();
@ -302,7 +306,7 @@ fn all_null_date(context: Context) -> Result<Response> {
})?)
}
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<Response> {
async fn on_this_day(context: Context) -> Result<Response> {
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<Response> {
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::<Vec<_>>();
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<Response> {
.select((y, count_star()))
.group_by(y)
.order(y.desc())
.load::<(Option<i16>, 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::<Photo>(&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<i16>, 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::<Photo>(&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::<Result<Vec<_>>>()?;
}
Ok(Builder::new().html(|o| {
templates::index_html(
o,
@ -394,10 +401,10 @@ fn on_this_day(context: Context) -> Result<Response> {
})?)
}
fn next_image(context: Context, param: FromParam) -> Result<Response> {
async fn next_image(context: Context, param: FromParam) -> Result<Response> {
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<Response> {
.or(p::date.eq(from_date).and(p::id.gt(param.from))),
)
.order((p::date, p::id))
.first::<i32>(&mut db),
.first::<i32>(&mut db)
.await,
context
);
Ok(redirect_to_img(photo))
}
fn prev_image(context: Context, param: FromParam) -> Result<Response> {
async fn prev_image(context: Context, param: FromParam) -> Result<Response> {
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<Response> {
.or(p::date.eq(from_date).and(p::id.lt(param.from))),
)
.order((p::date.desc().nulls_last(), p::id.desc()))
.first::<i32>(&mut db),
.first::<i32>(&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<NaiveDateTime> {
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)
}