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:
parent
088dd74e80
commit
5df25e02de
@ -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};
|
||||||
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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
111
src/server/image.rs
Normal 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
103
src/server/login.rs
Normal 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("/")
|
||||||
|
}
|
@ -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
127
src/server/photolink.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
174
src/server/views_by_category.rs
Normal file
174
src/server/views_by_category.rs
Normal 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,
|
||||||
|
}
|
@ -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;
|
||||||
|
@ -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, {
|
||||||
|
Loading…
Reference in New Issue
Block a user