Use warp instead of nickel.

Use the modern async warp web framework instead of nickel (which is
starting to feel a bit stale).

Url routing and context passing is changed a lot.
This commit is contained in:
Rasmus Kaj 2018-09-01 14:41:40 +02:00
parent 7080f7b382
commit 5c03a28f59
26 changed files with 1015 additions and 1153 deletions

View File

@ -7,31 +7,30 @@ edition = "2018"
build = "src/build.rs" build = "src/build.rs"
[build-dependencies] [build-dependencies]
ructe = { version = "^0.6.2", features = ["sass", "mime02"] } ructe = { version = "^0.6.2", features = ["sass", "mime03"] }
[dependencies] [dependencies]
nickel = "~0.10.0" warp = "0.1.6"
nickel-jwt-session = "~0.10.0"
hyper = "~0.10.0"
env_logger = "*" env_logger = "*"
libc = "*" libc = "*"
log = "*" log = "*"
chrono = "~0.4.0" # Must match version used by diesel chrono = "~0.4.0" # Must match version used by diesel
clap = { version = "^2.19", features = [ "color", "wrap_help" ] } clap = { version = "^2.19", features = [ "color", "wrap_help" ] }
typemap = "*"
plugin = "*"
image = "0.21" image = "0.21"
jwt = "0.4.0"
time = "*" time = "*"
kamadak-exif = "~0.3.0" kamadak-exif = "~0.3.0"
diesel = { version = "1.4.0", features = ["r2d2", "chrono", "postgres"] } diesel = { version = "1.4.0", features = ["r2d2", "chrono", "postgres"] }
dotenv = "0.13.0" dotenv = "0.13.0"
djangohashers = "*" djangohashers = "*"
rand = "0.6.5" rand = "0.6.5"
rust-crypto = "0.2.36"
memcached-rs = "0.4.1" memcached-rs = "0.4.1"
flate2 = "^1.0.0" flate2 = "^1.0.0"
brotli2 = "*" brotli2 = "*"
mime = "0.2.6" mime = "0.3.0"
regex = "*" regex = "*"
slug = "0.1" slug = "0.1"
reqwest = "0.9" reqwest = "0.9"
serde = { version = "1.0.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"

View File

@ -2,20 +2,14 @@
#![recursion_limit = "128"] #![recursion_limit = "128"]
#[macro_use] #[macro_use]
extern crate diesel; extern crate diesel;
#[macro_use]
extern crate nickel;
mod adm; mod adm;
mod env; mod env;
mod fetch_places; mod fetch_places;
mod memcachemiddleware;
mod models; mod models;
mod myexif; mod myexif;
mod nickel_diesel;
mod photosdir; mod photosdir;
mod photosdirmiddleware;
mod pidfiles; mod pidfiles;
mod requestloggermiddleware;
mod schema; mod schema;
mod server; mod server;

View File

@ -1,136 +0,0 @@
use log::{debug, warn};
use memcached::proto::{Error as MprotError, Operation, ProtoType};
use memcached::Client;
use nickel::{Continue, Middleware, MiddlewareResult, Request, Response};
use plugin::Extensible;
use std::convert::From;
use std::error::Error;
use std::{fmt, io};
use typemap::Key;
pub struct MemcacheMiddleware {
servers: Vec<(String, usize)>,
}
impl MemcacheMiddleware {
pub fn new(servers: Vec<(String, usize)>) -> Self {
MemcacheMiddleware { servers }
}
}
impl Key for MemcacheMiddleware {
type Value = Vec<(String, usize)>;
}
impl<D> Middleware<D> for MemcacheMiddleware {
fn invoke<'mw, 'conn>(
&self,
req: &mut Request<'mw, 'conn, D>,
res: Response<'mw, D>,
) -> MiddlewareResult<'mw, D> {
req.extensions_mut()
.insert::<MemcacheMiddleware>(self.servers.clone());
Ok(Continue(res))
}
}
pub trait MemcacheRequestExtensions {
fn cache(&self) -> Result<Client, McError>;
fn cached_or<F, E>(&self, key: &str, calculate: F) -> Result<Vec<u8>, E>
where
F: FnOnce() -> Result<Vec<u8>, E>;
fn clear_cache(&self, key: &str);
}
#[derive(Debug)]
pub enum McError {
UninitializedMiddleware,
IoError(io::Error),
}
impl fmt::Display for McError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
McError::UninitializedMiddleware => {
write!(f, "middleware not properly initialized")
}
McError::IoError(ref err) => write!(f, "{}", err.description()),
}
}
}
impl Error for McError {
fn description(&self) -> &str {
match *self {
McError::UninitializedMiddleware => {
"middleware not properly initialized"
}
McError::IoError(ref err) => err.description(),
}
}
}
impl From<io::Error> for McError {
fn from(err: io::Error) -> Self {
McError::IoError(err)
}
}
impl<'a, 'b, D> MemcacheRequestExtensions for Request<'a, 'b, D> {
fn cache(&self) -> Result<Client, McError> {
match self.extensions().get::<MemcacheMiddleware>() {
Some(ext) => {
let mut servers = Vec::new();
for &(ref s, n) in ext {
servers.push((&s[..], n));
}
Ok(Client::connect(&servers, ProtoType::Binary)?)
}
None => Err(McError::UninitializedMiddleware),
}
}
fn cached_or<F, E>(&self, key: &str, init: F) -> Result<Vec<u8>, E>
where
F: FnOnce() -> Result<Vec<u8>, E>,
{
match self.cache() {
Ok(mut client) => {
match client.get(key.as_bytes()) {
Ok((data, _flags)) => {
debug!("Cache: {} found", key);
return Ok(data);
}
Err(MprotError::BinaryProtoError(ref err))
if err.description() == "key not found" =>
{
debug!("Cache: {} not found", key);
}
Err(err) => {
warn!("Cache: get {} failed: {:?}", key, err);
}
}
let data = init()?;
match client.set(key.as_bytes(), &data, 0, 7 * 24 * 60 * 60) {
Ok(()) => debug!("Cache: stored {}", key),
Err(err) => warn!("Cache: Error storing {}: {}", key, err),
}
Ok(data)
}
Err(err) => {
warn!("Error connecting to memcached: {}", err);
init()
}
}
}
fn clear_cache(&self, key: &str) {
if let Ok(mut client) = self.cache() {
match client.delete(key.as_bytes()) {
Ok(()) => debug!("Cache: deleted {}", key),
Err(e) => warn!("Cache: Failed to delete {}: {}", key, e),
}
}
}
}

View File

@ -1,86 +0,0 @@
use diesel::connection::Connection;
use diesel::r2d2::{ConnectionManager, HandleError, Pool, PooledConnection};
use nickel::{Continue, Middleware, MiddlewareResult, Request, Response};
use plugin::Extensible;
use std::any::Any;
use std::error::Error as StdError;
use std::sync::Arc;
use typemap::Key;
pub struct DieselMiddleware<T>
where
T: Connection + Send + Any,
{
pub pool: Arc<Pool<ConnectionManager<T>>>,
}
impl<T> DieselMiddleware<T>
where
T: Connection + Send + Any,
{
pub fn new(
connect_str: &str,
num_connections: u32,
error_handler: Box<HandleError<::diesel::r2d2::Error>>,
) -> Result<DieselMiddleware<T>, Box<StdError>> {
let manager = ConnectionManager::<T>::new(connect_str);
let pool = Pool::builder()
.error_handler(error_handler)
.max_size(num_connections)
.build(manager)?;
Ok(DieselMiddleware {
pool: Arc::new(pool),
})
}
#[allow(dead_code)]
pub fn from_pool(pool: Pool<ConnectionManager<T>>) -> DieselMiddleware<T> {
DieselMiddleware {
pool: Arc::new(pool),
}
}
}
impl<T> Key for DieselMiddleware<T>
where
T: Connection + Send + Any,
{
type Value = Arc<Pool<ConnectionManager<T>>>;
}
impl<T, D> Middleware<D> for DieselMiddleware<T>
where
T: Connection + Send + Any,
{
fn invoke<'mw, 'conn>(
&self,
req: &mut Request<'mw, 'conn, D>,
res: Response<'mw, D>,
) -> MiddlewareResult<'mw, D> {
req.extensions_mut()
.insert::<DieselMiddleware<T>>(Arc::clone(&self.pool));
Ok(Continue(res))
}
}
pub trait DieselRequestExtensions<T>
where
T: Connection + Send + Any,
{
fn db_conn(&self) -> PooledConnection<ConnectionManager<T>>;
}
impl<'a, 'b, T, D> DieselRequestExtensions<T> for Request<'a, 'b, D>
where
T: Connection + Send + Any,
{
fn db_conn(&self) -> PooledConnection<ConnectionManager<T>> {
self.extensions()
.get::<DieselMiddleware<T>>()
.unwrap()
.get()
.unwrap()
}
}

View File

@ -1,41 +0,0 @@
use crate::photosdir::PhotosDir;
use nickel::{Continue, Middleware, MiddlewareResult, Request, Response};
use plugin::Extensible;
use std::path::PathBuf;
use typemap::Key;
pub struct PhotosDirMiddleware {
dir: PathBuf,
}
impl PhotosDirMiddleware {
pub fn new(dir: PathBuf) -> Self {
PhotosDirMiddleware { dir }
}
}
impl Key for PhotosDirMiddleware {
type Value = PhotosDir;
}
impl<D> Middleware<D> for PhotosDirMiddleware {
fn invoke<'mw, 'conn>(
&self,
req: &mut Request<'mw, 'conn, D>,
res: Response<'mw, D>,
) -> MiddlewareResult<'mw, D> {
req.extensions_mut()
.insert::<PhotosDirMiddleware>(PhotosDir::new(self.dir.clone()));
Ok(Continue(res))
}
}
pub trait PhotosDirRequestExtensions {
fn photos(&self) -> &PhotosDir;
}
impl<'a, 'b, D> PhotosDirRequestExtensions for Request<'a, 'b, D> {
fn photos(&self) -> &PhotosDir {
self.extensions().get::<PhotosDirMiddleware>().unwrap()
}
}

View File

@ -1,79 +0,0 @@
use log::{debug, info};
use nickel::status::StatusCode;
use nickel::{Continue, Middleware, MiddlewareResult, Request, Response};
use plugin::Extensible;
use std::sync::{Arc, Mutex};
use time::{get_time, Duration, Timespec};
use typemap::Key;
pub struct RequestLoggerMiddleware;
pub struct RequestLogger {
start: Timespec,
mu: String,
status: Arc<Mutex<StatusCode>>,
}
impl RequestLogger {
pub fn new(mu: String, status: Arc<Mutex<StatusCode>>) -> RequestLogger {
debug!("Start handling {}", mu);
RequestLogger {
start: get_time(),
mu,
status,
}
}
}
impl Drop for RequestLogger {
fn drop(&mut self) {
if let Ok(status) = self.status.lock() {
info!(
"{:?} {} after {}",
self.mu,
*status,
fmt_elapsed(get_time() - self.start)
);
}
}
}
fn fmt_elapsed(t: Duration) -> String {
let ms = t.num_milliseconds();
if ms > 1000 {
format!("{:.2} s", ms as f32 * 1e-3)
} else {
let ns = t.num_nanoseconds().unwrap();
if ns > 10_000_000 {
format!("{} ms", ns / 1_000_000)
} else if ns > 10_000 {
format!("{} µs", ns / 1000)
} else {
format!("{} ns", ns)
}
}
}
impl Key for RequestLoggerMiddleware {
type Value = RequestLogger;
}
impl<D> Middleware<D> for RequestLoggerMiddleware {
fn invoke<'mw, 'conn>(
&self,
req: &mut Request<'mw, 'conn, D>,
mut res: Response<'mw, D>,
) -> MiddlewareResult<'mw, D> {
let mu = format!("{} {}", req.origin.method, req.origin.uri);
let status = Arc::new(Mutex::new(StatusCode::Continue));
req.extensions_mut().insert::<RequestLoggerMiddleware>(
RequestLogger::new(mu, Arc::clone(&status)),
);
res.on_send(move |r| {
if let Ok(mut sw) = status.lock() {
*sw = r.status();
}
});
Ok(Continue(res))
}
}

View File

