Refactor: Move some code

Just move some code around, make the longest file in the source
tree a bit shorter.
This commit is contained in:
Rasmus Kaj 2019-07-02 11:37:47 +02:00
parent 088dd74e80
commit 5df25e02de
10 changed files with 551 additions and 523 deletions

View File

@ -1,8 +1,7 @@
use super::result::Error; use super::result::Error;
use crate::models::Photo; use crate::models::{Photo, SizeTag};
use crate::photosdir::PhotosDir; use crate::photosdir::PhotosDir;
use crate::schema::photos::dsl::{date, is_public}; use crate::schema::photos::dsl::{date, is_public};
use crate::server::SizeTag;
use crate::{CacheOpt, DbOpt, DirOpt}; use crate::{CacheOpt, DbOpt, DirOpt};
use diesel::prelude::*; use diesel::prelude::*;
use log::{debug, info}; use log::{debug, info};

View File

@ -1,4 +1,3 @@
use crate::server::SizeTag;
use chrono::naive::NaiveDateTime; use chrono::naive::NaiveDateTime;
use diesel; use diesel;
use diesel::pg::PgConnection; use diesel::pg::PgConnection;
@ -345,3 +344,20 @@ impl From<(i32, i32)> for Coord {
} }
} }
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SizeTag {
Small,
Medium,
Large,
}
impl SizeTag {
pub fn px(self) -> u32 {
match self {
SizeTag::Small => 240,
SizeTag::Medium => 960,
SizeTag::Large => 1900,
}
}
}

View File

@ -1,7 +1,7 @@
//! Admin-only views, generally called by javascript. //! Admin-only views, generally called by javascript.
use super::{not_found, permission_denied, redirect_to_img, Context, SizeTag}; use super::{not_found, permission_denied, redirect_to_img, Context};
use crate::fetch_places::update_image_places; use crate::fetch_places::update_image_places;
use crate::models::{Coord, Photo}; use crate::models::{Coord, Photo, SizeTag};
use diesel::{self, prelude::*}; use diesel::{self, prelude::*};
use log::{info, warn}; use log::{info, warn};
use serde::Deserialize; use serde::Deserialize;

111
src/server/image.rs Normal file
View File

@ -0,0 +1,111 @@
use super::render_ructe::RenderRucte;
use super::{error_response, not_found, Context};
use crate::models::{Photo, SizeTag};
use diesel::prelude::*;
use std::str::FromStr;
use warp::http::{header, Response, StatusCode};
pub fn show_image(img: ImgName, context: Context) -> Response<Vec<u8>> {
use crate::schema::photos::dsl::photos;
if let Ok(tphoto) = photos.find(img.id).first::<Photo>(context.db()) {
if context.is_authorized() || tphoto.is_public() {
if img.size == SizeTag::Large {
if context.is_authorized() {
use std::fs::File;
use std::io::Read;
// TODO: This should be done in a more async-friendly way.
let path = context.photos().get_raw_path(&tphoto);
let mut buf = Vec::new();
if File::open(path)
.map(|mut f| f.read_to_end(&mut buf))
.is_ok()
{
return Response::builder()
.status(StatusCode::OK)
.header(
header::CONTENT_TYPE,
mime::IMAGE_JPEG.as_ref(),
)
.far_expires()
.body(buf)
.unwrap();
} else {
return error_response(
StatusCode::INTERNAL_SERVER_ERROR,
);
}
}
} else {
let data = get_image_data(&context, &tphoto, img.size)
.expect("Get image data");
return Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, mime::IMAGE_JPEG.as_ref())
.far_expires()
.body(data)
.unwrap();
}
}
}
not_found(&context)
}
/// A client-side / url file name for a file.
/// Someting like 4711-s.jpg
#[derive(Debug, Eq, PartialEq)]
pub struct ImgName {
id: i32,
size: SizeTag,
}
#[derive(Debug, Eq, PartialEq)]
pub struct BadImgName {}
impl FromStr for ImgName {
type Err = BadImgName;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Some(pos) = s.find('-') {
let (num, rest) = s.split_at(pos);
let id = num.parse().map_err(|_| BadImgName {})?;
let size = match rest {
"-s.jpg" => SizeTag::Small,
"-m.jpg" => SizeTag::Medium,
"-l.jpg" => SizeTag::Large,
_ => return Err(BadImgName {}),
};
return Ok(ImgName { id, size });
}
Err(BadImgName {})
}
}
#[test]
fn parse_good_imgname() {
assert_eq!(
"4711-s.jpg".parse(),
Ok(ImgName {
id: 4711,
size: SizeTag::Small,
})
)
}
#[test]
fn parse_bad_imgname_1() {
assert_eq!("4711-q.jpg".parse::<ImgName>(), Err(BadImgName {}))
}
#[test]
fn parse_bad_imgname_2() {
assert_eq!("blurgel".parse::<ImgName>(), Err(BadImgName {}))
}
fn get_image_data(
context: &Context,
photo: &Photo,
size: SizeTag,
) -> Result<Vec<u8>, image::ImageError> {
context.cached_or(&photo.cache_key(size), || {
let size = size.px();
context.photos().scale_image(photo, size, size)
})
}

