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:
parent
3823e9a337
commit
b586c3df6c
@ -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.
|
||||
|
@ -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]
|
||||
|
@ -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)
|
||||
|
@ -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(())
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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<_>>(),
|
||||
);
|
||||
|
||||
|
@ -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 => {
|
||||
|
27
src/dbopt.rs
27
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<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)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
12
src/main.rs
12
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,
|
||||
|
124
src/models.rs
124
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<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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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(()) => (),
|
||||
|
@ -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))
|
||||
}
|
||||
|
||||
|
@ -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?)
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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.
|
||||
///
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
@ -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| {
|
||||
|
@ -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> {
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
})?)
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user