@ -1,129 +1,123 @@
//! Admin-only views, generally called by javascript. //! Admin-only views, generally called by javascript.
use super::nickelext::MyResponse; use super::{not_found, permission_denied, redirect_to_img, Context, SizeTag};
use super::SizeTag;
use crate::fetch_places::update_image_places; use crate::fetch_places::update_image_places;
use crate::memcachemiddleware::MemcacheRequestExtensions;
use crate::models::{Coord, Photo}; use crate::models::{Coord, Photo};
use crate::nickel_diesel::DieselRequestExtensions; use diesel::{self, prelude::*};
use diesel::prelude::*;
use log::{info, warn}; use log::{info, warn};
use nickel::extensions::Redirect; use serde::Deserialize;
use nickel::status::StatusCode;
use nickel::{BodyError, FormBody, MiddlewareResult, Request, Response};
use nickel_jwt_session::SessionRequestExtensions;
use slug::slugify; use slug::slugify;
use warp::filters::BoxedFilter;
use warp::http::Response;
use warp::{Filter, Reply};
pub fn rotate<'mw>( pub fn routes(s: BoxedFilter<(Context,)>) -> BoxedFilter<(impl Reply,)> {
req: &mut Request, use warp::{body::form, path, post2 as post};
res: Response<'mw>, let route = path("grade")
) -> MiddlewareResult<'mw> { .and(s.clone())
if req.authorized_user().is_none() { .and(form())
return res.error(StatusCode::Unauthorized, "permission denied"); .map(set_grade)
.or(path("locate").and(s.clone()).and(form()).map(set_location))
.unify()
.or(path("person").and(s.clone()).and(form()).map(set_person))
.unify()
.or(path("rotate").and(s.clone()).and(form()).map(rotate))
.unify()
.or(path("tag").and(s.clone()).and(form()).map(set_tag))
.unify();
post().and(route).boxed()
}
fn rotate(context: Context, form: RotateForm) -> Response<Vec<u8>> {
if !context.is_authorized() {
return permission_denied();
} }
if let (Some(image), Some(angle)) = try_with!(res, rotate_params(req)) { info!("Should rotate #{} by {}", form.image, form.angle);
info!("Should rotate #{} by {}", image, angle);
use crate::schema::photos::dsl::photos; use crate::schema::photos::dsl::photos;
let c: &PgConnection = &req.db_conn(); let c = context.db();
if let Ok(mut image) = photos.find(image).first::<Photo>(c) { if let Ok(mut image) = photos.find(form.image).first::<Photo>(c) {
let newvalue = (360 + image.rotation + angle) % 360; let newvalue = (360 + image.rotation + form.angle) % 360;
info!("Rotation was {}, setting to {}", image.rotation, newvalue); info!("Rotation was {}, setting to {}", image.rotation, newvalue);
image.rotation = newvalue; image.rotation = newvalue;
match image.save_changes::<Photo>(c) { match image.save_changes::<Photo>(c) {
Ok(image) => { Ok(image) => {
req.clear_cache(&image.cache_key(SizeTag::Small)); context.clear_cache(&image.cache_key(SizeTag::Small));
req.clear_cache(&image.cache_key(SizeTag::Medium)); context.clear_cache(&image.cache_key(SizeTag::Medium));
return res.ok(|o| writeln!(o, "ok")); return Response::builder().body(b"ok".to_vec()).unwrap();
} }
Err(error) => { Err(error) => {
warn!("Failed to save image #{}: {}", image.id, error); warn!("Failed to save image #{}: {}", image.id, error);
} }
} }
} }
} not_found(&context)
info!("Missing image and/or angle to rotate or image not found");
res.not_found("")
} }
type QResult<T> = Result<T, (StatusCode, BodyError)>; #[derive(Deserialize)]
struct RotateForm {
fn rotate_params(req: &mut Request) -> QResult<(Option<i32>, Option<i16>)> { image: i32,
let data = req.form_body()?; angle: i16,
Ok((
data.get("image").and_then(|s| s.parse().ok()),
data.get("angle").and_then(|s| s.parse().ok()),
))
} }
pub fn set_tag<'mw>( fn set_tag(context: Context, form: TagForm) -> Response<Vec<u8>> {
req: &mut Request, if !context.is_authorized() {
res: Response<'mw>, return permission_denied();
) -> MiddlewareResult<'mw> {
if req.authorized_user().is_none() {
return res.error(StatusCode::Unauthorized, "permission denied");
} }
if let (Some(image), Some(tag)) = try_with!(res, tag_params(req)) { let c = context.db();
let c: &PgConnection = &req.db_conn();
use crate::models::{PhotoTag, Tag}; use crate::models::{PhotoTag, Tag};
use diesel; use diesel;
let tag = { let tag = {
use crate::schema::tags::dsl::*; use crate::schema::tags::dsl::*;
tags.filter(tag_name.ilike(&tag)) tags.filter(tag_name.ilike(&form.tag))
.first::<Tag>(c) .first::<Tag>(c)
.or_else(|_| { .or_else(|_| {
diesel::insert_into(tags) diesel::insert_into(tags)
.values((tag_name.eq(&tag), slug.eq(&slugify(&tag)))) .values((
tag_name.eq(&form.tag),
slug.eq(&slugify(&form.tag)),
))
.get_result::<Tag>(c) .get_result::<Tag>(c)
}) })
.expect("Find or create tag") .expect("Find or create tag")
}; };
use crate::schema::photo_tags::dsl::*; use crate::schema::photo_tags::dsl::*;
let q = photo_tags let q = photo_tags
.filter(photo_id.eq(image)) .filter(photo_id.eq(form.image))
.filter(tag_id.eq(tag.id)); .filter(tag_id.eq(tag.id));
if q.first::<PhotoTag>(c).is_ok() { if q.first::<PhotoTag>(c).is_ok() {
info!("Photo #{} already has {:?}", image, tag); info!("Photo #{} already has {:?}", form.image, form.tag);
} else { } else {
info!("Add {:?} on photo #{}!", tag, image); info!("Add {:?} on photo #{}!", form.tag, form.image);
diesel::insert_into(photo_tags) diesel::insert_into(photo_tags)
.values((photo_id.eq(image), tag_id.eq(tag.id))) .values((photo_id.eq(form.image), tag_id.eq(tag.id)))
.execute(c) .execute(c)
.expect("Tag a photo"); .expect("Tag a photo");
} }
return res.redirect(format!("/img/{}", image)); redirect_to_img(form.image)
}
info!("Missing image and/or angle to rotate or image not found");
res.not_found("")
} }
fn tag_params(req: &mut Request) -> QResult<(Option<i32>, Option<String>)> { #[derive(Deserialize)]
let data = req.form_body()?; struct TagForm {
Ok(( image: i32,
data.get("image").and_then(|s| s.parse().ok()), tag: String,
data.get("tag").map(String::from),
))
} }
pub fn set_person<'mw>( fn set_person(context: Context, form: PersonForm) -> Response<Vec<u8>> {
req: &mut Request, if !context.is_authorized() {
res: Response<'mw>, return permission_denied();
) -> MiddlewareResult<'mw> {
if req.authorized_user().is_none() {
return res.error(StatusCode::Unauthorized, "permission denied");
} }
if let (Some(image), Some(name)) = try_with!(res, person_params(req)) { let c = context.db();
let c: &PgConnection = &req.db_conn();
use crate::models::{Person, PhotoPerson}; use crate::models::{Person, PhotoPerson};
use diesel; use diesel;
let person = { let person = {
use crate::schema::people::dsl::*; use crate::schema::people::dsl::*;
people people
.filter(person_name.ilike(&name)) .filter(person_name.ilike(&form.person))
.first::<Person>(c) .first::<Person>(c)
.or_else(|_| { .or_else(|_| {
diesel::insert_into(people) diesel::insert_into(people)
.values(( .values((
person_name.eq(&name), person_name.eq(&form.person),
slug.eq(&slugify(&name)), slug.eq(&slugify(&form.person)),
)) ))
.get_result::<Person>(c) .get_result::<Person>(c)
}) })
@ -131,87 +125,74 @@ pub fn set_person<'mw>(
}; };
use crate::schema::photo_people::dsl::*; use crate::schema::photo_people::dsl::*;
let q = photo_people let q = photo_people
.filter(photo_id.eq(image)) .filter(photo_id.eq(form.image))
.filter(person_id.eq(person.id)); .filter(person_id.eq(person.id));
if q.first::<PhotoPerson>(c).is_ok() { if q.first::<PhotoPerson>(c).is_ok() {
info!("Photo #{} already has {:?}", image, person); info!("Photo #{} already has {:?}", form.image, person);
} else { } else {
info!("Add {:?} on photo #{}!", person, image); info!("Add {:?} on photo #{}!", person, form.image);
diesel::insert_into(photo_people) diesel::insert_into(photo_people)
.values((photo_id.eq(image), person_id.eq(person.id))) .values((photo_id.eq(form.image), person_id.eq(person.id)))
.execute(c) .execute(c)
.expect("Name person in photo"); .expect("Name person in photo");
} }
return res.redirect(format!("/img/{}", image)); redirect_to_img(form.image)
}
info!("Missing image and/or angle to rotate or image not found");
res.not_found("")
} }
fn person_params(req: &mut Request) -> QResult<(Option<i32>, Option<String>)> { #[derive(Deserialize)]
let data = req.form_body()?; struct PersonForm {
Ok(( image: i32,
data.get("image").and_then(|s| s.parse().ok()), person: String,
data.get("person").map(String::from),
))
} }
pub fn set_grade<'mw>( fn set_grade(context: Context, form: GradeForm) -> Response<Vec<u8>> {
req: &mut Request, if !context.is_authorized() {
res: Response<'mw>, return permission_denied();
) -> MiddlewareResult<'mw> {
if req.authorized_user().is_none() {
return res.error(StatusCode::Unauthorized, "permission denied");
} }
if let (Some(image), Some(newgrade)) = try_with!(res, grade_params(req)) { if form.grade >= 0 && form.grade <= 100 {
if newgrade >= 0 && newgrade <= 100 { info!("Should set grade of #{} to {}", form.image, form.grade);
info!("Should set grade of #{} to {}", image, newgrade);
use crate::schema::photos::dsl::{grade, photos}; use crate::schema::photos::dsl::{grade, photos};
use diesel; let q =
let c: &PgConnection = &req.db_conn(); diesel::update(photos.find(form.image)).set(grade.eq(form.grade));
let q = diesel::update(photos.find(image)).set(grade.eq(newgrade)); match q.execute(context.db()) {
match q.execute(c) {
Ok(1) => { Ok(1) => {
return res.redirect(format!("/img/{}", image)); return redirect_to_img(form.image);
} }
Ok(0) => (), Ok(0) => (),
Ok(n) => { Ok(n) => {
warn!("Strange, updated {} images with id {}", n, image); warn!("Strange, updated {} images with id {}", n, form.image);
} }
Err(error) => { Err(error) => {
warn!("Failed set grade of image #{}: {}", image, error); warn!("Failed set grade of image #{}: {}", form.image, error);
} }
} }
} else { } else {
info!("Grade {} is out of range for image #{}", newgrade, image); info!(
"Grade {} out of range for image #{}",
form.grade, form.image
);
} }
} not_found(&context)
info!("Missing image and/or angle to rotate or image not found");
res.not_found("")
} }
fn grade_params(req: &mut Request) -> QResult<(Option<i32>, Option<i16>)> { #[derive(Deserialize)]
let data = req.form_body()?; struct GradeForm {
Ok(( image: i32,
data.get("image").and_then(|s| s.parse().ok()), grade: i16,
data.get("grade").and_then(|s| s.parse().ok()),
))
} }
pub fn set_location<'mw>( fn set_location(context: Context, form: CoordForm) -> Response<Vec<u8>> {
req: &mut Request, if !context.is_authorized() {
res: Response<'mw>, return permission_denied();
) -> MiddlewareResult<'mw> {
if req.authorized_user().is_none() {
return res.error(StatusCode::Unauthorized, "permission denied");
} }
if let (Some(image), Some(coord)) = try_with!(res, location_params(req)) { let image = form.image;
let coord = form.coord();
info!("Should set location of #{} to {:?}.", image, coord); info!("Should set location of #{} to {:?}.", image, coord);
let (lat, lng) = ((coord.x * 1e6) as i32, (coord.y * 1e6) as i32); let (lat, lng) = ((coord.x * 1e6) as i32, (coord.y * 1e6) as i32);
use crate::schema::positions::dsl::*; use crate::schema::positions::dsl::*;
use diesel::insert_into; use diesel::insert_into;
let db: &PgConnection = &req.db_conn(); let db = context.db();
insert_into(positions) insert_into(positions)
.values((photo_id.eq(image), latitude.eq(lat), longitude.eq(lng))) .values((photo_id.eq(image), latitude.eq(lat), longitude.eq(lng)))
.on_conflict(photo_id) .on_conflict(photo_id)
@ -220,30 +201,26 @@ pub fn set_location<'mw>(
.execute(db) .execute(db)
.expect("Insert image position"); .expect("Insert image position");
match update_image_places(db, image) { match update_image_places(db, form.image) {
Ok(()) => (), Ok(()) => (),
// TODO Tell the user something failed? // TODO Tell the user something failed?
Err(err) => warn!("Failed to fetch places: {:?}", err), Err(err) => warn!("Failed to fetch places: {:?}", err),
} }
return res.redirect(format!("/img/{}", image)); redirect_to_img(form.image)
}
info!("Missing image and/or position to set, or image not found.");
res.not_found("")
} }
fn location_params( #[derive(Deserialize)]
req: &mut Request, struct CoordForm {
) -> QResult<(Option<i32>, Option<Coord>)> { image: i32,
let data = req.form_body()?; lat: f64,
Ok(( lng: f64,
data.get("image").and_then(|s| s.parse().ok()), }
if let (Some(lat), Some(lng)) = (
data.get("lat").and_then(|s| s.parse().ok()), impl CoordForm {
data.get("lng").and_then(|s| s.parse().ok()), fn coord(&self) -> Coord {
) { Coord {
Some(Coord { x: lat, y: lng }) x: self.lat,
} else { y: self.lng,
None }
}, }
))
} }