103
src/server/login.rs Normal file
View File

@ -0,0 +1,103 @@
use super::render_ructe::RenderRucte;
use super::Context;
use crate::templates;
use diesel::prelude::*;
use log::info;
use serde::Deserialize;
use warp::http::{header, Response};
pub fn get_login(context: Context, param: NextQ) -> Response<Vec<u8>> {
info!("Got request for login form. Param: {:?}", param);
let next = sanitize_next(param.next.as_ref().map(AsRef::as_ref));
Response::builder().html(|o| templates::login(o, &context, next, None))
}
#[derive(Debug, Default, Deserialize)]
pub struct NextQ {
next: Option<String>,
}
pub fn post_login(context: Context, form: LoginForm) -> Response<Vec<u8>> {
let next = sanitize_next(form.next.as_ref().map(AsRef::as_ref));
use crate::schema::users::dsl::*;
if let Ok(hash) = users
.filter(username.eq(&form.user))
.select(password)
.first::<String>(context.db())
{
if djangohashers::check_password_tolerant(&form.password, &hash) {
info!("User {} logged in", form.user);
let token = context.make_token(&form.user).unwrap();
return Response::builder()
.header(
header::SET_COOKIE,
format!("EXAUTH={}; SameSite=Strict; HttpOpnly", token),
)
.redirect(next.unwrap_or("/"));
}
info!(
"Login failed: Password verification failed for {:?}",
form.user,
);
} else {
info!("Login failed: No hash found for {:?}", form.user);
}
let message = Some("Login failed, please try again");
Response::builder().html(|o| templates::login(o, &context, next, message))
}
/// The data submitted by the login form.
/// This does not derive Debug or Serialize, as the password is plain text.
#[derive(Deserialize)]
pub struct LoginForm {
user: String,
password: String,
next: Option<String>,
}
fn sanitize_next(next: Option<&str>) -> Option<&str> {
if let Some(next) = next {
use regex::Regex;
let re = Regex::new(r"^/([a-z0-9._-]+/?)*$").unwrap();
if re.is_match(next) {
return Some(next);
}
}
None
}
#[test]
fn test_sanitize_bad_1() {
assert_eq!(None, sanitize_next(Some("https://evil.org/")))
}
#[test]
fn test_sanitize_bad_2() {
assert_eq!(None, sanitize_next(Some("//evil.org/")))
}
#[test]
fn test_sanitize_bad_3() {
assert_eq!(None, sanitize_next(Some("/evil\"hack")))
}
#[test]
fn test_sanitize_bad_4() {
assert_eq!(None, sanitize_next(Some("/evil'hack")))
}
#[test]
fn test_sanitize_good_1() {
assert_eq!(Some("/foo/"), sanitize_next(Some("/foo/")))
}
#[test]
fn test_sanitize_good_2() {
assert_eq!(Some("/2017/7/15"), sanitize_next(Some("/2017/7/15")))
}
pub fn logout(_context: Context) -> Response<Vec<u8>> {
Response::builder()
.header(
header::SET_COOKIE,
"EXAUTH=; Max-Age=0; SameSite=Strict; HttpOpnly",
)
.redirect("/")
}

View File

