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 crate::models::Photo;
|
||||
use crate::models::{Photo, SizeTag};
|
||||
use crate::photosdir::PhotosDir;
|
||||
use crate::schema::photos::dsl::{date, is_public};
|
||||
use crate::server::SizeTag;
|
||||
use crate::{CacheOpt, DbOpt, DirOpt};
|
||||
use diesel::prelude::*;
|
||||
use log::{debug, info};
|
||||
|
@ -1,4 +1,3 @@
|
||||
use crate::server::SizeTag;
|
||||
use chrono::naive::NaiveDateTime;
|
||||
use diesel;
|
||||
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.
|
||||
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::models::{Coord, Photo};
|
||||
use crate::models::{Coord, Photo, SizeTag};
|
||||
use diesel::{self, prelude::*};
|
||||
use log::{info, warn};
|
||||
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 context;
|
||||
mod image;
|
||||
mod login;
|
||||
mod photolink;
|
||||
mod render_ructe;
|
||||
mod splitlist;
|
||||
mod views_by_category;
|
||||
mod views_by_date;
|
||||
|
||||
use self::context::create_session_filter;
|
||||
pub use self::context::Context;
|
||||
pub use self::photolink::PhotoLink;
|
||||
use self::render_ructe::RenderRucte;
|
||||
use self::splitlist::*;
|
||||
use self::views_by_category::*;
|
||||
use self::views_by_date::*;
|
||||
use super::{CacheOpt, DbOpt, DirOpt};
|
||||
use crate::adm::result::Error;
|
||||
use crate::models::{Person, Photo, Place, Tag};
|
||||
use crate::models::Photo;
|
||||
use crate::pidfiles::handle_pid_file;
|
||||
use crate::templates::{self, Html};
|
||||
use chrono::Datelike;
|
||||
use diesel::prelude::*;
|
||||
use djangohashers;
|
||||
use image;
|
||||
use log::info;
|
||||
use mime;
|
||||
use serde::Deserialize;
|
||||
use std::net::SocketAddr;
|
||||
use structopt::StructOpt;
|
||||
use warp::filters::path::Tail;
|
||||
use warp::http::{header, Response, StatusCode};
|
||||
use warp::{self, reply, Filter, Rejection, Reply};
|
||||
use warp::{self, Filter, Rejection, Reply};
|
||||
|
||||
#[derive(StructOpt)]
|
||||
#[structopt(rename_all = "kebab-case")]
|
||||
@ -57,127 +59,6 @@ pub struct Args {
|
||||
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> {
|
||||
if let Some(pidfile) = &args.pidfile {
|
||||
handle_pid_file(&pidfile, args.replace).unwrap()
|
||||
@ -199,12 +80,12 @@ pub fn run(args: &Args) -> Result<(), Error> {
|
||||
#[rustfmt::skip]
|
||||
let routes = warp::any()
|
||||
.and(static_routes)
|
||||
.or(get().and(path("login")).and(end()).and(s()).and(query()).map(login))
|
||||
.or(post().and(path("login")).and(end()).and(s()).and(body::form()).map(do_login))
|
||||
.or(path("logout").and(end()).and(s()).map(logout))
|
||||
.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(login::post_login))
|
||||
.or(path("logout").and(end()).and(s()).map(login::logout))
|
||||
.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(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(param()).and(end()).and(s()).map(months_in_year))
|
||||
.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."))
|
||||
}
|
||||
|
||||
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.
|
||||
/// 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).
|
||||
@ -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>> {
|
||||
use crate::schema::photos::dsl::id;
|
||||
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)]
|
||||
pub struct ImgRange {
|
||||
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::splitlist::links_by_time;
|
||||
use super::{
|
||||
not_found, redirect_to_img, Context, ImgRange, Link, PhotoLink, SizeTag,
|
||||
};
|
||||
use crate::models::Photo;
|
||||
use super::{not_found, redirect_to_img, Context, ImgRange, Link, PhotoLink};
|
||||
use crate::models::{Photo, SizeTag};
|
||||
use crate::templates;
|
||||
use chrono::naive::{NaiveDate, NaiveDateTime};
|
||||
use chrono::Duration as ChDuration;
|
||||
|
@ -1,6 +1,6 @@
|
||||
@use super::page_base;
|
||||
@use crate::models::{Photo, Person, Place, Tag, Camera, Coord};
|
||||
@use crate::server::{Context, Link, SizeTag};
|
||||
@use crate::models::{Photo, Person, Place, Tag, Camera, Coord, 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)
|
||||
@:page_base(context, "Photo details", lpath, {
|
||||
|
Loading…
Reference in New Issue
Block a user