227
src/server/context.rs Normal file
View File

@ -0,0 +1,227 @@
use crate::env::photos_dir;
use crate::photosdir::PhotosDir;
use crypto::sha2::Sha256;
use diesel::pg::PgConnection;
use diesel::r2d2::{ConnectionManager, Pool, PooledConnection};
use jwt::{Claims, Header, Registered, Token};
use log::{debug, error, warn};
use memcached::proto::{Error as MprotError, Operation, ProtoType};
use memcached::Client;
use std::collections::BTreeMap;
use std::error::Error;
use std::io;
use std::sync::Arc;
use std::time::Duration;
use warp::filters::{cookie, BoxedFilter};
use warp::path::{self, FullPath};
use warp::reject::custom;
use warp::{self, Filter};
type PooledPg = PooledConnection<ConnectionManager<PgConnection>>;
type PgPool = Pool<ConnectionManager<PgConnection>>;
pub fn create_session_filter(
db_url: &str,
memcached_server: String,
jwt_secret: String,
) -> BoxedFilter<(Context,)> {
let global =
Arc::new(GlobalContext::new(db_url, memcached_server, jwt_secret));
warp::any()
.and(path::full())
.and(cookie::optional("EXAUTH"))
.and_then(move |path, key: Option<String>| {
let user = key.and_then(|k| {
global
.verify_key(&k)
.map_err(|e| warn!("Auth failed: {}", e))
.ok()
});
Context::new(global.clone(), path, user).map_err(|e| {
error!("Failed to initialize session: {}", e);
custom(e)
})
})
.boxed()
}
struct GlobalContext {
db_pool: PgPool,
photosdir: PhotosDir,
memcached_server: String, // TODO: Use a connection pool!
jwt_secret: String,
}
impl GlobalContext {
fn new(
db_url: &str,
memcached_server: String,
jwt_secret: String,
) -> Self {
let db_manager = ConnectionManager::<PgConnection>::new(db_url);
GlobalContext {
db_pool: Pool::new(db_manager)
.expect("Postgres connection pool could not be created"),
photosdir: PhotosDir::new(photos_dir()),
memcached_server,
jwt_secret,
}
}
fn verify_key(&self, jwtstr: &str) -> Result<String, String> {
let token = Token::<Header, Claims>::parse(&jwtstr)
.map_err(|e| format!("Bad jwt token: {:?}", e))?;
if token.verify(self.jwt_secret.as_ref(), Sha256::new()) {
let claims = token.claims;
debug!("Verified token for: {:?}", claims);
let now = current_numeric_date();
if let Some(nbf) = claims.reg.nbf {
if now < nbf {
return Err(format!(
"Not-yet valid token, {} < {}",
now, nbf,
));
}
}
if let Some(exp) = claims.reg.exp {
if now > exp {
return Err(format!(
"Got an expired token: {} > {}",
now, exp,
));
}
}
if let Some(user) = claims.reg.sub {
return Ok(user);
} else {
return Err("User missing in claims".to_string());
}
} else {
Err(format!("Invalid token {:?}", token))
}
}
fn cache(&self) -> Result<Client, io::Error> {
Client::connect(&[(&self.memcached_server, 1)], ProtoType::Binary)
}
}
/// The request context, providing database, memcache and authorized user.
pub struct Context {
global: Arc<GlobalContext>,
db: PooledPg,
path: FullPath,
user: Option<String>,
}
impl Context {
fn new(
global: Arc<GlobalContext>,
path: FullPath,
user: Option<String>,
) -> Result<Self, String> {
let db = global
.db_pool
.get()
.map_err(|e| format!("Failed to get db {}", e))?;
Ok(Context {
global,
db,
path,
user,
})
}
pub fn db(&self) -> &PgConnection {
&self.db
}
pub fn authorized_user(&self) -> Option<&str> {
self.user.as_ref().map(AsRef::as_ref)
}
pub fn is_authorized(&self) -> bool {
self.user.is_some()
}
pub fn path_without_query(&self) -> &str {
self.path.as_str()
}
pub fn cached_or<F, E>(
&self,
key: &str,
calculate: F,
) -> Result<Vec<u8>, E>
where
F: FnOnce() -> Result<Vec<u8>, E>,
{
match self.global.cache() {
Ok(mut client) => {
match client.get(key.as_bytes()) {
Ok((data, _flags)) => {
debug!("Cache: {} found", key);
return Ok(data);
}
Err(MprotError::BinaryProtoError(ref err))
if err.description() == "key not found" =>
{
debug!("Cache: {} not found", key);
}
Err(err) => {
warn!("Cache: get {} failed: {:?}", key, err);
}
}
let data = calculate()?;
match client.set(key.as_bytes(), &data, 0, 7 * 24 * 60 * 60) {
Ok(()) => debug!("Cache: stored {}", key),
Err(err) => warn!("Cache: Error storing {}: {}", key, err),
}
Ok(data)
}
Err(err) => {
warn!("Error connecting to memcached: {}", err);
calculate()
}
}
}
pub fn clear_cache(&self, key: &str) {
if let Ok(mut client) = self.global.cache() {
match client.delete(key.as_bytes()) {
Ok(()) => debug!("Cache: deleted {}", key),
Err(e) => warn!("Cache: Failed to delete {}: {}", key, e),
}
}
}
pub fn photos(&self) -> &PhotosDir {
&self.global.photosdir
}
pub fn make_token(&self, user: &str) -> Option<String> {
let header: Header = Default::default();
let now = current_numeric_date();
let expiration_time = Duration::from_secs(14 * 24 * 60 * 60);
let claims = Claims {
reg: Registered {
iss: None, // TODO?
sub: Some(user.into()),
exp: Some(now + expiration_time.as_secs()),
nbf: Some(now),
..Default::default()
},
private: BTreeMap::new(),
};
let token = Token::new(header, claims);
token
.signed(self.global.jwt_secret.as_ref(), Sha256::new())
.ok()
}
}
/// Get the current value for jwt NumericDate.
///
/// Defined in RFC 7519 section 2 to be equivalent to POSIX.1 "Seconds
/// Since the Epoch". The RFC allows a NumericDate to be non-integer
/// (for sub-second resolution), but the jwt crate uses u64.
fn current_numeric_date() -> u64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
}

View File