@ -1,32 +1,34 @@
#[macro_use]
mod admin; mod admin;
mod context; mod context;
mod image;
mod login;
mod photolink;
mod render_ructe; mod render_ructe;
mod splitlist; mod splitlist;
mod views_by_category;
mod views_by_date; mod views_by_date;
use self::context::create_session_filter; use self::context::create_session_filter;
pub use self::context::Context; pub use self::context::Context;
pub use self::photolink::PhotoLink;
use self::render_ructe::RenderRucte; use self::render_ructe::RenderRucte;
use self::splitlist::*; use self::splitlist::*;
use self::views_by_category::*;
use self::views_by_date::*; use self::views_by_date::*;
use super::{CacheOpt, DbOpt, DirOpt}; use super::{CacheOpt, DbOpt, DirOpt};
use crate::adm::result::Error; use crate::adm::result::Error;
use crate::models::{Person, Photo, Place, Tag}; use crate::models::Photo;
use crate::pidfiles::handle_pid_file; use crate::pidfiles::handle_pid_file;
use crate::templates::{self, Html}; use crate::templates::{self, Html};
use chrono::Datelike; use chrono::Datelike;
use diesel::prelude::*; use diesel::prelude::*;
use djangohashers;
use image;
use log::info; use log::info;
use mime;
use serde::Deserialize; use serde::Deserialize;
use std::net::SocketAddr; use std::net::SocketAddr;
use structopt::StructOpt; use structopt::StructOpt;
use warp::filters::path::Tail; use warp::filters::path::Tail;
use warp::http::{header, Response, StatusCode}; use warp::http::{header, Response, StatusCode};
use warp::{self, reply, Filter, Rejection, Reply}; use warp::{self, Filter, Rejection, Reply};
#[derive(StructOpt)] #[derive(StructOpt)]
#[structopt(rename_all = "kebab-case")] #[structopt(rename_all = "kebab-case")]
@ -57,127 +59,6 @@ pub struct Args {
jwt_key: String, jwt_key: String,
} }
pub struct PhotoLink {
pub title: Option<String>,
pub href: String,
pub id: i32,
pub size: (u32, u32),
pub lable: Option<String>,
}
impl PhotoLink {
fn for_group(g: &[Photo], base_url: &str, with_date: bool) -> PhotoLink {
if g.len() == 1 {
if with_date {
PhotoLink::date_title(&g[0])
} else {
PhotoLink::no_title(&g[0])
}
} else {
fn imgscore(p: &Photo) -> i16 {
// Only score below 19 is worse than ungraded.
p.grade.unwrap_or(19) * if p.is_public { 5 } else { 4 }
}
let photo = g.iter().max_by_key(|p| imgscore(p)).unwrap();
let (title, lable) = {
let from = g.last().and_then(|p| p.date);
let to = g.first().and_then(|p| p.date);
if let (Some(from), Some(to)) = (from, to) {
if from.date() == to.date() {
(
Some(from.format("%F").to_string()),
format!(
"{} - {} ({})",
from.format("%R"),
to.format("%R"),
g.len(),
),
)
} else if from.year() == to.year() {
if from.month() == to.month() {
(
Some(from.format("%Y-%m").to_string()),
format!(
"{} - {} ({})",
from.format("%F"),
to.format("%d"),
g.len(),
),
)
} else {
(
Some(from.format("%Y").to_string()),
format!(
"{} - {} ({})",
from.format("%F"),
to.format("%m-%d"),
g.len(),
),
)
}
} else {
(
None,
format!(
"{} - {} ({})",
from.format("%F"),
to.format("%F"),
g.len(),
),
)
}
} else {
(
None,
format!(
"{} - {} ({})",
from.map(|d| format!("{}", d.format("%F %R")))
.unwrap_or_else(|| "-".to_string()),
to.map(|d| format!("{}", d.format("%F %R")))
.unwrap_or_else(|| "-".to_string()),
g.len(),
),
)
}
};
let title = if with_date { title } else { None };
PhotoLink {
title,
href: format!(
"{}?from={}&to={}",
base_url,
g.last().map(|p| p.id).unwrap_or(0),
g.first().map(|p| p.id).unwrap_or(0),
),
id: photo.id,
size: photo.get_size(SizeTag::Small),
lable: Some(lable),
}
}
}
fn date_title(p: &Photo) -> PhotoLink {
PhotoLink {
title: p.date.map(|d| d.format("%F").to_string()),
href: format!("/img/{}", p.id),
id: p.id,
size: p.get_size(SizeTag::Small),
lable: p.date.map(|d| d.format("%T").to_string()),
}
}
fn no_title(p: &Photo) -> PhotoLink {
PhotoLink {
title: None, // p.date.map(|d| d.format("%F").to_string()),
href: format!("/img/{}", p.id),
id: p.id,
size: p.get_size(SizeTag::Small),
lable: p.date.map(|d| d.format("%T").to_string()),
}
}
pub fn is_portrait(&self) -> bool {
self.size.1 > self.size.0
}
}
pub fn run(args: &Args) -> Result<(), Error> { pub fn run(args: &Args) -> Result<(), Error> {
if let Some(pidfile) = &args.pidfile { if let Some(pidfile) = &args.pidfile {
handle_pid_file(&pidfile, args.replace).unwrap() handle_pid_file(&pidfile, args.replace).unwrap()
@ -199,12 +80,12 @@ pub fn run(args: &Args) -> Result<(), Error> {
#[rustfmt::skip] #[rustfmt::skip]
let routes = warp::any() let routes = warp::any()
.and(static_routes) .and(static_routes)
.or(get().and(path("login")).and(end()).and(s()).and(query()).map(login)) .or(get().and(path("login")).and(end()).and(s()).and(query()).map(login::get_login))
.or(post().and(path("login")).and(end()).and(s()).and(body::form()).map(do_login)) .or(post().and(path("login")).and(end()).and(s()).and(body::form()).map(login::post_login))
.or(path("logout").and(end()).and(s()).map(logout)) .or(path("logout").and(end()).and(s()).map(login::logout))
.or(get().and(end()).and(s()).map(all_years)) .or(get().and(end()).and(s()).map(all_years))
.or(get().and(path("img")).and(param()).and(end()).and(s()).map(photo_details)) .or(get().and(path("img")).and(param()).and(end()).and(s()).map(photo_details))
.or(get().and(path("img")).and(param()).and(end()).and(s()).map(show_image)) .or(get().and(path("img")).and(param()).and(end()).and(s()).map(image::show_image))
.or(get().and(path("0")).and(end()).and(s()).map(all_null_date)) .or(get().and(path("0")).and(end()).and(s()).map(all_null_date))
.or(get().and(param()).and(end()).and(s()).map(months_in_year)) .or(get().and(param()).and(end()).and(s()).map(months_in_year))
.or(get().and(param()).and(param()).and(end()).and(s()).map(days_in_month)) .or(get().and(param()).and(param()).and(end()).and(s()).map(days_in_month))
@ -277,287 +158,6 @@ fn error_response(err: StatusCode) -> Response<Vec<u8>> {
.html(|o| templates::error(o, err, "Sorry about this.")) .html(|o| templates::error(o, err, "Sorry about this."))
} }
fn login(context: Context, param: NextQ) -> Response<Vec<u8>> {
info!("Got request for login form. Param: {:?}", param);
let next = sanitize_next(param.next.as_ref().map(AsRef::as_ref));
Response::builder().html(|o| templates::login(o, &context, next, None))
}
#[derive(Debug, Default, Deserialize)]
struct NextQ {
next: Option<String>,
}
fn do_login(context: Context, form: LoginForm) -> Response<Vec<u8>> {
let next = sanitize_next(form.next.as_ref().map(AsRef::as_ref));
use crate::schema::users::dsl::*;
if let Ok(hash) = users
.filter(username.eq(&form.user))
.select(password)
.first::<String>(context.db())
{
if djangohashers::check_password_tolerant(&form.password, &hash) {
info!("User {} logged in", form.user);
let token = context.make_token(&form.user).unwrap();
return Response::builder()
.header(
header::SET_COOKIE,
format!("EXAUTH={}; SameSite=Strict; HttpOpnly", token),
)
.redirect(next.unwrap_or("/"));
}
info!(
"Login failed: Password verification failed for {:?}",
form.user,
);
} else {
info!("Login failed: No hash found for {:?}", form.user);
}
let message = Some("Login failed, please try again");
Response::builder().html(|o| templates::login(o, &context, next, message))
}
/// The data submitted by the login form.
/// This does not derive Debug or Serialize, as the password is plain text.
#[derive(Deserialize)]
struct LoginForm {
user: String,
password: String,
next: Option<String>,
}
fn sanitize_next(next: Option<&str>) -> Option<&str> {
if let Some(next) = next {
use regex::Regex;
let re = Regex::new(r"^/([a-z0-9._-]+/?)*$").unwrap();
if re.is_match(next) {
return Some(next);
}
}
None
}
#[test]
fn test_sanitize_bad_1() {
assert_eq!(None, sanitize_next(Some("https://evil.org/")))
}
#[test]
fn test_sanitize_bad_2() {
assert_eq!(None, sanitize_next(Some("//evil.org/")))
}
#[test]
fn test_sanitize_bad_3() {
assert_eq!(None, sanitize_next(Some("/evil\"hack")))
}
#[test]
fn test_sanitize_bad_4() {
assert_eq!(None, sanitize_next(Some("/evil'hack")))
}
#[test]
fn test_sanitize_good_1() {
assert_eq!(Some("/foo/"), sanitize_next(Some("/foo/")))
}
#[test]
fn test_sanitize_good_2() {
assert_eq!(Some("/2017/7/15"), sanitize_next(Some("/2017/7/15")))
}
fn logout(_context: Context) -> Response<Vec<u8>> {
Response::builder()
.header(
header::SET_COOKIE,
"EXAUTH=; Max-Age=0; SameSite=Strict; HttpOpnly",
)
.redirect("/")
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SizeTag {
Small,
Medium,
Large,
}
impl SizeTag {
pub fn px(self) -> u32 {
match self {
SizeTag::Small => 240,
SizeTag::Medium => 960,
SizeTag::Large => 1900,
}
}
}
fn show_image(img: ImgName, context: Context) -> Response<Vec<u8>> {
use crate::schema::photos::dsl::photos;
if let Ok(tphoto) = photos.find(img.id).first::<Photo>(context.db()) {
if context.is_authorized() || tphoto.is_public() {
if img.size == SizeTag::Large {
if context.is_authorized() {
use std::fs::File;
use std::io::Read;
// TODO: This should be done in a more async-friendly way.
let path = context.photos().get_raw_path(&tphoto);
let mut buf = Vec::new();
if File::open(path)
.map(|mut f| f.read_to_end(&mut buf))
.is_ok()
{
return Response::builder()
.status(StatusCode::OK)
.header(
header::CONTENT_TYPE,
mime::IMAGE_JPEG.as_ref(),
)
.far_expires()
.body(buf)
.unwrap();
} else {
return error_response(
StatusCode::INTERNAL_SERVER_ERROR,
);
}
}
} else {
let data = get_image_data(&context, &tphoto, img.size)
.expect("Get image data");
return Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, mime::IMAGE_JPEG.as_ref())
.far_expires()
.body(data)
.unwrap();
}
}
}
not_found(&context)
}
/// A client-side / url file name for a file.
/// Someting like 4711-s.jpg
#[derive(Debug, Eq, PartialEq)]
struct ImgName {
id: i32,
size: SizeTag,
}
use std::str::FromStr;
#[derive(Debug, Eq, PartialEq)]
struct BadImgName {}
impl FromStr for ImgName {
type Err = BadImgName;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Some(pos) = s.find('-') {
let (num, rest) = s.split_at(pos);
let id = num.parse().map_err(|_| BadImgName {})?;
let size = match rest {
"-s.jpg" => SizeTag::Small,
"-m.jpg" => SizeTag::Medium,
"-l.jpg" => SizeTag::Large,
_ => return Err(BadImgName {}),
};
return Ok(ImgName { id, size });
}
Err(BadImgName {})
}
}
#[test]
fn parse_good_imgname() {
assert_eq!(
"4711-s.jpg".parse(),
Ok(ImgName {
id: 4711,
size: SizeTag::Small,
})
)
}
#[test]
fn parse_bad_imgname_1() {
assert_eq!("4711-q.jpg".parse::<ImgName>(), Err(BadImgName {}))
}
#[test]
fn parse_bad_imgname_2() {
assert_eq!("blurgel".parse::<ImgName>(), Err(BadImgName {}))
}
fn get_image_data(
context: &Context,
photo: &Photo,
size: SizeTag,
) -> Result<Vec<u8>, image::ImageError> {
context.cached_or(&photo.cache_key(size), || {
let size = size.px();
context.photos().scale_image(photo, size, size)
})
}
fn tag_all(context: Context) -> Response<Vec<u8>> {
use crate::schema::tags::dsl::{id, tag_name, tags};
let query = tags.into_boxed();
let query = if context.is_authorized() {
query
} else {
use crate::schema::photo_tags::dsl as tp;
use crate::schema::photos::dsl as p;
query.filter(id.eq_any(tp::photo_tags.select(tp::tag_id).filter(
tp::photo_id.eq_any(p::photos.select(p::id).filter(p::is_public)),
)))
};
Response::builder().html(|o| {
templates::tags(
o,
&context,
&query.order(tag_name).load(context.db()).expect("List tags"),
)
})
}
fn tag_one(
context: Context,
tslug: String,
range: ImgRange,
) -> Response<Vec<u8>> {
use crate::schema::tags::dsl::{slug, tags};
if let Ok(tag) = tags.filter(slug.eq(tslug)).first::<Tag>(context.db()) {
use crate::schema::photo_tags::dsl::{photo_id, photo_tags, tag_id};
use crate::schema::photos::dsl::id;
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);
Response::builder()
.html(|o| templates::tag(o, &context, &links, &coords, &tag))
} else {
not_found(&context)
}
}
fn place_all(context: Context) -> Response<Vec<u8>> {
use crate::schema::places::dsl::{id, place_name, places};
let query = places.into_boxed();
let query = if context.is_authorized() {
query
} else {
use crate::schema::photo_places::dsl as pp;
use crate::schema::photos::dsl as p;
query.filter(id.eq_any(pp::photo_places.select(pp::place_id).filter(
pp::photo_id.eq_any(p::photos.select(p::id).filter(p::is_public)),
)))
};
Response::builder().html(|o| {
templates::places(
o,
&context,
&query
.order(place_name)
.load(context.db())
.expect("List places"),
)
})
}
/// Handler for static files. /// Handler for static files.
/// Create a response from the file data with a correct content type /// Create a response from the file data with a correct content type
/// and a far expires header (or a 404 if the file does not exist). /// and a far expires header (or a 404 if the file does not exist).
@ -575,81 +175,6 @@ fn static_file(name: Tail) -> Result<impl Reply, Rejection> {
} }
} }
fn place_one(
context: Context,
tslug: String,
range: ImgRange,
) -> Response<Vec<u8>> {
use crate::schema::places::dsl::{places, slug};
if let Ok(place) =
places.filter(slug.eq(tslug)).first::<Place>(context.db())
{
use crate::schema::photo_places::dsl::{
photo_id, photo_places, place_id,
};
use crate::schema::photos::dsl::id;
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);
Response::builder()
.html(|o| templates::place(o, &context, &links, &coord, &place))
} else {
not_found(&context)
}
}
fn person_all(context: Context) -> Response<Vec<u8>> {
use crate::schema::people::dsl::{id, people, person_name};
let query = people.into_boxed();
let query = if context.is_authorized() {
query
} else {
use crate::schema::photo_people::dsl as pp;
use crate::schema::photos::dsl as p;
query.filter(id.eq_any(pp::photo_people.select(pp::person_id).filter(
pp::photo_id.eq_any(p::photos.select(p::id).filter(p::is_public)),
)))
};
Response::builder().html(|o| {
templates::people(
o,
&context,
&query
.order(person_name)
.load(context.db())
.expect("list people"),
)
})
}
fn person_one(
context: Context,
tslug: String,
range: ImgRange,
) -> Response<Vec<u8>> {
use crate::schema::people::dsl::{people, slug};
let c = context.db();
if let Ok(person) = people.filter(slug.eq(tslug)).first::<Person>(c) {
use crate::schema::photo_people::dsl::{
person_id, photo_id, photo_people,
};
use crate::schema::photos::dsl::id;
let photos = Photo::query(context.is_authorized()).filter(
id.eq_any(
photo_people
.select(photo_id)
.filter(person_id.eq(person.id)),
),
);
let (links, coords) = links_by_time(&context, photos, range, true);
Response::builder()
.html(|o| templates::person(o, &context, &links, &coords, &person))
} else {
not_found(&context)
}
}
fn random_image(context: Context) -> Response<Vec<u8>> { fn random_image(context: Context) -> Response<Vec<u8>> {
use crate::schema::photos::dsl::id; use crate::schema::photos::dsl::id;
use diesel::expression::dsl::sql; use diesel::expression::dsl::sql;
@ -746,31 +271,6 @@ impl Link {
} }
} }
fn auto_complete_tag(context: Context, query: AcQ) -> impl Reply {
use crate::schema::tags::dsl::{tag_name, tags};
let q = tags
.select(tag_name)
.filter(tag_name.ilike(query.q + "%"))
.order(tag_name)
.limit(10);
reply::json(&q.load::<String>(context.db()).unwrap())
}
fn auto_complete_person(context: Context, query: AcQ) -> impl Reply {
use crate::schema::people::dsl::{people, person_name};
let q = people
.select(person_name)
.filter(person_name.ilike(query.q + "%"))
.order(person_name)
.limit(10);
reply::json(&q.load::<String>(context.db()).unwrap())
}
#[derive(Deserialize)]
struct AcQ {
q: String,
}
#[derive(Debug, Default, Deserialize)] #[derive(Debug, Default, Deserialize)]
pub struct ImgRange { pub struct ImgRange {
pub from: Option<i32>, pub from: Option<i32>,

127
src/server/photolink.rs Normal file
View File

@ -0,0 +1,127 @@
use crate::models::{Photo, SizeTag};
use chrono::Datelike;
pub struct PhotoLink {
pub title: Option<String>,
pub href: String,
pub id: i32,
pub size: (u32, u32),
pub lable: Option<String>,
}
impl PhotoLink {
pub fn for_group(
g: &[Photo],
base_url: &str,
with_date: bool,
) -> PhotoLink {
if g.len() == 1 {
if with_date {
PhotoLink::date_title(&g[0])
} else {
PhotoLink::no_title(&g[0])
}
} else {
fn imgscore(p: &Photo) -> i16 {
// Only score below 19 is worse than ungraded.
p.grade.unwrap_or(19) * if p.is_public { 5 } else { 4 }
}
let photo = g.iter().max_by_key(|p| imgscore(p)).unwrap();
let (title, lable) = {
let from = g.last().and_then(|p| p.date);
let to = g.first().and_then(|p| p.date);
if let (Some(from), Some(to)) = (from, to) {
if from.date() == to.date() {
(
Some(from.format("%F").to_string()),
format!(
"{} - {} ({})",
from.format("%R"),
to.format("%R"),
g.len(),
),
)
} else if from.year() == to.year() {
if from.month() == to.month() {
(
Some(from.format("%Y-%m").to_string()),
format!(
"{} - {} ({})",
from.format("%F"),
to.format("%d"),
g.len(),
),
)
} else {
(
Some(from.format("%Y").to_string()),
format!(
"{} - {} ({})",
from.format("%F"),
to.format("%m-%d"),
g.len(),
),
)
}
} else {
(
None,
format!(
"{} - {} ({})",
from.format("%F"),
to.format("%F"),
g.len(),
),
)
}
} else {
(
None,
format!(
"{} - {} ({})",
from.map(|d| format!("{}", d.format("%F %R")))
.unwrap_or_else(|| "-".to_string()),
to.map(|d| format!("{}", d.format("%F %R")))
.unwrap_or_else(|| "-".to_string()),
g.len(),
),
)
}
};
let title = if with_date { title } else { None };
PhotoLink {
title,
href: format!(
"{}?from={}&to={}",
base_url,
g.last().map(|p| p.id).unwrap_or(0),
g.first().map(|p| p.id).unwrap_or(0),
),
id: photo.id,
size: photo.get_size(SizeTag::Small),
lable: Some(lable),
}
}
}
pub fn date_title(p: &Photo) -> PhotoLink {
PhotoLink {
title: p.date.map(|d| d.format("%F").to_string()),
href: format!("/img/{}", p.id),
id: p.id,
size: p.get_size(SizeTag::Small),
lable: p.date.map(|d| d.format("%T").to_string()),
}
}
pub fn no_title(p: &Photo) -> PhotoLink {
PhotoLink {
title: None, // p.date.map(|d| d.format("%F").to_string()),
href: format!("/img/{}", p.id),
id: p.id,
size: p.get_size(SizeTag::Small),
lable: p.date.map(|d| d.format("%T").to_string()),
}
}
pub fn is_portrait(&self) -> bool {
self.size.1 > self.size.0
}
}

View File

@ -0,0 +1,174 @@
//! Handle photos by tag, person, or place.
use super::render_ructe::RenderRucte;
use super::{links_by_time, not_found, Context, ImgRange};
use crate::models::{Person, Photo, Place, Tag};
use crate::templates;
use diesel::prelude::*;
use serde::Deserialize;
use warp::http::Response;
use warp::{reply, Reply};
pub fn person_all(context: Context) -> Response<Vec<u8>> {
use crate::schema::people::dsl::{id, people, person_name};
let query = people.into_boxed();
let query = if context.is_authorized() {
query
} else {
use crate::schema::photo_people::dsl as pp;
use crate::schema::photos::dsl as p;
query.filter(id.eq_any(pp::photo_people.select(pp::person_id).filter(
pp::photo_id.eq_any(p::photos.select(p::id).filter(p::is_public)),
)))
};
Response::builder().html(|o| {
templates::people(
o,
&context,
&query
.order(person_name)
.load(context.db())
.expect("list people"),
)
})
}
pub fn person_one(
context: Context,
tslug: String,
range: ImgRange,
) -> Response<Vec<u8>> {
use crate::schema::people::dsl::{people, slug};
let c = context.db();
if let Ok(person) = people.filter(slug.eq(tslug)).first::<Person>(c) {
use crate::schema::photo_people::dsl::{
person_id, photo_id, photo_people,
};
use crate::schema::photos::dsl::id;
let photos = Photo::query(context.is_authorized()).filter(
id.eq_any(
photo_people
.select(photo_id)
.filter(person_id.eq(person.id)),
),
);
let (links, coords) = links_by_time(&context, photos, range, true);
Response::builder()
.html(|o| templates::person(o, &context, &links, &coords, &person))
} else {
not_found(&context)
}
}
pub fn tag_all(context: Context) -> Response<Vec<u8>> {
use crate::schema::tags::dsl::{id, tag_name, tags};
let query = tags.into_boxed();
let query = if context.is_authorized() {
query
} else {
use crate::schema::photo_tags::dsl as tp;
use crate::schema::photos::dsl as p;
query.filter(id.eq_any(tp::photo_tags.select(tp::tag_id).filter(
tp::photo_id.eq_any(p::photos.select(p::id).filter(p::is_public)),
)))
};
Response::builder().html(|o| {
templates::tags(
o,
&context,
&query.order(tag_name).load(context.db()).expect("List tags"),
)
})
}
pub fn tag_one(
context: Context,
tslug: String,
range: ImgRange,
) -> Response<Vec<u8>> {
use crate::schema::tags::dsl::{slug, tags};
if let Ok(tag) = tags.filter(slug.eq(tslug)).first::<Tag>(context.db()) {
use crate::schema::photo_tags::dsl::{photo_id, photo_tags, tag_id};
use crate::schema::photos::dsl::id;
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);
Response::builder()
.html(|o| templates::tag(o, &context, &links, &coords, &tag))
} else {
not_found(&context)
}
}
pub fn place_all(context: Context) -> Response<Vec<u8>> {
use crate::schema::places::dsl::{id, place_name, places};
let query = places.into_boxed();
let query = if context.is_authorized() {
query
} else {
use crate::schema::photo_places::dsl as pp;
use crate::schema::photos::dsl as p;
query.filter(id.eq_any(pp::photo_places.select(pp::place_id).filter(
pp::photo_id.eq_any(p::photos.select(p::id).filter(p::is_public)),
)))
};
Response::builder().html(|o| {
templates::places(
o,
&context,
&query
.order(place_name)
.load(context.db())
.expect("List places"),
)
})
}
pub fn place_one(
context: Context,
tslug: String,
range: ImgRange,
) -> Response<Vec<u8>> {
use crate::schema::places::dsl::{places, slug};
if let Ok(place) =
places.filter(slug.eq(tslug)).first::<Place>(context.db())
{
use crate::schema::photo_places::dsl::{
photo_id, photo_places, place_id,
};
use crate::schema::photos::dsl::id;
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);
Response::builder()
.html(|o| templates::place(o, &context, &links, &coord, &place))
} else {
not_found(&context)
}
}
pub fn auto_complete_tag(context: Context, query: AcQ) -> impl Reply {
use crate::schema::tags::dsl::{tag_name, tags};
let q = tags
.select(tag_name)
.filter(tag_name.ilike(query.q + "%"))
.order(tag_name)
.limit(10);
reply::json(&q.load::<String>(context.db()).unwrap())
}
pub fn auto_complete_person(context: Context, query: AcQ) -> impl Reply {
use crate::schema::people::dsl::{people, person_name};
let q = people
.select(person_name)
.filter(person_name.ilike(query.q + "%"))
.order(person_name)
.limit(10);
reply::json(&q.load::<String>(context.db()).unwrap())
}
#[derive(Deserialize)]
pub struct AcQ {
q: String,
}

View File

@ -1,9 +1,7 @@
use super::render_ructe::RenderRucte; use super::render_ructe::RenderRucte;
use super::splitlist::links_by_time; use super::splitlist::links_by_time;
use super::{ use super::{not_found, redirect_to_img, Context, ImgRange, Link, PhotoLink};
not_found, redirect_to_img, Context, ImgRange, Link, PhotoLink, SizeTag, use crate::models::{Photo, SizeTag};
};
use crate::models::Photo;
use crate::templates; use crate::templates;
use chrono::naive::{NaiveDate, NaiveDateTime}; use chrono::naive::{NaiveDate, NaiveDateTime};
use chrono::Duration as ChDuration; use chrono::Duration as ChDuration;

View File

@ -1,6 +1,6 @@
@use super::page_base; @use super::page_base;
@use crate::models::{Photo, Person, Place, Tag, Camera, Coord}; @use crate::models::{Photo, Person, Place, Tag, Camera, Coord, SizeTag};
@use crate::server::{Context, Link, SizeTag}; @use crate::server::{Context, Link};
@(context: &Context, lpath: &[Link], people: &[Person], places: &[Place], tags: &[Tag], position: &Option<Coord>, attribution: &Option<String>, camera: &Option<Camera>, photo: &Photo) @(context: &Context, lpath: &[Link], people: &[Person], places: &[Place], tags: &[Tag], position: &Option<Coord>, attribution: &Option<String>, camera: &Option<Camera>, photo: &Photo)
@:page_base(context, "Photo details", lpath, { @:page_base(context, "Photo details", lpath, {