@ -1,43 +1,44 @@
#[macro_use] #[macro_use]
mod nickelext;
mod admin; mod admin;
mod context;
mod render_ructe;
mod splitlist; mod splitlist;
mod views_by_date; mod views_by_date;
use self::nickelext::{far_expires, FromSlug, MyResponse}; use self::context::create_session_filter;
pub use self::context::Context;
use self::render_ructe::RenderRucte;
use self::splitlist::*; use self::splitlist::*;
use self::views_by_date::*; use self::views_by_date::*;
use crate::adm::result::Error; use crate::adm::result::Error;
use crate::env::{dburl, env_or, jwt_key, photos_dir}; use crate::env::{dburl, env_or, jwt_key};
use crate::memcachemiddleware::{
MemcacheMiddleware, MemcacheRequestExtensions,
};
use crate::models::{Person, Photo, Place, Tag}; use crate::models::{Person, Photo, Place, Tag};
use crate::nickel_diesel::{DieselMiddleware, DieselRequestExtensions};
use crate::photosdirmiddleware::{
PhotosDirMiddleware, PhotosDirRequestExtensions,
};
use crate::pidfiles::handle_pid_file; use crate::pidfiles::handle_pid_file;
use crate::requestloggermiddleware::RequestLoggerMiddleware; use crate::templates::{self, Html};
use crate::templates::{self, statics, Html}; use chrono::{Datelike, Duration, Utc};
use chrono::Datelike;
use clap::ArgMatches; use clap::ArgMatches;
use diesel::pg::PgConnection;
use diesel::prelude::*; use diesel::prelude::*;
use diesel::r2d2::NopErrorHandler;
use djangohashers; use djangohashers;
use hyper::header::ContentType;
use image; use image;
use log::{debug, info}; use log::info;
use nickel::extensions::response::Redirect; use mime;
use nickel::status::StatusCode; use serde::Deserialize;
use nickel::{ use std::net::SocketAddr;
Action, Continue, FormBody, Halt, HttpRouter, MediaType, MiddlewareResult, use warp::filters::path::Tail;
Nickel, NickelError, QueryString, Request, Response, use warp::http::{header, Response, StatusCode};
}; use warp::{self, reply, Filter, Rejection, Reply};
use nickel_jwt_session::{
SessionMiddleware, SessionRequestExtensions, SessionResponseExtensions, /// Trait to easily add a far expires header to a response builder.
}; trait FarExpires {
fn far_expires(&mut self) -> &mut Self;
}
impl FarExpires for warp::http::response::Builder {
fn far_expires(&mut self) -> &mut Self {
let far_expires = Utc::now() + Duration::days(180);
self.header(header::EXPIRES, far_expires.to_rfc2822())
}
}
pub struct PhotoLink { pub struct PhotoLink {
pub title: Option<String>, pub title: Option<String>,
@ -119,114 +120,166 @@ pub fn run(args: &ArgMatches) -> Result<(), Error> {
if let Some(pidfile) = args.value_of("PIDFILE") { if let Some(pidfile) = args.value_of("PIDFILE") {
handle_pid_file(pidfile, args.is_present("REPLACE")).unwrap() handle_pid_file(pidfile, args.is_present("REPLACE")).unwrap()
} }
let session_filter = create_session_filter(
let mut server = Nickel::new(); &dburl(),
server.utilize(RequestLoggerMiddleware); env_or("MEMCACHED_SERVER", "tcp://127.0.0.1:11211"),
wrap3!(server.get "/static/",.. static_file); jwt_key(),
server.utilize(MemcacheMiddleware::new(vec![(
"tcp://127.0.0.1:11211".into(),
1,
)]));
server.utilize(SessionMiddleware::new(&jwt_key()));
let dm: DieselMiddleware<PgConnection> =
DieselMiddleware::new(&dburl(), 5, Box::new(NopErrorHandler)).unwrap();
server.utilize(dm);
server.utilize(PhotosDirMiddleware::new(photos_dir()));
wrap3!(server.get "/login", login);
wrap3!(server.post "/login", do_login);
wrap3!(server.get "/logout", logout);
wrap3!(server.get "/", all_years);
use self::admin::{rotate, set_grade, set_location, set_person, set_tag};
wrap3!(server.get "/ac/tag", auto_complete_tag);
wrap3!(server.get "/ac/person", auto_complete_person);
wrap3!(server.post "/adm/grade", set_grade);
wrap3!(server.post "/adm/person", set_person);
wrap3!(server.post "/adm/rotate", rotate);
wrap3!(server.post "/adm/tag", set_tag);
wrap3!(server.post "/adm/locate", set_location);
wrap3!(server.get "/img/{}[-]{}\\.jpg", show_image: id, size);
wrap3!(server.get "/img/{}", photo_details: id);
wrap3!(server.get "/next", next_image);
wrap3!(server.get "/prev", prev_image);
wrap3!(server.get "/tag/", tag_all);
wrap3!(server.get "/tag/{}", tag_one: tag);
wrap3!(server.get "/place/", place_all);
wrap3!(server.get "/place/{}", place_one: slug);
wrap3!(server.get "/person/", person_all);
wrap3!(server.get "/person/{}", person_one: slug);
wrap3!(server.get "/random", random_image);
wrap3!(server.get "/0/", all_null_date);
wrap3!(server.get "/{}/", months_in_year: year);
wrap3!(server.get "/{}/{}/", days_in_month: year, month);
wrap3!(server.get "/{}/{}/{}", all_for_day: year, month, day);
wrap3!(server.get "/thisday", on_this_day);
server.handle_error(
custom_errors as fn(&mut NickelError, &mut Request) -> Action,
); );
let s = move || session_filter.clone();
server use warp::filters::query::query;
.listen(&*env_or("RPHOTOS_LISTEN", "127.0.0.1:6767")) use warp::path::{end, param};
.map_err(|e| Error::Other(format!("listen: {}", e)))?; use warp::{body, get2 as get, path, post2 as post};
let static_routes = path("static")
.and(get())
.and(path::tail())
.and_then(static_file);
#[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(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("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))
.or(get().and(param()).and(param()).and(param()).and(end()).and(query()).and(s()).map(all_for_day))
.or(get().and(path("person")).and(end()).and(s()).map(person_all))
.or(get().and(path("person")).and(s()).and(param()).and(end()).and(query()).map(person_one))
.or(get().and(path("place")).and(end()).and(s()).map(place_all))
.or(get().and(path("place")).and(s()).and(param()).and(end()).and(query()).map(place_one))
.or(get().and(path("tag")).and(end()).and(s()).map(tag_all))
.or(get().and(path("tag")).and(s()).and(param()).and(end()).and(query()).map(tag_one))
.or(get().and(path("random")).and(end()).and(s()).map(random_image))
.or(get().and(path("thisday")).and(end()).and(s()).map(on_this_day))
.or(get().and(path("next")).and(end()).and(s()).and(query()).map(next_image))
.or(get().and(path("prev")).and(end()).and(s()).and(query()).map(prev_image))
.or(get().and(path("ac")).and(path("tag")).and(s()).and(query()).map(auto_complete_tag))
.or(get().and(path("ac")).and(path("person")).and(s()).and(query()).map(auto_complete_person))
.or(path("adm").and(admin::routes(s())));
let addr = env_or("RPHOTOS_LISTEN", "127.0.0.1:6767")
.parse::<SocketAddr>()
.map_err(|e| Error::Other(format!("{}", e)))?;
warp::serve(routes.recover(customize_error)).run(addr);
Ok(()) Ok(())
} }
fn custom_errors(err: &mut NickelError, req: &mut Request) -> Action { /// Create custom error pages.
if let Some(ref mut res) = err.stream { fn customize_error(err: Rejection) -> Result<impl Reply, Rejection> {
if res.status() == StatusCode::NotFound { match err.status() {
templates::not_found(res, req).unwrap(); StatusCode::NOT_FOUND => {
return Halt(()); eprintln!("Got a 404: {:?}", err);
Ok(Response::builder().status(StatusCode::NOT_FOUND).html(|o| {
templates::error(
o,
StatusCode::NOT_FOUND,
"The resource you requested could not be located.",
)
}))
}
code => {
eprintln!("Got a {}: {:?}", code.as_u16(), err);
Ok(Response::builder()
.status(code)
.html(|o| templates::error(o, code, "Something went wrong.")))
} }
} }
Continue(())
} }
fn login<'mw>( fn not_found(context: &Context) -> Response<Vec<u8>> {
req: &mut Request, Response::builder().status(StatusCode::NOT_FOUND).html(|o| {
mut res: Response<'mw>, templates::not_found(
) -> MiddlewareResult<'mw> { o,
res.clear_jwt(); context,
let next = sanitize_next(req.query().get("next")).map(String::from); StatusCode::NOT_FOUND,
res.ok(|o| templates::login(o, req, next, None)) "The resource you requested could not be located.",
)
})
} }
fn do_login<'mw>( fn redirect_to_img(image: i32) -> Response<Vec<u8>> {
req: &mut Request, redirect(&format!("/img/{}", image))
mut res: Response<'mw>, }
) -> MiddlewareResult<'mw> {
fn redirect(url: &str) -> Response<Vec<u8>> {
Response::builder()
.status(StatusCode::FOUND)
.header(header::LOCATION, url)
.body(format!("Please refer to {}", url).into_bytes())
.unwrap()
}
fn permission_denied() -> Response<Vec<u8>> {
error_response(StatusCode::UNAUTHORIZED)
}
fn error_response(err: StatusCode) -> Response<Vec<u8>> {
Response::builder()
.status(err)
.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))
.map(String::from);
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 = { let next = {
let c: &PgConnection = &req.db_conn(); let next = sanitize_next(form.next.as_ref().map(AsRef::as_ref))
let form_data = try_with!(res, req.form_body()); .map(String::from);
let next = sanitize_next(form_data.get("next")).map(String::from);
if let (Some(user), Some(pw)) =
(form_data.get("user"), form_data.get("password"))
{
use crate::schema::users::dsl::*; use crate::schema::users::dsl::*;
if let Ok(hash) = users if let Ok(hash) = users
.filter(username.eq(user)) .filter(username.eq(&form.user))
.select(password) .select(password)
.first::<String>(c) .first::<String>(context.db())
{ {
debug!("Hash for {} is {}", user, hash); if djangohashers::check_password_tolerant(&form.password, &hash) {
if djangohashers::check_password_tolerant(pw, &hash) { info!("User {} logged in", form.user);
info!("User {} logged in", user); let token = context.make_token(&form.user).unwrap();
res.set_jwt_user(user); let url = next.unwrap_or_else(|| "/".into());
return res.redirect(next.unwrap_or_else(|| "/".into())); return Response::builder()
.status(StatusCode::FOUND)
.header(header::LOCATION, url.clone())
.header(
header::SET_COOKIE,
format!(
"EXAUTH={}; SameSite=Strict; HttpOpnly",
token
),
)
.body(format!("Please refer to {}", url).into_bytes())
.unwrap();
} }
info!( info!(
"Login failed: Password verification failed for {:?}", "Login failed: Password verification failed for {:?}",
user, form.user,
); );
} else { } else {
info!("Login failed: No hash found for {:?}", user); info!("Login failed: No hash found for {:?}", form.user);
}
} }
next next
}; };
let message = Some("Login failed, please try again"); let message = Some("Login failed, please try again");
res.ok(|o| templates::login(o, req, next, message)) 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> { fn sanitize_next(next: Option<&str>) -> Option<&str> {
@ -267,12 +320,17 @@ fn test_sanitize_good_2() {
assert_eq!(Some("/2017/7/15"), sanitize_next(Some("/2017/7/15"))) assert_eq!(Some("/2017/7/15"), sanitize_next(Some("/2017/7/15")))
} }
fn logout<'mw>( fn logout(_context: Context) -> Response<Vec<u8>> {
_req: &mut Request, let url = "/";
mut res: Response<'mw>, Response::builder()
) -> MiddlewareResult<'mw> { .status(StatusCode::FOUND)
res.clear_jwt(); .header(header::LOCATION, url.to_string())
res.redirect("/") .header(
header::SET_COOKIE,
"EXAUTH=; Max-Age=0; SameSite=Strict; HttpOpnly".to_string(),
)
.body(format!("Please refer to {}", url).into_bytes())
.unwrap()
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
@ -291,63 +349,114 @@ impl SizeTag {
} }
} }
impl FromSlug for SizeTag { fn show_image(img: ImgName, context: Context) -> Response<Vec<u8>> {
fn parse(slug: &str) -> Option<Self> { use crate::schema::photos::dsl::photos;
match slug { if let Ok(tphoto) = photos.find(img.id).first::<Photo>(context.db()) {
"s" => Some(SizeTag::Small), if context.is_authorized() || tphoto.is_public() {
"m" => Some(SizeTag::Medium), if img.size == SizeTag::Large {
"l" => Some(SizeTag::Large), if context.is_authorized() {
_ => None, 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 {})
}
} }
fn show_image<'mw>( #[test]
req: &Request, fn parse_good_imgname() {
mut res: Response<'mw>, assert_eq!(
the_id: i32, "4711-s.jpg".parse(),
size: SizeTag, Ok(ImgName {
) -> MiddlewareResult<'mw> { id: 4711,
use crate::schema::photos::dsl::photos; size: SizeTag::Small,
let c: &PgConnection = &req.db_conn(); })
if let Ok(tphoto) = photos.find(the_id).first::<Photo>(c) { )
if req.authorized_user().is_some() || tphoto.is_public() { }
if size == SizeTag::Large {
if req.authorized_user().is_some() { #[test]
let path = req.photos().get_raw_path(tphoto); fn parse_bad_imgname_1() {
res.set((MediaType::Jpeg, far_expires())); assert_eq!("4711-q.jpg".parse::<ImgName>(), Err(BadImgName {}))
return res.send_file(path); }
} #[test]
} else { fn parse_bad_imgname_2() {
let data = get_image_data(req, &tphoto, size) assert_eq!("blurgel".parse::<ImgName>(), Err(BadImgName {}))
.expect("Get image data");
res.set((MediaType::Jpeg, far_expires()));
return res.send(data);
}
}
}
res.not_found("No such image")
} }
fn get_image_data( fn get_image_data(
req: &Request, context: Context,
photo: &Photo, photo: &Photo,
size: SizeTag, size: SizeTag,
) -> Result<Vec<u8>, image::ImageError> { ) -> Result<Vec<u8>, image::ImageError> {
req.cached_or(&photo.cache_key(size), || { context.cached_or(&photo.cache_key(size), || {
let size = size.px(); let size = size.px();
req.photos().scale_image(photo, size, size) context.photos().scale_image(photo, size, size)
}) })
} }
fn tag_all<'mw>( fn tag_all(context: Context) -> Response<Vec<u8>> {
req: &mut Request,
res: Response<'mw>,
) -> MiddlewareResult<'mw> {
use crate::schema::tags::dsl::{id, tag_name, tags}; use crate::schema::tags::dsl::{id, tag_name, tags};
let c: &PgConnection = &req.db_conn();
let query = tags.into_boxed(); let query = tags.into_boxed();
let query = if req.authorized_user().is_some() { let query = if context.is_authorized() {
query query
} else { } else {
use crate::schema::photo_tags::dsl as tp; use crate::schema::photo_tags::dsl as tp;
@ -356,41 +465,39 @@ fn tag_all<'mw>(
tp::photo_id.eq_any(p::photos.select(p::id).filter(p::is_public)), tp::photo_id.eq_any(p::photos.select(p::id).filter(p::is_public)),
))) )))
}; };
res.ok(|o| { Response::builder().html(|o| {
templates::tags( templates::tags(
o, o,
req, &context,
&query.order(tag_name).load(c).expect("List tags"), &query.order(tag_name).load(context.db()).expect("List tags"),
) )
}) })
} }
fn tag_one<'mw>( fn tag_one(
req: &mut Request, context: Context,
res: Response<'mw>,
tslug: String, tslug: String,
) -> MiddlewareResult<'mw> { range: ImgRange,
) -> Response<Vec<u8>> {
use crate::schema::tags::dsl::{slug, tags}; use crate::schema::tags::dsl::{slug, tags};
let c: &PgConnection = &req.db_conn(); if let Ok(tag) = tags.filter(slug.eq(tslug)).first::<Tag>(context.db()) {
if let Ok(tag) = tags.filter(slug.eq(tslug)).first::<Tag>(c) {
use crate::schema::photo_tags::dsl::{photo_id, photo_tags, tag_id}; use crate::schema::photo_tags::dsl::{photo_id, photo_tags, tag_id};
use crate::schema::photos::dsl::id; use crate::schema::photos::dsl::id;
let photos = Photo::query(req.authorized_user().is_some()).filter( let photos = Photo::query(context.is_authorized()).filter(
id.eq_any(photo_tags.select(photo_id).filter(tag_id.eq(tag.id))), id.eq_any(photo_tags.select(photo_id).filter(tag_id.eq(tag.id))),
); );
let (links, coords) = links_by_time(req, photos); let (links, coords) = links_by_time(&context, photos, range);
return res.ok(|o| templates::tag(o, req, &links, &coords, &tag)); Response::builder()
.html(|o| templates::tag(o, &context, &links, &coords, &tag))
} else {
not_found(&context)
} }
res.not_found("Not a tag")
} }
fn place_all<'mw>( fn place_all(context: Context) -> Response<Vec<u8>> {
req: &mut Request,
res: Response<'mw>,
) -> MiddlewareResult<'mw> {
use crate::schema::places::dsl::{id, place_name, places}; use crate::schema::places::dsl::{id, place_name, places};
let query = places.into_boxed(); let query = places.into_boxed();
let query = if req.authorized_user().is_some() { let query = if context.is_authorized() {
query query
} else { } else {
use crate::schema::photo_places::dsl as pp; use crate::schema::photo_places::dsl as pp;
@ -399,57 +506,63 @@ fn place_all<'mw>(
pp::photo_id.eq_any(p::photos.select(p::id).filter(p::is_public)), pp::photo_id.eq_any(p::photos.select(p::id).filter(p::is_public)),
))) )))
}; };
let c: &PgConnection = &req.db_conn(); Response::builder().html(|o| {
res.ok(|o| {
templates::places( templates::places(
o, o,
req, &context,
&query.order(place_name).load(c).expect("List places"), &query
.order(place_name)
.load(context.db())
.expect("List places"),
) )
}) })
} }
fn static_file<'mw>( /// Handler for static files.
_req: &Request, /// Create a response from the file data with a correct content type
mut res: Response<'mw>, /// and a far expires header (or a 404 if the file does not exist).
path: &str, fn static_file(name: Tail) -> Result<impl Reply, Rejection> {
) -> MiddlewareResult<'mw> { use templates::statics::StaticFile;
if let Some(s) = statics::StaticFile::get(path) { if let Some(data) = StaticFile::get(name.as_str()) {
res.set((ContentType(s.mime()), far_expires())); Ok(Response::builder()
return res.send(s.content); .status(StatusCode::OK)
.header(header::CONTENT_TYPE, data.mime.as_ref())
.far_expires()
.body(data.content))
} else {
println!("Static file {:?} not found", name);
Err(warp::reject::not_found())
} }
res.not_found("No such file")
} }
fn place_one<'mw>( fn place_one(
req: &mut Request, context: Context,
res: Response<'mw>,
tslug: String, tslug: String,
) -> MiddlewareResult<'mw> { range: ImgRange,
) -> Response<Vec<u8>> {
use crate::schema::places::dsl::{places, slug}; use crate::schema::places::dsl::{places, slug};
let c: &PgConnection = &req.db_conn(); if let Ok(place) =
if let Ok(place) = places.filter(slug.eq(tslug)).first::<Place>(c) { places.filter(slug.eq(tslug)).first::<Place>(context.db())
{
use crate::schema::photo_places::dsl::{ use crate::schema::photo_places::dsl::{
photo_id, photo_places, place_id, photo_id, photo_places, place_id,
}; };
use crate::schema::photos::dsl::id; use crate::schema::photos::dsl::id;
let photos = let photos = Photo::query(context.is_authorized()).filter(id.eq_any(
Photo::query(req.authorized_user().is_some()).filter(id.eq_any(
photo_places.select(photo_id).filter(place_id.eq(place.id)), photo_places.select(photo_id).filter(place_id.eq(place.id)),
)); ));
let (links, coord) = links_by_time(req, photos); let (links, coord) = links_by_time(&context, photos, range);
return res.ok(|o| templates::place(o, req, &links, &coord, &place)); Response::builder()
.html(|o| templates::place(o, &context, &links, &coord, &place))
} else {
not_found(&context)
} }
res.not_found("Not a place")
} }
fn person_all<'mw>( fn person_all(context: Context) -> Response<Vec<u8>> {
req: &mut Request,
res: Response<'mw>,
) -> MiddlewareResult<'mw> {
use crate::schema::people::dsl::{id, people, person_name}; use crate::schema::people::dsl::{id, people, person_name};
let query = people.into_boxed(); let query = people.into_boxed();
let query = if req.authorized_user().is_some() { let query = if context.is_authorized() {
query query
} else { } else {
use crate::schema::photo_people::dsl as pp; use crate::schema::photo_people::dsl as pp;
@ -458,73 +571,71 @@ fn person_all<'mw>(
pp::photo_id.eq_any(p::photos.select(p::id).filter(p::is_public)), pp::photo_id.eq_any(p::photos.select(p::id).filter(p::is_public)),
))) )))
}; };
let c: &PgConnection = &req.db_conn(); Response::builder().html(|o| {
res.ok(|o| {
templates::people( templates::people(
o, o,
req, &context,
&query.order(person_name).load(c).expect("list people"), &query
.order(person_name)
.load(context.db())
.expect("list people"),
) )
}) })
} }
fn person_one<'mw>( fn person_one(
req: &mut Request, context: Context,
res: Response<'mw>,
tslug: String, tslug: String,
) -> MiddlewareResult<'mw> { range: ImgRange,
) -> Response<Vec<u8>> {
use crate::schema::people::dsl::{people, slug}; use crate::schema::people::dsl::{people, slug};
let c: &PgConnection = &req.db_conn(); let c = context.db();
if let Ok(person) = people.filter(slug.eq(tslug)).first::<Person>(c) { if let Ok(person) = people.filter(slug.eq(tslug)).first::<Person>(c) {
use crate::schema::photo_people::dsl::{ use crate::schema::photo_people::dsl::{
person_id, photo_id, photo_people, person_id, photo_id, photo_people,
}; };
use crate::schema::photos::dsl::id; use crate::schema::photos::dsl::id;
let photos = Photo::query(req.authorized_user().is_some()).filter( let photos = Photo::query(context.is_authorized()).filter(
id.eq_any( id.eq_any(
photo_people photo_people
.select(photo_id) .select(photo_id)
.filter(person_id.eq(person.id)), .filter(person_id.eq(person.id)),
), ),
); );
let (links, coords) = links_by_time(req, photos); let (links, coords) = links_by_time(&context, photos, range);
res.ok(|o| templates::person(o, req, &links, &coords, &person)) Response::builder()
.html(|o| templates::person(o, &context, &links, &coords, &person))
} else { } else {
res.not_found("Not a person") not_found(&context)
} }
} }
fn random_image<'mw>( fn random_image(context: Context) -> Response<Vec<u8>> {
req: &mut Request,
res: Response<'mw>,
) -> MiddlewareResult<'mw> {
use crate::schema::photos::dsl::id; use crate::schema::photos::dsl::id;
use diesel::expression::dsl::sql; use diesel::expression::dsl::sql;
use diesel::sql_types::Integer; use diesel::sql_types::Integer;
let c: &PgConnection = &req.db_conn(); if let Ok(photo) = Photo::query(context.is_authorized())
let photo: i32 = Photo::query(req.authorized_user().is_some())
.select(id) .select(id)
.limit(1) .limit(1)
.order(sql::<Integer>("random()")) .order(sql::<Integer>("random()"))
.first(c) .first(context.db())
.unwrap(); {
info!("Random: {:?}", photo); info!("Random: {:?}", photo);
res.redirect(format!("/img/{}", photo)) // to photo_details redirect_to_img(photo)
} else {
not_found(&context)
}
} }
fn photo_details<'mw>( fn photo_details(id: i32, context: Context) -> Response<Vec<u8>> {
req: &mut Request,
res: Response<'mw>,
id: i32,
) -> MiddlewareResult<'mw> {
use crate::schema::photos::dsl::photos; use crate::schema::photos::dsl::photos;
let c: &PgConnection = &req.db_conn(); let c = context.db();
if let Ok(tphoto) = photos.find(id).first::<Photo>(c) { if let Ok(tphoto) = photos.find(id).first::<Photo>(c) {
if req.authorized_user().is_some() || tphoto.is_public() { if context.is_authorized() || tphoto.is_public() {
return res.ok(|o| { return Response::builder().html(|o| {
templates::details( templates::details(
o, o,
req, &context,
&tphoto &tphoto
.date .date
.map(|d| { .map(|d| {
@ -548,7 +659,7 @@ fn photo_details<'mw>(
}); });
} }
} }
res.not_found("Photo not found") not_found(&context)
} }
pub type Link = Html<String>; pub type Link = Html<String>;
@ -595,38 +706,33 @@ impl Link {
} }
} }
fn auto_complete_tag<'mw>( fn auto_complete_tag(context: Context, query: AcQ) -> impl Reply {
req: &mut Request,
res: Response<'mw>,
) -> MiddlewareResult<'mw> {
if let Some(q) = req.query().get("q").map(String::from) {
use crate::schema::tags::dsl::{tag_name, tags}; use crate::schema::tags::dsl::{tag_name, tags};
let c: &PgConnection = &req.db_conn();
let q = tags let q = tags
.select(tag_name) .select(tag_name)
.filter(tag_name.ilike(q + "%")) .filter(tag_name.ilike(query.q + "%"))
.order(tag_name) .order(tag_name)
.limit(10); .limit(10);
res.send(serde_json::to_string(&q.load::<String>(c).unwrap()).unwrap()) reply::json(&q.load::<String>(context.db()).unwrap())
} else {
res.error(StatusCode::BadRequest, "Missing 'q' parameter")
}
} }
fn auto_complete_person<'mw>( fn auto_complete_person(context: Context, query: AcQ) -> impl Reply {
req: &mut Request,
res: Response<'mw>,
) -> MiddlewareResult<'mw> {
if let Some(q) = req.query().get("q").map(String::from) {
use crate::schema::people::dsl::{people, person_name}; use crate::schema::people::dsl::{people, person_name};
let c: &PgConnection = &req.db_conn();
let q = people let q = people
.select(person_name) .select(person_name)
.filter(person_name.ilike(q + "%")) .filter(person_name.ilike(query.q + "%"))
.order(person_name) .order(person_name)
.limit(10); .limit(10);
res.send(serde_json::to_string(&q.load::<String>(c).unwrap()).unwrap()) reply::json(&q.load::<String>(context.db()).unwrap())
} else { }
res.error(StatusCode::BadRequest, "Missing 'q' parameter")
} #[derive(Deserialize)]
struct AcQ {
q: String,
}
#[derive(Debug, Default, Deserialize)]
pub struct ImgRange {
pub from: Option<i32>,
pub to: Option<i32>,
} }

View File

@ -1,122 +0,0 @@
//! A module of stuff that might evolve into improvements in nickel
//! itself.
//! Mainly, I'm experimenting with parsing url segments.
use hyper::header::{Expires, HttpDate};
use nickel::status::StatusCode;
use nickel::{Halt, MiddlewareResult, Response};
use std::io::{self, Write};
use time::{now, Duration};
macro_rules! wrap3 {
($server:ident.$method:ident $url:expr,
$handler:ident : $( $param:ident ),*) => {{
#[allow(unused_parens)]
fn wrapped<'mw>(req: &mut Request,
res: Response<'mw>)
-> MiddlewareResult<'mw> {
if let ($(Some($param),)*) =
($(req.param(stringify!($param)).and_then(FromSlug::parse),)*)
{
$handler(req, res, $($param),*)
} else {
res.not_found("Parameter mismatch")
}
}
let matcher = format!($url, $(concat!(":", stringify!($param))),+);
log::info!("Route {} {} to {}",
stringify!($method),
matcher,
stringify!($handler));
$server.$method(matcher, wrapped);
}};
($server:ident.$method:ident $url:expr,.. $handler:ident) => {{
#[allow(unused_parens)]
fn wrapped<'mw>(req: &mut Request,
res: Response<'mw>)
-> MiddlewareResult<'mw> {
if let Some(ref path) = req.path_without_query() {
$handler(req, res, &path[$url.len()..])
} else {
res.not_found("Path missing")
}
}
let matcher = format!("{}**", $url);
log::info!("Route {} {} to {}",
stringify!($method),
matcher,
stringify!($handler));
$server.$method(matcher, wrapped);
}};
($server:ident.$method:ident $url:expr, $handler:ident) => {
log::info!("Route {} {} to {}",
stringify!($method),
$url,
stringify!($handler));
$server.$method($url, $handler);
};
}
pub trait FromSlug: Sized {
fn parse(slug: &str) -> Option<Self>;
}
impl FromSlug for String {
fn parse(slug: &str) -> Option<Self> {
Some(slug.to_string())
}
}
impl FromSlug for i32 {
fn parse(slug: &str) -> Option<Self> {
slug.parse::<Self>().ok()
}
}
impl FromSlug for u8 {
fn parse(slug: &str) -> Option<Self> {
slug.parse::<Self>().ok()
}
}
impl FromSlug for u16 {
fn parse(slug: &str) -> Option<Self> {
slug.parse::<Self>().ok()
}
}
impl FromSlug for u32 {
fn parse(slug: &str) -> Option<Self> {
slug.parse::<Self>().ok()
}
}
impl FromSlug for usize {
fn parse(slug: &str) -> Option<Self> {
slug.parse::<Self>().ok()
}
}
pub trait MyResponse<'mw> {
fn ok<F>(self, do_render: F) -> MiddlewareResult<'mw>
where
F: FnOnce(&mut Write) -> io::Result<()>;
fn not_found(self, msg: &'static str) -> MiddlewareResult<'mw>;
}
impl<'mw> MyResponse<'mw> for Response<'mw> {
fn ok<F>(self, do_render: F) -> MiddlewareResult<'mw>
where
F: FnOnce(&mut Write) -> io::Result<()>,
{
let mut stream = self.start()?;
match do_render(&mut stream) {
Ok(()) => Ok(Halt(stream)),
Err(e) => {
stream.bail(format!("Error rendering template: {:?}", e))
}
}
}
fn not_found(self, msg: &'static str) -> MiddlewareResult<'mw> {
self.error(StatusCode::NotFound, msg)
}
}
pub fn far_expires() -> Expires {
Expires(HttpDate(now() + Duration::days(300)))
}

View File

@ -0,0 +1,26 @@
/// This module defines the `RenderRucte` trait for a response builer.
///
/// If ructe gets a warp feature, this is probably it.
use mime::TEXT_HTML_UTF_8;
use std::io::{self, Write};
use warp::http::response::Builder;
use warp::http::Response;
pub trait RenderRucte {
fn html<F>(&mut self, f: F) -> Response<Vec<u8>>
where
F: FnOnce(&mut Write) -> io::Result<()>;
}
impl RenderRucte for Builder {
fn html<F>(&mut self, f: F) -> Response<Vec<u8>>
where
F: FnOnce(&mut Write) -> io::Result<()>,
{
let mut buf = Vec::new();
f(&mut buf).unwrap();
self.header("content-type", TEXT_HTML_UTF_8.as_ref())
.body(buf)
.unwrap()
}
}

View File

@ -1,25 +1,25 @@
use super::views_by_date::query_date; use super::views_by_date::date_of_img;
use super::PhotoLink; use super::{Context, ImgRange, PhotoLink};
use crate::models::{Coord, Photo}; use crate::models::{Coord, Photo};
use crate::nickel_diesel::DieselRequestExtensions;
use crate::schema::photos; use crate::schema::photos;
use diesel::pg::{Pg, PgConnection}; use diesel::pg::{Pg, PgConnection};
use diesel::prelude::*; use diesel::prelude::*;
use log::{debug, info, warn}; use log::{debug, info, warn};
use nickel::Request;
pub fn links_by_time<'a>( pub fn links_by_time<'a>(
req: &mut Request, context: &Context,
photos: photos::BoxedQuery<'a, Pg>, photos: photos::BoxedQuery<'a, Pg>,
range: ImgRange,
) -> (Vec<PhotoLink>, Vec<(Coord, i32)>) { ) -> (Vec<PhotoLink>, Vec<(Coord, i32)>) {
let c: &PgConnection = &req.db_conn(); let c = context.db();
use crate::schema::photos::dsl::{date, id}; use crate::schema::photos::dsl::{date, id};
let photos = if let Some((_, from_date)) = query_date(req, "from") { let photos = if let Some(from_date) = range.from.map(|i| date_of_img(c, i))
{
photos.filter(date.ge(from_date)) photos.filter(date.ge(from_date))
} else { } else {
photos photos
}; };
let photos = if let Some((_, to_date)) = query_date(req, "to") { let photos = if let Some(to_date) = range.to.map(|i| date_of_img(c, i)) {
photos.filter(date.le(to_date)) photos.filter(date.le(to_date))
} else { } else {
photos photos
@ -30,7 +30,7 @@ pub fn links_by_time<'a>(
.unwrap(); .unwrap();
( (
if let Some(groups) = split_to_groups(&photos) { if let Some(groups) = split_to_groups(&photos) {
let path = req.path_without_query().unwrap_or("/"); let path = context.path_without_query();
groups groups
.iter() .iter()
.map(|g| PhotoLink::for_group(g, path)) .map(|g| PhotoLink::for_group(g, path))

View File

@ -1,39 +1,35 @@
use super::nickelext::MyResponse; use super::render_ructe::RenderRucte;
use super::splitlist::links_by_time; use super::splitlist::links_by_time;
use super::{Link, PhotoLink, SizeTag}; use super::{
not_found, redirect_to_img, Context, ImgRange, Link, PhotoLink, SizeTag,
};
use crate::models::Photo; use crate::models::Photo;
use crate::nickel_diesel::DieselRequestExtensions;
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;
use diesel::dsl::sql; use diesel::dsl::sql;
use diesel::pg::PgConnection;
use diesel::prelude::*; use diesel::prelude::*;
use diesel::sql_types::{BigInt, Integer, Nullable}; use diesel::sql_types::{BigInt, Integer, Nullable};
use log::warn; use log::warn;
use nickel::extensions::response::Redirect; use serde::Deserialize;
use nickel::{MiddlewareResult, QueryString, Request, Response};
use nickel_jwt_session::SessionRequestExtensions;
use time; use time;
use warp::http::Response;
use warp::Reply;
pub fn all_years<'mw>( pub fn all_years(context: Context) -> impl Reply {
req: &mut Request,
res: Response<'mw>,
) -> MiddlewareResult<'mw> {
use crate::schema::photos::dsl::{date, grade}; use crate::schema::photos::dsl::{date, grade};
let c: &PgConnection = &req.db_conn();
let groups = Photo::query(req.authorized_user().is_some()) let groups = Photo::query(context.is_authorized())
.select(sql::<(Nullable<Integer>, BigInt)>( .select(sql::<(Nullable<Integer>, BigInt)>(
"cast(extract(year from date) as int) y, count(*)", "cast(extract(year from date) as int) y, count(*)",
)) ))
.group_by(sql::<Nullable<Integer>>("y")) .group_by(sql::<Nullable<Integer>>("y"))
.order(sql::<Nullable<Integer>>("y").desc().nulls_last()) .order(sql::<Nullable<Integer>>("y").desc().nulls_last())
.load::<(Option<i32>, i64)>(c) .load::<(Option<i32>, i64)>(context.db())
.unwrap() .unwrap()
.iter() .iter()
.map(|&(year, count)| { .map(|&(year, count)| {
let q = Photo::query(req.authorized_user().is_some()) let q = Photo::query(context.is_authorized())
.order((grade.desc().nulls_last(), date.asc())) .order((grade.desc().nulls_last(), date.asc()))
.limit(1); .limit(1);
let photo = if let Some(year) = year { let photo = if let Some(year) = year {
@ -42,7 +38,7 @@ pub fn all_years<'mw>(
} else { } else {
q.filter(date.is_null()) q.filter(date.is_null())
}; };
let photo = photo.first::<Photo>(c).unwrap(); let photo = photo.first::<Photo>(context.db()).unwrap();
PhotoLink { PhotoLink {
title: Some( title: Some(
year.map(|y| format!("{}", y)) year.map(|y| format!("{}", y))
@ -56,23 +52,20 @@ pub fn all_years<'mw>(
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
res.ok(|o| templates::index(o, req, "All photos", &[], &groups, &[])) Response::builder().html(|o| {
templates::index(o, &context, "All photos", &[], &groups, &[])
})
} }
fn start_of_year(year: i32) -> NaiveDateTime { fn start_of_year(year: i32) -> NaiveDateTime {
NaiveDate::from_ymd(year, 1, 1).and_hms(0, 0, 0) NaiveDate::from_ymd(year, 1, 1).and_hms(0, 0, 0)
} }
pub fn months_in_year<'mw>( pub fn months_in_year(year: i32, context: Context) -> Response<Vec<u8>> {
req: &mut Request,
res: Response<'mw>,
year: i32,
) -> MiddlewareResult<'mw> {
use crate::schema::photos::dsl::{date, grade}; use crate::schema::photos::dsl::{date, grade};
let c: &PgConnection = &req.db_conn();
let title: String = format!("Photos from {}", year); let title: String = format!("Photos from {}", year);
let groups = Photo::query(req.authorized_user().is_some()) let groups = Photo::query(context.is_authorized())
.filter(date.ge(start_of_year(year))) .filter(date.ge(start_of_year(year)))
.filter(date.lt(start_of_year(year + 1))) .filter(date.lt(start_of_year(year + 1)))
.select(sql::<(Integer, BigInt)>( .select(sql::<(Integer, BigInt)>(
@ -80,17 +73,17 @@ pub fn months_in_year<'mw>(
)) ))
.group_by(sql::<Integer>("m")) .group_by(sql::<Integer>("m"))
.order(sql::<Integer>("m").desc().nulls_last()) .order(sql::<Integer>("m").desc().nulls_last())
.load::<(i32, i64)>(c) .load::<(i32, i64)>(context.db())
.unwrap() .unwrap()
.iter() .iter()
.map(|&(month, count)| { .map(|&(month, count)| {
let month = month as u32; let month = month as u32;
let photo = Photo::query(req.authorized_user().is_some()) let photo = Photo::query(context.is_authorized())
.filter(date.ge(start_of_month(year, month))) .filter(date.ge(start_of_month(year, month)))
.filter(date.lt(start_of_month(year, month + 1))) .filter(date.lt(start_of_month(year, month + 1)))
.order((grade.desc().nulls_last(), date.asc())) .order((grade.desc().nulls_last(), date.asc()))
.limit(1) .limit(1)
.first::<Photo>(c) .first::<Photo>(context.db())
.unwrap(); .unwrap();
PhotoLink { PhotoLink {
@ -104,17 +97,17 @@ pub fn months_in_year<'mw>(
.collect::<Vec<_>>(); .collect::<Vec<_>>();
if groups.is_empty() { if groups.is_empty() {
res.not_found("No such image") not_found(&context)
} else { } else {
use crate::schema::positions::dsl::{ use crate::schema::positions::dsl::{
latitude, longitude, photo_id, positions, latitude, longitude, photo_id, positions,
}; };
let pos = Photo::query(req.authorized_user().is_some()) let pos = Photo::query(context.is_authorized())
.inner_join(positions) .inner_join(positions)
.filter(date.ge(start_of_year(year))) .filter(date.ge(start_of_year(year)))
.filter(date.lt(start_of_year(year + 1))) .filter(date.lt(start_of_year(year + 1)))
.select((photo_id, latitude, longitude)) .select((photo_id, latitude, longitude))
.load(c) .load(context.db())
.map_err(|e| warn!("Failed to load positions: {}", e)) .map_err(|e| warn!("Failed to load positions: {}", e))
.unwrap_or_default() .unwrap_or_default()
.into_iter() .into_iter()
@ -122,7 +115,9 @@ pub fn months_in_year<'mw>(
((lat, long).into(), p_id) ((lat, long).into(), p_id)
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
res.ok(|o| templates::index(o, req, &title, &[], &groups, &pos)) Response::builder().html(|o| {
templates::index(o, &context, &title, &[], &groups, &pos)
})
} }
} }
@ -135,18 +130,16 @@ fn start_of_month(year: i32, month: u32) -> NaiveDateTime {
date.and_hms(0, 0, 0) date.and_hms(0, 0, 0)
} }
pub fn days_in_month<'mw>( pub fn days_in_month(
req: &mut Request,
res: Response<'mw>,
year: i32, year: i32,
month: u32, month: u32,
) -> MiddlewareResult<'mw> { context: Context,
) -> Response<Vec<u8>> {
use crate::schema::photos::dsl::{date, grade}; use crate::schema::photos::dsl::{date, grade};
let c: &PgConnection = &req.db_conn();
let lpath: Vec<Link> = vec![Link::year(year)]; let lpath: Vec<Link> = vec![Link::year(year)];
let title: String = format!("Photos from {} {}", monthname(month), year); let title: String = format!("Photos from {} {}", monthname(month), year);
let groups = Photo::query(req.authorized_user().is_some()) let groups = Photo::query(context.is_authorized())
.filter(date.ge(start_of_month(year, month))) .filter(date.ge(start_of_month(year, month)))
.filter(date.lt(start_of_month(year, month + 1))) .filter(date.lt(start_of_month(year, month + 1)))
.select(sql::<(Integer, BigInt)>( .select(sql::<(Integer, BigInt)>(
@ -154,19 +147,19 @@ pub fn days_in_month<'mw>(
)) ))
.group_by(sql::<Integer>("d")) .group_by(sql::<Integer>("d"))
.order(sql::<Integer>("d").desc().nulls_last()) .order(sql::<Integer>("d").desc().nulls_last())
.load::<(i32, i64)>(c) .load::<(i32, i64)>(context.db())
.unwrap() .unwrap()
.iter() .iter()
.map(|&(day, count)| { .map(|&(day, count)| {
let day = day as u32; let day = day as u32;
let fromdate = let fromdate =
NaiveDate::from_ymd(year, month, day).and_hms(0, 0, 0); NaiveDate::from_ymd(year, month, day).and_hms(0, 0, 0);
let photo = Photo::query(req.authorized_user().is_some()) let photo = Photo::query(context.is_authorized())
.filter(date.ge(fromdate)) .filter(date.ge(fromdate))
.filter(date.lt(fromdate + ChDuration::days(1))) .filter(date.lt(fromdate + ChDuration::days(1)))
.order((grade.desc().nulls_last(), date.asc())) .order((grade.desc().nulls_last(), date.asc()))
.limit(1) .limit(1)
.first::<Photo>(c) .first::<Photo>(context.db())
.unwrap(); .unwrap();
PhotoLink { PhotoLink {
@ -180,17 +173,17 @@ pub fn days_in_month<'mw>(
.collect::<Vec<_>>(); .collect::<Vec<_>>();
if groups.is_empty() { if groups.is_empty() {
res.not_found("No such image") not_found(&context)
} else { } else {
use crate::schema::positions::dsl::{ use crate::schema::positions::dsl::{
latitude, longitude, photo_id, positions, latitude, longitude, photo_id, positions,
}; };
let pos = Photo::query(req.authorized_user().is_some()) let pos = Photo::query(context.is_authorized())
.inner_join(positions) .inner_join(positions)
.filter(date.ge(start_of_month(year, month))) .filter(date.ge(start_of_month(year, month)))
.filter(date.lt(start_of_month(year, month + 1))) .filter(date.lt(start_of_month(year, month + 1)))
.select((photo_id, latitude, longitude)) .select((photo_id, latitude, longitude))
.load(c) .load(context.db())
.map_err(|e| warn!("Failed to load positions: {}", e)) .map_err(|e| warn!("Failed to load positions: {}", e))
.unwrap_or_default() .unwrap_or_default()
.into_iter() .into_iter()
@ -198,28 +191,26 @@ pub fn days_in_month<'mw>(
((lat, long).into(), p_id) ((lat, long).into(), p_id)
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
res.ok(|o| templates::index(o, req, &title, &lpath, &groups, &pos)) Response::builder().html(|o| {
templates::index(o, &context, &title, &lpath, &groups, &pos)
})
} }
} }
pub fn all_null_date<'mw>( pub fn all_null_date(context: Context) -> impl Reply {
req: &mut Request,
res: Response<'mw>,
) -> MiddlewareResult<'mw> {
use crate::schema::photos::dsl::{date, path}; use crate::schema::photos::dsl::{date, path};
let c: &PgConnection = &req.db_conn(); Response::builder().html(|o| {
res.ok(|o| {
templates::index( templates::index(
o, o,
req, &context,
"Photos without a date", "Photos without a date",
&[], &[],
&Photo::query(req.authorized_user().is_some()) &Photo::query(context.is_authorized())
.filter(date.is_null()) .filter(date.is_null())
.order(path.asc()) .order(path.asc())
.limit(500) .limit(500)
.load(c) .load(context.db())
.unwrap() .unwrap()
.iter() .iter()
.map(PhotoLink::from) .map(PhotoLink::from)
@ -229,28 +220,28 @@ pub fn all_null_date<'mw>(
}) })
} }
pub fn all_for_day<'mw>( pub fn all_for_day(
req: &mut Request,
res: Response<'mw>,
year: i32, year: i32,
month: u32, month: u32,
day: u32, day: u32,
) -> MiddlewareResult<'mw> { range: ImgRange,
context: Context,
) -> impl Reply {
let thedate = NaiveDate::from_ymd(year, month, day).and_hms(0, 0, 0); let thedate = NaiveDate::from_ymd(year, month, day).and_hms(0, 0, 0);
use crate::schema::photos::dsl::date; use crate::schema::photos::dsl::date;
let photos = Photo::query(req.authorized_user().is_some()) let photos = Photo::query(context.is_authorized())
.filter(date.ge(thedate)) .filter(date.ge(thedate))
.filter(date.lt(thedate + ChDuration::days(1))); .filter(date.lt(thedate + ChDuration::days(1)));
let (links, coords) = links_by_time(req, photos); let (links, coords) = links_by_time(&context, photos, range);
if links.is_empty() { if links.is_empty() {
res.not_found("No such image") not_found(&context)
} else { } else {
res.ok(|o| { Response::builder().html(|o| {
templates::index( templates::index(
o, o,
req, &context,
&format!("Photos from {} {} {}", day, monthname(month), year), &format!("Photos from {} {} {}", day, monthname(month), year),
&[Link::year(year), Link::month(year, month)], &[Link::year(year), Link::month(year, month)],
&links, &links,
@ -260,41 +251,37 @@ pub fn all_for_day<'mw>(
} }
} }
pub fn on_this_day<'mw>( pub fn on_this_day(context: Context) -> impl Reply {
req: &mut Request,
res: Response<'mw>,
) -> MiddlewareResult<'mw> {
use crate::schema::photos::dsl::{date, grade}; use crate::schema::photos::dsl::{date, grade};
use crate::schema::positions::dsl::{ use crate::schema::positions::dsl::{
latitude, longitude, photo_id, positions, latitude, longitude, photo_id, positions,
}; };
let c: &PgConnection = &req.db_conn();
let (month, day) = { let (month, day) = {
let now = time::now(); let now = time::now();
(now.tm_mon as u32 + 1, now.tm_mday as u32) (now.tm_mon as u32 + 1, now.tm_mday as u32)
}; };
let pos = Photo::query(req.authorized_user().is_some()) let pos = Photo::query(context.is_authorized())
.inner_join(positions) .inner_join(positions)
.filter( .filter(
sql("extract(month from date)=").bind::<Integer, _>(month as i32), sql("extract(month from date)=").bind::<Integer, _>(month as i32),
) )
.filter(sql("extract(day from date)=").bind::<Integer, _>(day as i32)) .filter(sql("extract(day from date)=").bind::<Integer, _>(day as i32))
.select((photo_id, latitude, longitude)) .select((photo_id, latitude, longitude))
.load(c) .load(context.db())
.map_err(|e| warn!("Failed to load positions: {}", e)) .map_err(|e| warn!("Failed to load positions: {}", e))
.unwrap_or_default() .unwrap_or_default()
.into_iter() .into_iter()
.map(|(p_id, lat, long): (i32, i32, i32)| ((lat, long).into(), p_id)) .map(|(p_id, lat, long): (i32, i32, i32)| ((lat, long).into(), p_id))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
res.ok(|o| { Response::builder().html(|o| {
templates::index( templates::index(
o, o,
req, &context,
&format!("Photos from {} {}", day, monthname(month)), &format!("Photos from {} {}", day, monthname(month)),
&[], &[],
&Photo::query(req.authorized_user().is_some()) &Photo::query(context.is_authorized())
.select(sql::<(Integer, BigInt)>( .select(sql::<(Integer, BigInt)>(
"cast(extract(year from date) as int) y, count(*)", "cast(extract(year from date) as int) y, count(*)",
)) ))
@ -308,19 +295,19 @@ pub fn on_this_day<'mw>(
.bind::<Integer, _>(day as i32), .bind::<Integer, _>(day as i32),
) )
.order(sql::<Integer>("y").desc()) .order(sql::<Integer>("y").desc())
.load::<(i32, i64)>(c) .load::<(i32, i64)>(context.db())
.unwrap() .unwrap()
.iter() .iter()
.map(|&(year, count)| { .map(|&(year, count)| {
let fromdate = let fromdate =
NaiveDate::from_ymd(year, month as u32, day) NaiveDate::from_ymd(year, month as u32, day)
.and_hms(0, 0, 0); .and_hms(0, 0, 0);
let photo = Photo::query(req.authorized_user().is_some()) let photo = Photo::query(context.is_authorized())
.filter(date.ge(fromdate)) .filter(date.ge(fromdate))
.filter(date.lt(fromdate + ChDuration::days(1))) .filter(date.lt(fromdate + ChDuration::days(1)))
.order((grade.desc().nulls_last(), date.asc())) .order((grade.desc().nulls_last(), date.asc()))
.limit(1) .limit(1)
.first::<Photo>(c) .first::<Photo>(context.db())
.unwrap(); .unwrap();
PhotoLink { PhotoLink {
@ -337,65 +324,48 @@ pub fn on_this_day<'mw>(
}) })
} }
pub fn next_image<'mw>( pub fn next_image(context: Context, param: FromParam) -> impl Reply {
req: &mut Request,
res: Response<'mw>,
) -> MiddlewareResult<'mw> {
use crate::schema::photos::dsl::{date, id}; use crate::schema::photos::dsl::{date, id};
if let Some((from_id, from_date)) = query_date(req, "from") { if let Some(from_date) = date_of_img(context.db(), param.from) {
let q = Photo::query(req.authorized_user().is_some()) let q = Photo::query(context.is_authorized())
.select(id) .select(id)
.filter( .filter(
date.gt(from_date) date.gt(from_date)
.or(date.eq(from_date).and(id.gt(from_id))), .or(date.eq(from_date).and(id.gt(param.from))),
) )
.order((date, id)); .order((date, id));
let c: &PgConnection = &req.db_conn(); if let Ok(photo) = q.first::<i32>(context.db()) {
if let Ok(photo) = q.first::<i32>(c) { return redirect_to_img(photo);
return res.redirect(format!("/img/{}", photo)); // to photo_details
} }
} }
res.not_found("No such image") not_found(&context)
} }
pub fn prev_image<'mw>( pub fn prev_image(context: Context, param: FromParam) -> impl Reply {
req: &mut Request,
res: Response<'mw>,
) -> MiddlewareResult<'mw> {
use crate::schema::photos::dsl::{date, id}; use crate::schema::photos::dsl::{date, id};
if let Some((from_id, from_date)) = query_date(req, "from") { if let Some(from_date) = date_of_img(context.db(), param.from) {
let q = Photo::query(req.authorized_user().is_some()) let q = Photo::query(context.is_authorized())
.select(id) .select(id)
.filter( .filter(
date.lt(from_date) date.lt(from_date)
.or(date.eq(from_date).and(id.lt(from_id))), .or(date.eq(from_date).and(id.lt(param.from))),
) )
.order((date.desc().nulls_last(), id.desc())); .order((date.desc().nulls_last(), id.desc()));
let c: &PgConnection = &req.db_conn(); if let Ok(photo) = q.first::<i32>(context.db()) {
if let Ok(photo) = q.first::<i32>(c) { return redirect_to_img(photo);
return res.redirect(format!("/img/{}", photo)); // to photo_details
} }
} }
res.not_found("No such image") not_found(&context)
} }
pub fn query_date( #[derive(Deserialize)]
req: &mut Request, pub struct FromParam {
name: &str, from: i32,
) -> Option<(i32, NaiveDateTime)> { }
req.query()
.get(name) pub fn date_of_img(db: &PgConnection, photo_id: i32) -> Option<NaiveDateTime> {
.and_then(|s| s.parse().ok())
.and_then(|i: i32| {
use crate::schema::photos::dsl::{date, photos}; use crate::schema::photos::dsl::{date, photos};
let c: &PgConnection = &req.db_conn(); photos.find(photo_id).select(date).first(db).unwrap_or(None)
photos
.find(i)
.select(date)
.first(c)
.unwrap_or(None)
.map(|d| (i, d))
})
} }
pub fn monthname(n: u32) -> &'static str { pub fn monthname(n: u32) -> &'static str {

View File

@ -1,11 +1,9 @@
@use nickel::Request;
@use nickel_jwt_session::SessionRequestExtensions;
@use crate::models::{Photo, Person, Place, Tag, Camera, Coord};
@use crate::server::{Link, SizeTag};
@use super::page_base; @use super::page_base;
@use crate::models::{Photo, Person, Place, Tag, Camera, Coord};
@use crate::server::{Context, Link, SizeTag};
@(req: &Request, 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(req, "Photo details", lpath, { @:page_base(context, "Photo details", lpath, {
<meta property='og:title' content='Photo @if let Some(d) = photo.date {(@d.format("%F"))}'> <meta property='og:title' content='Photo @if let Some(d) = photo.date {(@d.format("%F"))}'>
<meta property='og:type' content='image' /> <meta property='og:type' content='image' />
<meta property='og:image' content='/img/@photo.id-m.jpg' /> <meta property='og:image' content='/img/@photo.id-m.jpg' />
@ -14,7 +12,7 @@
<div class="details" data-imgid="@photo.id"@if let Some(g) = photo.grade { data-grade="@g"}@if let Some(ref p) = *position { data-position="[@p.x, @p.y]"}> <div class="details" data-imgid="@photo.id"@if let Some(g) = photo.grade { data-grade="@g"}@if let Some(ref p) = *position { data-position="[@p.x, @p.y]"}>
<div class="item"><img src="/img/@photo.id-m.jpg"@if let Some((w,h)) = photo.get_size(SizeTag::Medium.px()) { width="@w" height="@h"}></div> <div class="item"><img src="/img/@photo.id-m.jpg"@if let Some((w,h)) = photo.get_size(SizeTag::Medium.px()) { width="@w" height="@h"}></div>
<div class="meta"> <div class="meta">
@if req.authorized_user().is_some() { @if context.is_authorized() {
<p><a href="/img/@photo.id-l.jpg">@photo.path</a></p> <p><a href="/img/@photo.id-l.jpg">@photo.path</a></p>
@if photo.is_public() {<p>This photo is public.</p>} @if photo.is_public() {<p>This photo is public.</p>}
else {<p>This photo is not public.</p>} else {<p>This photo is not public.</p>}

37
templates/error.rs.html Normal file
View File

@ -0,0 +1,37 @@
@use super::statics::{photos_css, ux_js};
@use warp::http::StatusCode;
@(code: StatusCode, message: &str)
<!doctype html>
<html>
<head>
<title>Error @code.as_u16() @code.canonical_reason().unwrap_or("error")</title>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link rel="stylesheet" href="/static/@photos_css.name" type="text/css"/>
<script src="/static/@ux_js.name" type="text/javascript" defer>
</script>
</head>
<body>
<header>
<span><a href="/" accesskey="h" title="Images from all years">Images</a>
</span>
<span>· <a href="/tag/">Tags</a></span>
<span>· <a href="/person/">People</a></span>
<span>· <a href="/place/">Places</a></span>
<span>· <a href="/thisday">On this day</a></span>
<span>· <a href="/random" accesskey="r">Random pic</a></span>
<span class="user"></span>
</header>
<main>
<h1>@code.canonical_reason().unwrap_or("error")</h1>
<p>@message (@code.as_u16())</p>
</main>
<footer>
<p>Managed by
<a href="https://github.com/kaj/rphotos">rphotos
@env!("CARGO_PKG_VERSION")</a>.</p>
</footer>
</body>
</html>

View File

@ -1,8 +1,6 @@
@use nickel::Request; @use crate::server::{Context, Link};
@use nickel_jwt_session::SessionRequestExtensions;
@use crate::server::Link;
@(req: &Request, lpath: &[Link]) @(context: &Context, lpath: &[Link])
<header> <header>
<span><a href="/" accesskey="h" title="Images from all years">Images</a> <span><a href="/" accesskey="h" title="Images from all years">Images</a>
@ -13,6 +11,6 @@
<span>· <a href="/place/">Places</a></span> <span>· <a href="/place/">Places</a></span>
<span>· <a href="/thisday">On this day</a></span> <span>· <a href="/thisday">On this day</a></span>
<span>· <a href="/random" accesskey="r">Random pic</a></span> <span>· <a href="/random" accesskey="r">Random pic</a></span>
@if let Some(ref u) = req.authorized_user() {<span class="user">@u (<a href="/logout">log out</a>)</span>} @if let Some(ref u) = context.authorized_user() {<span class="user">@u (<a href="/logout">log out</a>)</span>}
else {<span class="user">(<a href="/login@if let Some(p) = req.path_without_query() {?next=@p}">log in</a>)</span>} else {<span class="user">(<a href="/login?next=@context.path_without_query()">log in</a>)</span>}
</header> </header>

View File

@ -1,11 +1,10 @@
@use crate::models::Coord;
@use nickel::Request;
@use crate::server::{Link, PhotoLink};
@use super::{data_positions, page_base, photo_link}; @use super::{data_positions, page_base, photo_link};
@use crate::models::Coord;
@use crate::server::{Context, Link, PhotoLink};
@(req: &Request, title: &str, lpath: &[Link], photos: &[PhotoLink], coords: &[(Coord, i32)]) @(context: &Context, title: &str, lpath: &[Link], photos: &[PhotoLink], coords: &[(Coord, i32)])
@:page_base(req, title, lpath, {}, { @:page_base(context, title, lpath, {}, {
<div class="group"@:data_positions(coords)> <div class="group"@:data_positions(coords)>
@for p in photos {@:photo_link(p)} @for p in photos {@:photo_link(p)}
</div> </div>

View File

@ -1,9 +1,9 @@
@use nickel::Request;
@use super::page_base; @use super::page_base;
@use crate::server::Context;
@(req: &Request, next: Option<String>, message: Option<&str>) @(context: &Context, next: Option<String>, message: Option<&str>)
@:page_base(req, "login", &[], {}, { @:page_base(context, "login", &[], {}, {
<form action="/login" method="post"> <form action="/login" method="post">
@if let Some(message) = message {<p>@message</p>} @if let Some(message) = message {<p>@message</p>}
<p><label for="user">User:</label> <p><label for="user">User:</label>

View File

@ -1,13 +1,13 @@
@use nickel::Request;
@use super::page_base; @use super::page_base;
@use nickel_jwt_session::SessionRequestExtensions; @use crate::server::Context;
@use warp::http::StatusCode;
@(req: &Request) @(context: &Context, code: StatusCode, message: &str)
@:page_base(req, "Not found", &[], {}, { @:page_base(context, code.canonical_reason().unwrap_or("error"), &[], {}, {
<p>No page or photo match that url.</p> <p>@message (@code.as_u16())</p>
@if req.authorized_user().is_none() { @if !context.is_authorized() {
<p>At least nothing publicly visible, you might try <p>At least nothing publicly visible, you might try
<a href="/login@if let Some(p) = req.path_without_query() {?next=@p}">logging in</a>.</p> <a href="/login?next=@context.path_without_query()">logging in</a>.</p>
} }
}) })

View File

@ -1,10 +1,8 @@
@use nickel::Request;
@use nickel_jwt_session::SessionRequestExtensions;
@use crate::server::Link;
@use super::head;
@use super::statics::{photos_css, admin_js, ux_js}; @use super::statics::{photos_css, admin_js, ux_js};
@use crate::server::{Context, Link};
@use crate::templates::head;
@(req: &Request, title: &str, lpath: &[Link], meta: Content, content: Content) @(context: &Context, title: &str, lpath: &[Link], meta: Content, content: Content)
<!doctype html> <!doctype html>
<html> <html>
@ -13,7 +11,7 @@
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" /> <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1"/> <meta name="viewport" content="width=device-width, initial-scale=1"/>
<link rel="stylesheet" href="/static/@photos_css.name" type="text/css"/> <link rel="stylesheet" href="/static/@photos_css.name" type="text/css"/>
@if req.authorized_user().is_some() { @if context.is_authorized() {
<script src="/static/@admin_js.name" type="text/javascript" defer> <script src="/static/@admin_js.name" type="text/javascript" defer>
</script> </script>
} }
@ -22,7 +20,7 @@
@:meta() @:meta()
</head> </head>
<body> <body>
@:head(req, lpath) @:head(context, lpath)
<main> <main>
<h1>@title</h1> <h1>@title</h1>
@:content() @:content()

View File

@ -1,9 +1,9 @@
@use nickel::Request;
@use crate::models::Person;
@use super::page_base; @use super::page_base;
@use crate::models::Person;
@use crate::server::Context;
@(req: &Request, people: &[Person]) @(context: &Context, people: &[Person])
@:page_base(req, "Photo people", &[], {}, { @:page_base(context, "Photo people", &[], {}, {
<ul class="allpeople"> <ul class="allpeople">
@for p in people { @for p in people {
<li><a href="/person/@p.slug">@p.person_name</a> <li><a href="/person/@p.slug">@p.person_name</a>

View File

@ -1,10 +1,9 @@
@use nickel::Request;
@use crate::models::{Coord, Person};
@use crate::server::PhotoLink;
@use super::{data_positions, page_base, photo_link}; @use super::{data_positions, page_base, photo_link};
@use crate::models::{Coord, Person};
@use crate::server::{Context, PhotoLink};
@(req: &Request, photos: &[PhotoLink], coords: &[(Coord, i32)], person: &Person) @(context: &Context, photos: &[PhotoLink], coords: &[(Coord, i32)], person: &Person)
@:page_base(req, &format!("Photos with {}", person.person_name), &[], {}, { @:page_base(context, &format!("Photos with {}", person.person_name), &[], {}, {
<div class="group"@:data_positions(coords)> <div class="group"@:data_positions(coords)>
@for p in photos {@:photo_link(p)} @for p in photos {@:photo_link(p)}
</div> </div>

View File

@ -1,10 +1,9 @@
@use nickel::Request;
@use crate::models::{Coord, Place}; @use crate::models::{Coord, Place};
@use crate::server::PhotoLink; @use crate::server::{Context, PhotoLink};
@use super::{data_positions, page_base, photo_link}; @use super::{data_positions, page_base, photo_link};
@(req: &Request, photos: &[PhotoLink], coords: &[(Coord, i32)], place: &Place) @(context: &Context, photos: &[PhotoLink], coords: &[(Coord, i32)], place: &Place)
@:page_base(req, &format!("Photos from {}", place.place_name), &[], {}, { @:page_base(context, &format!("Photos from {}", place.place_name), &[], {}, {
<div class="group"@:data_positions(coords)> <div class="group"@:data_positions(coords)>
@for p in photos {@:photo_link(p)} @for p in photos {@:photo_link(p)}
</div> </div>

View File

@ -1,10 +1,10 @@
@use nickel::Request;
@use crate::models::Place;
@use super::page_base; @use super::page_base;
@use crate::models::Place;
@use crate::server::Context;
@(req: &Request, places: &[Place]) @(context: &Context, places: &[Place])
@:page_base(req, "Photo places", &[], {}, { @:page_base(context, "Photo places", &[], {}, {
<ul class="allplaces"> <ul class="allplaces">
@for p in places { @for p in places {
<li><a href="/place/@p.slug">@p.place_name</a> <li><a href="/place/@p.slug">@p.place_name</a>

View File

@ -1,11 +1,10 @@
@use nickel::Request;
@use crate::models::{Coord, Tag}; @use crate::models::{Coord, Tag};
@use crate::server::PhotoLink; @use crate::server::{Context, PhotoLink};
@use super::{data_positions, page_base, photo_link}; @use super::{data_positions, page_base, photo_link};
@(req: &Request, photos: &[PhotoLink], coords: &[(Coord, i32)], tag: &Tag) @(context: &Context, photos: &[PhotoLink], coords: &[(Coord, i32)], tag: &Tag)
@:page_base(req, &format!("Photos tagged {}", tag.tag_name), &[], {}, { @:page_base(context, &format!("Photos tagged {}", tag.tag_name), &[], {}, {
<div class="group"@:data_positions(coords)> <div class="group"@:data_positions(coords)>
@for p in photos {@:photo_link(p)} @for p in photos {@:photo_link(p)}
</div> </div>

View File

@ -1,9 +1,9 @@
@use nickel::Request;
@use crate::models::Tag;
@use super::page_base; @use super::page_base;
@use crate::models::Tag;
@use crate::server::Context;
@(req: &Request, tags: &[Tag]) @(context: &Context, tags: &[Tag])
@:page_base(req, "Photo tags", &[], {}, { @:page_base(context, "Photo tags", &[], {}, {
<ul class="alltags"> <ul class="alltags">
@for tag in tags { @for tag in tags {
<li><a href="/tag/@tag.slug">@tag.tag_name</a> <li><a href="/tag/@tag.slug">@tag.tag_name</a>