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:
parent
7080f7b382
commit
5c03a28f59
13
Cargo.toml
13
Cargo.toml
@ -7,31 +7,30 @@ edition = "2018"
|
||||
build = "src/build.rs"
|
||||
|
||||
[build-dependencies]
|
||||
ructe = { version = "^0.6.2", features = ["sass", "mime02"] }
|
||||
ructe = { version = "^0.6.2", features = ["sass", "mime03"] }
|
||||
|
||||
[dependencies]
|
||||
nickel = "~0.10.0"
|
||||
nickel-jwt-session = "~0.10.0"
|
||||
hyper = "~0.10.0"
|
||||
warp = "0.1.6"
|
||||
env_logger = "*"
|
||||
libc = "*"
|
||||
log = "*"
|
||||
chrono = "~0.4.0" # Must match version used by diesel
|
||||
clap = { version = "^2.19", features = [ "color", "wrap_help" ] }
|
||||
typemap = "*"
|
||||
plugin = "*"
|
||||
image = "0.21"
|
||||
jwt = "0.4.0"
|
||||
time = "*"
|
||||
kamadak-exif = "~0.3.0"
|
||||
diesel = { version = "1.4.0", features = ["r2d2", "chrono", "postgres"] }
|
||||
dotenv = "0.13.0"
|
||||
djangohashers = "*"
|
||||
rand = "0.6.5"
|
||||
rust-crypto = "0.2.36"
|
||||
memcached-rs = "0.4.1"
|
||||
flate2 = "^1.0.0"
|
||||
brotli2 = "*"
|
||||
mime = "0.2.6"
|
||||
mime = "0.3.0"
|
||||
regex = "*"
|
||||
slug = "0.1"
|
||||
reqwest = "0.9"
|
||||
serde = { version = "1.0.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
@ -2,20 +2,14 @@
|
||||
#![recursion_limit = "128"]
|
||||
#[macro_use]
|
||||
extern crate diesel;
|
||||
#[macro_use]
|
||||
extern crate nickel;
|
||||
|
||||
mod adm;
|
||||
mod env;
|
||||
mod fetch_places;
|
||||
mod memcachemiddleware;
|
||||
mod models;
|
||||
mod myexif;
|
||||
mod nickel_diesel;
|
||||
mod photosdir;
|
||||
mod photosdirmiddleware;
|
||||
mod pidfiles;
|
||||
mod requestloggermiddleware;
|
||||
mod schema;
|
||||
mod server;
|
||||
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
@ -1,249 +1,226 @@
|
||||
//! Admin-only views, generally called by javascript.
|
||||
use super::nickelext::MyResponse;
|
||||
use super::SizeTag;
|
||||
use super::{not_found, permission_denied, redirect_to_img, Context, SizeTag};
|
||||
use crate::fetch_places::update_image_places;
|
||||
use crate::memcachemiddleware::MemcacheRequestExtensions;
|
||||
use crate::models::{Coord, Photo};
|
||||
use crate::nickel_diesel::DieselRequestExtensions;
|
||||
use diesel::prelude::*;
|
||||
use diesel::{self, prelude::*};
|
||||
use log::{info, warn};
|
||||
use nickel::extensions::Redirect;
|
||||
use nickel::status::StatusCode;
|
||||
use nickel::{BodyError, FormBody, MiddlewareResult, Request, Response};
|
||||
use nickel_jwt_session::SessionRequestExtensions;
|
||||
use serde::Deserialize;
|
||||
use slug::slugify;
|
||||
use warp::filters::BoxedFilter;
|
||||
use warp::http::Response;
|
||||
use warp::{Filter, Reply};
|
||||
|
||||
pub fn rotate<'mw>(
|
||||
req: &mut Request,
|
||||
res: Response<'mw>,
|
||||
) -> MiddlewareResult<'mw> {
|
||||
if req.authorized_user().is_none() {
|
||||
return res.error(StatusCode::Unauthorized, "permission denied");
|
||||
pub fn routes(s: BoxedFilter<(Context,)>) -> BoxedFilter<(impl Reply,)> {
|
||||
use warp::{body::form, path, post2 as post};
|
||||
let route = path("grade")
|
||||
.and(s.clone())
|
||||
.and(form())
|
||||
.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 {}", image, angle);
|
||||
use crate::schema::photos::dsl::photos;
|
||||
let c: &PgConnection = &req.db_conn();
|
||||
if let Ok(mut image) = photos.find(image).first::<Photo>(c) {
|
||||
let newvalue = (360 + image.rotation + angle) % 360;
|
||||
info!("Rotation was {}, setting to {}", image.rotation, newvalue);
|
||||
image.rotation = newvalue;
|
||||
match image.save_changes::<Photo>(c) {
|
||||
Ok(image) => {
|
||||
req.clear_cache(&image.cache_key(SizeTag::Small));
|
||||
req.clear_cache(&image.cache_key(SizeTag::Medium));
|
||||
return res.ok(|o| writeln!(o, "ok"));
|
||||
}
|
||||
Err(error) => {
|
||||
warn!("Failed to save image #{}: {}", image.id, error);
|
||||
}
|
||||
info!("Should rotate #{} by {}", form.image, form.angle);
|
||||
use crate::schema::photos::dsl::photos;
|
||||
let c = context.db();
|
||||
if let Ok(mut image) = photos.find(form.image).first::<Photo>(c) {
|
||||
let newvalue = (360 + image.rotation + form.angle) % 360;
|
||||
info!("Rotation was {}, setting to {}", image.rotation, newvalue);
|
||||
image.rotation = newvalue;
|
||||
match image.save_changes::<Photo>(c) {
|
||||
Ok(image) => {
|
||||
context.clear_cache(&image.cache_key(SizeTag::Small));
|
||||
context.clear_cache(&image.cache_key(SizeTag::Medium));
|
||||
return Response::builder().body(b"ok".to_vec()).unwrap();
|
||||
}
|
||||
Err(error) => {
|
||||
warn!("Failed to save image #{}: {}", image.id, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
info!("Missing image and/or angle to rotate or image not found");
|
||||
res.not_found("")
|
||||
not_found(&context)
|
||||
}
|
||||
|
||||
type QResult<T> = Result<T, (StatusCode, BodyError)>;
|
||||
|
||||
fn rotate_params(req: &mut Request) -> QResult<(Option<i32>, Option<i16>)> {
|
||||
let data = req.form_body()?;
|
||||
Ok((
|
||||
data.get("image").and_then(|s| s.parse().ok()),
|
||||
data.get("angle").and_then(|s| s.parse().ok()),
|
||||
))
|
||||
#[derive(Deserialize)]
|
||||
struct RotateForm {
|
||||
image: i32,
|
||||
angle: i16,
|
||||
}
|
||||
|
||||
pub fn set_tag<'mw>(
|
||||
req: &mut Request,
|
||||
res: Response<'mw>,
|
||||
) -> MiddlewareResult<'mw> {
|
||||
if req.authorized_user().is_none() {
|
||||
return res.error(StatusCode::Unauthorized, "permission denied");
|
||||
fn set_tag(context: Context, form: TagForm) -> Response<Vec<u8>> {
|
||||
if !context.is_authorized() {
|
||||
return permission_denied();
|
||||
}
|
||||
if let (Some(image), Some(tag)) = try_with!(res, tag_params(req)) {
|
||||
let c: &PgConnection = &req.db_conn();
|
||||
use crate::models::{PhotoTag, Tag};
|
||||
use diesel;
|
||||
let tag = {
|
||||
use crate::schema::tags::dsl::*;
|
||||
tags.filter(tag_name.ilike(&tag))
|
||||
.first::<Tag>(c)
|
||||
.or_else(|_| {
|
||||
diesel::insert_into(tags)
|
||||
.values((tag_name.eq(&tag), slug.eq(&slugify(&tag))))
|
||||
.get_result::<Tag>(c)
|
||||
})
|
||||
.expect("Find or create tag")
|
||||
};
|
||||
use crate::schema::photo_tags::dsl::*;
|
||||
let q = photo_tags
|
||||
.filter(photo_id.eq(image))
|
||||
.filter(tag_id.eq(tag.id));
|
||||
if q.first::<PhotoTag>(c).is_ok() {
|
||||
info!("Photo #{} already has {:?}", image, tag);
|
||||
} else {
|
||||
info!("Add {:?} on photo #{}!", tag, image);
|
||||
diesel::insert_into(photo_tags)
|
||||
.values((photo_id.eq(image), tag_id.eq(tag.id)))
|
||||
.execute(c)
|
||||
.expect("Tag a photo");
|
||||
}
|
||||
return res.redirect(format!("/img/{}", image));
|
||||
let c = context.db();
|
||||
use crate::models::{PhotoTag, Tag};
|
||||
use diesel;
|
||||
let tag = {
|
||||
use crate::schema::tags::dsl::*;
|
||||
tags.filter(tag_name.ilike(&form.tag))
|
||||
.first::<Tag>(c)
|
||||
.or_else(|_| {
|
||||
diesel::insert_into(tags)
|
||||
.values((
|
||||
tag_name.eq(&form.tag),
|
||||
slug.eq(&slugify(&form.tag)),
|
||||
))
|
||||
.get_result::<Tag>(c)
|
||||
})
|
||||
.expect("Find or create tag")
|
||||
};
|
||||
use crate::schema::photo_tags::dsl::*;
|
||||
let q = photo_tags
|
||||
.filter(photo_id.eq(form.image))
|
||||
.filter(tag_id.eq(tag.id));
|
||||
if q.first::<PhotoTag>(c).is_ok() {
|
||||
info!("Photo #{} already has {:?}", form.image, form.tag);
|
||||
} else {
|
||||
info!("Add {:?} on photo #{}!", form.tag, form.image);
|
||||
diesel::insert_into(photo_tags)
|
||||
.values((photo_id.eq(form.image), tag_id.eq(tag.id)))
|
||||
.execute(c)
|
||||
.expect("Tag a photo");
|
||||
}
|
||||
info!("Missing image and/or angle to rotate or image not found");
|
||||
res.not_found("")
|
||||
redirect_to_img(form.image)
|
||||
}
|
||||
|
||||
fn tag_params(req: &mut Request) -> QResult<(Option<i32>, Option<String>)> {
|
||||
let data = req.form_body()?;
|
||||
Ok((
|
||||
data.get("image").and_then(|s| s.parse().ok()),
|
||||
data.get("tag").map(String::from),
|
||||
))
|
||||
#[derive(Deserialize)]
|
||||
struct TagForm {
|
||||
image: i32,
|
||||
tag: String,
|
||||
}
|
||||
|
||||
pub fn set_person<'mw>(
|
||||
req: &mut Request,
|
||||
res: Response<'mw>,
|
||||
) -> MiddlewareResult<'mw> {
|
||||
if req.authorized_user().is_none() {
|
||||
return res.error(StatusCode::Unauthorized, "permission denied");
|
||||
fn set_person(context: Context, form: PersonForm) -> Response<Vec<u8>> {
|
||||
if !context.is_authorized() {
|
||||
return permission_denied();
|
||||
}
|
||||
if let (Some(image), Some(name)) = try_with!(res, person_params(req)) {
|
||||
let c: &PgConnection = &req.db_conn();
|
||||
use crate::models::{Person, PhotoPerson};
|
||||
use diesel;
|
||||
let person = {
|
||||
use crate::schema::people::dsl::*;
|
||||
people
|
||||
.filter(person_name.ilike(&name))
|
||||
.first::<Person>(c)
|
||||
.or_else(|_| {
|
||||
diesel::insert_into(people)
|
||||
.values((
|
||||
person_name.eq(&name),
|
||||
slug.eq(&slugify(&name)),
|
||||
))
|
||||
.get_result::<Person>(c)
|
||||
})
|
||||
.expect("Find or create tag")
|
||||
};
|
||||
use crate::schema::photo_people::dsl::*;
|
||||
let q = photo_people
|
||||
.filter(photo_id.eq(image))
|
||||
.filter(person_id.eq(person.id));
|
||||
if q.first::<PhotoPerson>(c).is_ok() {
|
||||
info!("Photo #{} already has {:?}", image, person);
|
||||
} else {
|
||||
info!("Add {:?} on photo #{}!", person, image);
|
||||
diesel::insert_into(photo_people)
|
||||
.values((photo_id.eq(image), person_id.eq(person.id)))
|
||||
.execute(c)
|
||||
.expect("Name person in photo");
|
||||
}
|
||||
return res.redirect(format!("/img/{}", image));
|
||||
let c = context.db();
|
||||
use crate::models::{Person, PhotoPerson};
|
||||
use diesel;
|
||||
let person = {
|
||||
use crate::schema::people::dsl::*;
|
||||
people
|
||||
.filter(person_name.ilike(&form.person))
|
||||
.first::<Person>(c)
|
||||
.or_else(|_| {
|
||||
diesel::insert_into(people)
|
||||
.values((
|
||||
person_name.eq(&form.person),
|
||||
slug.eq(&slugify(&form.person)),
|
||||
))
|
||||
.get_result::<Person>(c)
|
||||
})
|
||||
.expect("Find or create tag")
|
||||
};
|
||||
use crate::schema::photo_people::dsl::*;
|
||||
let q = photo_people
|
||||
.filter(photo_id.eq(form.image))
|
||||
.filter(person_id.eq(person.id));
|
||||
if q.first::<PhotoPerson>(c).is_ok() {
|
||||
info!("Photo #{} already has {:?}", form.image, person);
|
||||
} else {
|
||||
info!("Add {:?} on photo #{}!", person, form.image);
|
||||
diesel::insert_into(photo_people)
|
||||
.values((photo_id.eq(form.image), person_id.eq(person.id)))
|
||||
.execute(c)
|
||||
.expect("Name person in photo");
|
||||
}
|
||||
info!("Missing image and/or angle to rotate or image not found");
|
||||
res.not_found("")
|
||||
redirect_to_img(form.image)
|
||||
}
|
||||
|
||||
fn person_params(req: &mut Request) -> QResult<(Option<i32>, Option<String>)> {
|
||||
let data = req.form_body()?;
|
||||
Ok((
|
||||
data.get("image").and_then(|s| s.parse().ok()),
|
||||
data.get("person").map(String::from),
|
||||
))
|
||||
#[derive(Deserialize)]
|
||||
struct PersonForm {
|
||||
image: i32,
|
||||
person: String,
|
||||
}
|
||||
|
||||
pub fn set_grade<'mw>(
|
||||
req: &mut Request,
|
||||
res: Response<'mw>,
|
||||
) -> MiddlewareResult<'mw> {
|
||||
if req.authorized_user().is_none() {
|
||||
return res.error(StatusCode::Unauthorized, "permission denied");
|
||||
fn set_grade(context: Context, form: GradeForm) -> Response<Vec<u8>> {
|
||||
if !context.is_authorized() {
|
||||
return permission_denied();
|
||||
}
|
||||
if let (Some(image), Some(newgrade)) = try_with!(res, grade_params(req)) {
|
||||
if newgrade >= 0 && newgrade <= 100 {
|
||||
info!("Should set grade of #{} to {}", image, newgrade);
|
||||
use crate::schema::photos::dsl::{grade, photos};
|
||||
use diesel;
|
||||
let c: &PgConnection = &req.db_conn();
|
||||
let q = diesel::update(photos.find(image)).set(grade.eq(newgrade));
|
||||
match q.execute(c) {
|
||||
Ok(1) => {
|
||||
return res.redirect(format!("/img/{}", image));
|
||||
}
|
||||
Ok(0) => (),
|
||||
Ok(n) => {
|
||||
warn!("Strange, updated {} images with id {}", n, image);
|
||||
}
|
||||
Err(error) => {
|
||||
warn!("Failed set grade of image #{}: {}", image, error);
|
||||
}
|
||||
if form.grade >= 0 && form.grade <= 100 {
|
||||
info!("Should set grade of #{} to {}", form.image, form.grade);
|
||||
use crate::schema::photos::dsl::{grade, photos};
|
||||
let q =
|
||||
diesel::update(photos.find(form.image)).set(grade.eq(form.grade));
|
||||
match q.execute(context.db()) {
|
||||
Ok(1) => {
|
||||
return redirect_to_img(form.image);
|
||||
}
|
||||
} else {
|
||||
info!("Grade {} is out of range for image #{}", newgrade, image);
|
||||
Ok(0) => (),
|
||||
Ok(n) => {
|
||||
warn!("Strange, updated {} images with id {}", n, form.image);
|
||||
}
|
||||
Err(error) => {
|
||||
warn!("Failed set grade of image #{}: {}", form.image, error);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
info!(
|
||||
"Grade {} out of range for image #{}",
|
||||
form.grade, form.image
|
||||
);
|
||||
}
|
||||
not_found(&context)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct GradeForm {
|
||||
image: i32,
|
||||
grade: i16,
|
||||
}
|
||||
|
||||
fn set_location(context: Context, form: CoordForm) -> Response<Vec<u8>> {
|
||||
if !context.is_authorized() {
|
||||
return permission_denied();
|
||||
}
|
||||
let image = form.image;
|
||||
let coord = form.coord();
|
||||
info!("Should set location of #{} to {:?}.", image, coord);
|
||||
|
||||
let (lat, lng) = ((coord.x * 1e6) as i32, (coord.y * 1e6) as i32);
|
||||
use crate::schema::positions::dsl::*;
|
||||
use diesel::insert_into;
|
||||
let db = context.db();
|
||||
insert_into(positions)
|
||||
.values((photo_id.eq(image), latitude.eq(lat), longitude.eq(lng)))
|
||||
.on_conflict(photo_id)
|
||||
.do_update()
|
||||
.set((latitude.eq(lat), longitude.eq(lng)))
|
||||
.execute(db)
|
||||
.expect("Insert image position");
|
||||
|
||||
match update_image_places(db, form.image) {
|
||||
Ok(()) => (),
|
||||
// TODO Tell the user something failed?
|
||||
Err(err) => warn!("Failed to fetch places: {:?}", err),
|
||||
}
|
||||
redirect_to_img(form.image)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CoordForm {
|
||||
image: i32,
|
||||
lat: f64,
|
||||
lng: f64,
|
||||
}
|
||||
|
||||
impl CoordForm {
|
||||
fn coord(&self) -> Coord {
|
||||
Coord {
|
||||
x: self.lat,
|
||||
y: self.lng,
|
||||
}
|
||||
}
|
||||
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>)> {
|
||||
let data = req.form_body()?;
|
||||
Ok((
|
||||
data.get("image").and_then(|s| s.parse().ok()),
|
||||
data.get("grade").and_then(|s| s.parse().ok()),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn set_location<'mw>(
|
||||
req: &mut Request,
|
||||
res: Response<'mw>,
|
||||
) -> 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)) {
|
||||
info!("Should set location of #{} to {:?}.", image, coord);
|
||||
|
||||
let (lat, lng) = ((coord.x * 1e6) as i32, (coord.y * 1e6) as i32);
|
||||
use crate::schema::positions::dsl::*;
|
||||
use diesel::insert_into;
|
||||
let db: &PgConnection = &req.db_conn();
|
||||
insert_into(positions)
|
||||
.values((photo_id.eq(image), latitude.eq(lat), longitude.eq(lng)))
|
||||
.on_conflict(photo_id)
|
||||
.do_update()
|
||||
.set((latitude.eq(lat), longitude.eq(lng)))
|
||||
.execute(db)
|
||||
.expect("Insert image position");
|
||||
|
||||
match update_image_places(db, image) {
|
||||
Ok(()) => (),
|
||||
// TODO Tell the user something failed?
|
||||
Err(err) => warn!("Failed to fetch places: {:?}", err),
|
||||
}
|
||||
return res.redirect(format!("/img/{}", image));
|
||||
}
|
||||
info!("Missing image and/or position to set, or image not found.");
|
||||
res.not_found("")
|
||||
}
|
||||
|
||||
fn location_params(
|
||||
req: &mut Request,
|
||||
) -> QResult<(Option<i32>, Option<Coord>)> {
|
||||
let data = req.form_body()?;
|
||||
Ok((
|
||||
data.get("image").and_then(|s| s.parse().ok()),
|
||||
if let (Some(lat), Some(lng)) = (
|
||||
data.get("lat").and_then(|s| s.parse().ok()),
|
||||
data.get("lng").and_then(|s| s.parse().ok()),
|
||||
) {
|
||||
Some(Coord { x: lat, y: lng })
|
||||
} else {
|
||||
None
|
||||
},
|
||||
))
|
||||
}
|
||||
|
227
src/server/context.rs
Normal file
227
src/server/context.rs
Normal 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()
|
||||
}
|
@ -1,43 +1,44 @@
|
||||
#[macro_use]
|
||||
mod nickelext;
|
||||
mod admin;
|
||||
mod context;
|
||||
mod render_ructe;
|
||||
mod splitlist;
|
||||
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::views_by_date::*;
|
||||
use crate::adm::result::Error;
|
||||
use crate::env::{dburl, env_or, jwt_key, photos_dir};
|
||||
use crate::memcachemiddleware::{
|
||||
MemcacheMiddleware, MemcacheRequestExtensions,
|
||||
};
|
||||
use crate::env::{dburl, env_or, jwt_key};
|
||||
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::requestloggermiddleware::RequestLoggerMiddleware;
|
||||
use crate::templates::{self, statics, Html};
|
||||
use chrono::Datelike;
|
||||
use crate::templates::{self, Html};
|
||||
use chrono::{Datelike, Duration, Utc};
|
||||
use clap::ArgMatches;
|
||||
use diesel::pg::PgConnection;
|
||||
use diesel::prelude::*;
|
||||
use diesel::r2d2::NopErrorHandler;
|
||||
use djangohashers;
|
||||
use hyper::header::ContentType;
|
||||
use image;
|
||||
use log::{debug, info};
|
||||
use nickel::extensions::response::Redirect;
|
||||
use nickel::status::StatusCode;
|
||||
use nickel::{
|
||||
Action, Continue, FormBody, Halt, HttpRouter, MediaType, MiddlewareResult,
|
||||
Nickel, NickelError, QueryString, Request, Response,
|
||||
};
|
||||
use nickel_jwt_session::{
|
||||
SessionMiddleware, SessionRequestExtensions, SessionResponseExtensions,
|
||||
};
|
||||
use log::info;
|
||||
use mime;
|
||||
use serde::Deserialize;
|
||||
use std::net::SocketAddr;
|
||||
use warp::filters::path::Tail;
|
||||
use warp::http::{header, Response, StatusCode};
|
||||
use warp::{self, reply, Filter, Rejection, Reply};
|
||||
|
||||
/// 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 title: Option<String>,
|
||||
@ -119,114 +120,166 @@ pub fn run(args: &ArgMatches) -> Result<(), Error> {
|
||||
if let Some(pidfile) = args.value_of("PIDFILE") {
|
||||
handle_pid_file(pidfile, args.is_present("REPLACE")).unwrap()
|
||||
}
|
||||
|
||||
let mut server = Nickel::new();
|
||||
server.utilize(RequestLoggerMiddleware);
|
||||
wrap3!(server.get "/static/",.. static_file);
|
||||
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 session_filter = create_session_filter(
|
||||
&dburl(),
|
||||
env_or("MEMCACHED_SERVER", "tcp://127.0.0.1:11211"),
|
||||
jwt_key(),
|
||||
);
|
||||
|
||||
server
|
||||
.listen(&*env_or("RPHOTOS_LISTEN", "127.0.0.1:6767"))
|
||||
.map_err(|e| Error::Other(format!("listen: {}", e)))?;
|
||||
let s = move || session_filter.clone();
|
||||
use warp::filters::query::query;
|
||||
use warp::path::{end, param};
|
||||
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(())
|
||||
}
|
||||
|
||||
fn custom_errors(err: &mut NickelError, req: &mut Request) -> Action {
|
||||
if let Some(ref mut res) = err.stream {
|
||||
if res.status() == StatusCode::NotFound {
|
||||
templates::not_found(res, req).unwrap();
|
||||
return Halt(());
|
||||
/// Create custom error pages.
|
||||
fn customize_error(err: Rejection) -> Result<impl Reply, Rejection> {
|
||||
match err.status() {
|
||||
StatusCode::NOT_FOUND => {
|
||||
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>(
|
||||
req: &mut Request,
|
||||
mut res: Response<'mw>,
|
||||
) -> MiddlewareResult<'mw> {
|
||||
res.clear_jwt();
|
||||
let next = sanitize_next(req.query().get("next")).map(String::from);
|
||||
res.ok(|o| templates::login(o, req, next, None))
|
||||
fn not_found(context: &Context) -> Response<Vec<u8>> {
|
||||
Response::builder().status(StatusCode::NOT_FOUND).html(|o| {
|
||||
templates::not_found(
|
||||
o,
|
||||
context,
|
||||
StatusCode::NOT_FOUND,
|
||||
"The resource you requested could not be located.",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn do_login<'mw>(
|
||||
req: &mut Request,
|
||||
mut res: Response<'mw>,
|
||||
) -> MiddlewareResult<'mw> {
|
||||
fn redirect_to_img(image: i32) -> Response<Vec<u8>> {
|
||||
redirect(&format!("/img/{}", image))
|
||||
}
|
||||
|
||||
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 c: &PgConnection = &req.db_conn();
|
||||
let form_data = try_with!(res, req.form_body());
|
||||
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"))
|
||||
let next = sanitize_next(form.next.as_ref().map(AsRef::as_ref))
|
||||
.map(String::from);
|
||||
use crate::schema::users::dsl::*;
|
||||
if let Ok(hash) = users
|
||||
.filter(username.eq(&form.user))
|
||||
.select(password)
|
||||
.first::<String>(context.db())
|
||||
{
|
||||
use crate::schema::users::dsl::*;
|
||||
if let Ok(hash) = users
|
||||
.filter(username.eq(user))
|
||||
.select(password)
|
||||
.first::<String>(c)
|
||||
{
|
||||
debug!("Hash for {} is {}", user, hash);
|
||||
if djangohashers::check_password_tolerant(pw, &hash) {
|
||||
info!("User {} logged in", user);
|
||||
res.set_jwt_user(user);
|
||||
return res.redirect(next.unwrap_or_else(|| "/".into()));
|
||||
}
|
||||
info!(
|
||||
"Login failed: Password verification failed for {:?}",
|
||||
user,
|
||||
);
|
||||
} else {
|
||||
info!("Login failed: No hash found for {:?}", user);
|
||||
if djangohashers::check_password_tolerant(&form.password, &hash) {
|
||||
info!("User {} logged in", form.user);
|
||||
let token = context.make_token(&form.user).unwrap();
|
||||
let url = 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!(
|
||||
"Login failed: Password verification failed for {:?}",
|
||||
form.user,
|
||||
);
|
||||
} else {
|
||||
info!("Login failed: No hash found for {:?}", form.user);
|
||||
}
|
||||
next
|
||||
};
|
||||
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> {
|
||||
@ -267,12 +320,17 @@ fn test_sanitize_good_2() {
|
||||
assert_eq!(Some("/2017/7/15"), sanitize_next(Some("/2017/7/15")))
|
||||
}
|
||||
|
||||
fn logout<'mw>(
|
||||
_req: &mut Request,
|
||||
mut res: Response<'mw>,
|
||||
) -> MiddlewareResult<'mw> {
|
||||
res.clear_jwt();
|
||||
res.redirect("/")
|
||||
fn logout(_context: Context) -> Response<Vec<u8>> {
|
||||
let url = "/";
|
||||
Response::builder()
|
||||
.status(StatusCode::FOUND)
|
||||
.header(header::LOCATION, url.to_string())
|
||||
.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)]
|
||||
@ -291,63 +349,114 @@ impl SizeTag {
|
||||
}
|
||||
}
|
||||
|
||||
impl FromSlug for SizeTag {
|
||||
fn parse(slug: &str) -> Option<Self> {
|
||||
match slug {
|
||||
"s" => Some(SizeTag::Small),
|
||||
"m" => Some(SizeTag::Medium),
|
||||
"l" => Some(SizeTag::Large),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn show_image<'mw>(
|
||||
req: &Request,
|
||||
mut res: Response<'mw>,
|
||||
the_id: i32,
|
||||
size: SizeTag,
|
||||
) -> MiddlewareResult<'mw> {
|
||||
fn show_image(img: ImgName, context: Context) -> Response<Vec<u8>> {
|
||||
use crate::schema::photos::dsl::photos;
|
||||
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() {
|
||||
let path = req.photos().get_raw_path(tphoto);
|
||||
res.set((MediaType::Jpeg, far_expires()));
|
||||
return res.send_file(path);
|
||||
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(req, &tphoto, size)
|
||||
let data = get_image_data(context, &tphoto, img.size)
|
||||
.expect("Get image data");
|
||||
res.set((MediaType::Jpeg, far_expires()));
|
||||
return res.send(data);
|
||||
return Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(header::CONTENT_TYPE, mime::IMAGE_JPEG.as_ref())
|
||||
.far_expires()
|
||||
.body(data)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
res.not_found("No such image")
|
||||
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(
|
||||
req: &Request,
|
||||
context: Context,
|
||||
photo: &Photo,
|
||||
size: SizeTag,
|
||||
) -> Result<Vec<u8>, image::ImageError> {
|
||||
req.cached_or(&photo.cache_key(size), || {
|
||||
context.cached_or(&photo.cache_key(size), || {
|
||||
let size = size.px();
|
||||
req.photos().scale_image(photo, size, size)
|
||||
context.photos().scale_image(photo, size, size)
|
||||
})
|
||||
}
|
||||
|
||||
fn tag_all<'mw>(
|
||||
req: &mut Request,
|
||||
res: Response<'mw>,
|
||||
) -> MiddlewareResult<'mw> {
|
||||
fn tag_all(context: Context) -> Response<Vec<u8>> {
|
||||
use crate::schema::tags::dsl::{id, tag_name, tags};
|
||||
let c: &PgConnection = &req.db_conn();
|
||||
let query = tags.into_boxed();
|
||||
let query = if req.authorized_user().is_some() {
|
||||
let query = if context.is_authorized() {
|
||||
query
|
||||
} else {
|
||||
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)),
|
||||
)))
|
||||
};
|
||||
res.ok(|o| {
|
||||
Response::builder().html(|o| {
|
||||
templates::tags(
|
||||
o,
|
||||
req,
|
||||
&query.order(tag_name).load(c).expect("List tags"),
|
||||
&context,
|
||||
&query.order(tag_name).load(context.db()).expect("List tags"),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn tag_one<'mw>(
|
||||
req: &mut Request,
|
||||
res: Response<'mw>,
|
||||
fn tag_one(
|
||||
context: Context,
|
||||
tslug: String,
|
||||
) -> MiddlewareResult<'mw> {
|
||||
range: ImgRange,
|
||||
) -> Response<Vec<u8>> {
|
||||
use crate::schema::tags::dsl::{slug, tags};
|
||||
let c: &PgConnection = &req.db_conn();
|
||||
if let Ok(tag) = tags.filter(slug.eq(tslug)).first::<Tag>(c) {
|
||||
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(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))),
|
||||
);
|
||||
let (links, coords) = links_by_time(req, photos);
|
||||
return res.ok(|o| templates::tag(o, req, &links, &coords, &tag));
|
||||
let (links, coords) = links_by_time(&context, photos, range);
|
||||
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>(
|
||||
req: &mut Request,
|
||||
res: Response<'mw>,
|
||||
) -> MiddlewareResult<'mw> {
|
||||
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 req.authorized_user().is_some() {
|
||||
let query = if context.is_authorized() {
|
||||
query
|
||||
} else {
|
||||
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)),
|
||||
)))
|
||||
};
|
||||
let c: &PgConnection = &req.db_conn();
|
||||
res.ok(|o| {
|
||||
Response::builder().html(|o| {
|
||||
templates::places(
|
||||
o,
|
||||
req,
|
||||
&query.order(place_name).load(c).expect("List places"),
|
||||
&context,
|
||||
&query
|
||||
.order(place_name)
|
||||
.load(context.db())
|
||||
.expect("List places"),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn static_file<'mw>(
|
||||
_req: &Request,
|
||||
mut res: Response<'mw>,
|
||||
path: &str,
|
||||
) -> MiddlewareResult<'mw> {
|
||||
if let Some(s) = statics::StaticFile::get(path) {
|
||||
res.set((ContentType(s.mime()), far_expires()));
|
||||
return res.send(s.content);
|
||||
/// 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).
|
||||
fn static_file(name: Tail) -> Result<impl Reply, Rejection> {
|
||||
use templates::statics::StaticFile;
|
||||
if let Some(data) = StaticFile::get(name.as_str()) {
|
||||
Ok(Response::builder()
|
||||
.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>(
|
||||
req: &mut Request,
|
||||
res: Response<'mw>,
|
||||
fn place_one(
|
||||
context: Context,
|
||||
tslug: String,
|
||||
) -> MiddlewareResult<'mw> {
|
||||
range: ImgRange,
|
||||
) -> Response<Vec<u8>> {
|
||||
use crate::schema::places::dsl::{places, slug};
|
||||
let c: &PgConnection = &req.db_conn();
|
||||
if let Ok(place) = places.filter(slug.eq(tslug)).first::<Place>(c) {
|
||||
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(req.authorized_user().is_some()).filter(id.eq_any(
|
||||
photo_places.select(photo_id).filter(place_id.eq(place.id)),
|
||||
));
|
||||
let (links, coord) = links_by_time(req, photos);
|
||||
return res.ok(|o| templates::place(o, req, &links, &coord, &place));
|
||||
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);
|
||||
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>(
|
||||
req: &mut Request,
|
||||
res: Response<'mw>,
|
||||
) -> MiddlewareResult<'mw> {
|
||||
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 req.authorized_user().is_some() {
|
||||
let query = if context.is_authorized() {
|
||||
query
|
||||
} else {
|
||||
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)),
|
||||
)))
|
||||
};
|
||||
let c: &PgConnection = &req.db_conn();
|
||||
res.ok(|o| {
|
||||
Response::builder().html(|o| {
|
||||
templates::people(
|
||||
o,
|
||||
req,
|
||||
&query.order(person_name).load(c).expect("list people"),
|
||||
&context,
|
||||
&query
|
||||
.order(person_name)
|
||||
.load(context.db())
|
||||
.expect("list people"),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn person_one<'mw>(
|
||||
req: &mut Request,
|
||||
res: Response<'mw>,
|
||||
fn person_one(
|
||||
context: Context,
|
||||
tslug: String,
|
||||
) -> MiddlewareResult<'mw> {
|
||||
range: ImgRange,
|
||||
) -> Response<Vec<u8>> {
|
||||
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) {
|
||||
use crate::schema::photo_people::dsl::{
|
||||
person_id, photo_id, photo_people,
|
||||
};
|
||||
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_people
|
||||
.select(photo_id)
|
||||
.filter(person_id.eq(person.id)),
|
||||
),
|
||||
);
|
||||
let (links, coords) = links_by_time(req, photos);
|
||||
res.ok(|o| templates::person(o, req, &links, &coords, &person))
|
||||
let (links, coords) = links_by_time(&context, photos, range);
|
||||
Response::builder()
|
||||
.html(|o| templates::person(o, &context, &links, &coords, &person))
|
||||
} else {
|
||||
res.not_found("Not a person")
|
||||
not_found(&context)
|
||||
}
|
||||
}
|
||||
|
||||
fn random_image<'mw>(
|
||||
req: &mut Request,
|
||||
res: Response<'mw>,
|
||||
) -> MiddlewareResult<'mw> {
|
||||
fn random_image(context: Context) -> Response<Vec<u8>> {
|
||||
use crate::schema::photos::dsl::id;
|
||||
use diesel::expression::dsl::sql;
|
||||
use diesel::sql_types::Integer;
|
||||
let c: &PgConnection = &req.db_conn();
|
||||
let photo: i32 = Photo::query(req.authorized_user().is_some())
|
||||
if let Ok(photo) = Photo::query(context.is_authorized())
|
||||
.select(id)
|
||||
.limit(1)
|
||||
.order(sql::<Integer>("random()"))
|
||||
.first(c)
|
||||
.unwrap();
|
||||
info!("Random: {:?}", photo);
|
||||
res.redirect(format!("/img/{}", photo)) // to photo_details
|
||||
.first(context.db())
|
||||
{
|
||||
info!("Random: {:?}", photo);
|
||||
redirect_to_img(photo)
|
||||
} else {
|
||||
not_found(&context)
|
||||
}
|
||||
}
|
||||
|
||||
fn photo_details<'mw>(
|
||||
req: &mut Request,
|
||||
res: Response<'mw>,
|
||||
id: i32,
|
||||
) -> MiddlewareResult<'mw> {
|
||||
fn photo_details(id: i32, context: Context) -> Response<Vec<u8>> {
|
||||
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 req.authorized_user().is_some() || tphoto.is_public() {
|
||||
return res.ok(|o| {
|
||||
if context.is_authorized() || tphoto.is_public() {
|
||||
return Response::builder().html(|o| {
|
||||
templates::details(
|
||||
o,
|
||||
req,
|
||||
&context,
|
||||
&tphoto
|
||||
.date
|
||||
.map(|d| {
|
||||
@ -548,7 +659,7 @@ fn photo_details<'mw>(
|
||||
});
|
||||
}
|
||||
}
|
||||
res.not_found("Photo not found")
|
||||
not_found(&context)
|
||||
}
|
||||
|
||||
pub type Link = Html<String>;
|
||||
@ -595,38 +706,33 @@ impl Link {
|
||||
}
|
||||
}
|
||||
|
||||
fn auto_complete_tag<'mw>(
|
||||
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};
|
||||
let c: &PgConnection = &req.db_conn();
|
||||
let q = tags
|
||||
.select(tag_name)
|
||||
.filter(tag_name.ilike(q + "%"))
|
||||
.order(tag_name)
|
||||
.limit(10);
|
||||
res.send(serde_json::to_string(&q.load::<String>(c).unwrap()).unwrap())
|
||||
} else {
|
||||
res.error(StatusCode::BadRequest, "Missing 'q' parameter")
|
||||
}
|
||||
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<'mw>(
|
||||
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};
|
||||
let c: &PgConnection = &req.db_conn();
|
||||
let q = people
|
||||
.select(person_name)
|
||||
.filter(person_name.ilike(q + "%"))
|
||||
.order(person_name)
|
||||
.limit(10);
|
||||
res.send(serde_json::to_string(&q.load::<String>(c).unwrap()).unwrap())
|
||||
} else {
|
||||
res.error(StatusCode::BadRequest, "Missing 'q' parameter")
|
||||
}
|
||||
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>,
|
||||
pub to: Option<i32>,
|
||||
}
|
||||
|
@ -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)))
|
||||
}
|
26
src/server/render_ructe.rs
Normal file
26
src/server/render_ructe.rs
Normal 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()
|
||||
}
|
||||
}
|
@ -1,25 +1,25 @@
|
||||
use super::views_by_date::query_date;
|
||||
use super::PhotoLink;
|
||||
use super::views_by_date::date_of_img;
|
||||
use super::{Context, ImgRange, PhotoLink};
|
||||
use crate::models::{Coord, Photo};
|
||||
use crate::nickel_diesel::DieselRequestExtensions;
|
||||
use crate::schema::photos;
|
||||
use diesel::pg::{Pg, PgConnection};
|
||||
use diesel::prelude::*;
|
||||
use log::{debug, info, warn};
|
||||
use nickel::Request;
|
||||
|
||||
pub fn links_by_time<'a>(
|
||||
req: &mut Request,
|
||||
context: &Context,
|
||||
photos: photos::BoxedQuery<'a, Pg>,
|
||||
range: ImgRange,
|
||||
) -> (Vec<PhotoLink>, Vec<(Coord, i32)>) {
|
||||
let c: &PgConnection = &req.db_conn();
|
||||
let c = context.db();
|
||||
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))
|
||||
} else {
|
||||
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))
|
||||
} else {
|
||||
photos
|
||||
@ -30,7 +30,7 @@ pub fn links_by_time<'a>(
|
||||
.unwrap();
|
||||
(
|
||||
if let Some(groups) = split_to_groups(&photos) {
|
||||
let path = req.path_without_query().unwrap_or("/");
|
||||
let path = context.path_without_query();
|
||||
groups
|
||||
.iter()
|
||||
.map(|g| PhotoLink::for_group(g, path))
|
||||
|
@ -1,39 +1,35 @@
|
||||
use super::nickelext::MyResponse;
|
||||
use super::render_ructe::RenderRucte;
|
||||
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::nickel_diesel::DieselRequestExtensions;
|
||||
use crate::templates;
|
||||
use chrono::naive::{NaiveDate, NaiveDateTime};
|
||||
use chrono::Duration as ChDuration;
|
||||
use diesel::dsl::sql;
|
||||
use diesel::pg::PgConnection;
|
||||
use diesel::prelude::*;
|
||||
use diesel::sql_types::{BigInt, Integer, Nullable};
|
||||
use log::warn;
|
||||
use nickel::extensions::response::Redirect;
|
||||
use nickel::{MiddlewareResult, QueryString, Request, Response};
|
||||
use nickel_jwt_session::SessionRequestExtensions;
|
||||
use serde::Deserialize;
|
||||
use time;
|
||||
use warp::http::Response;
|
||||
use warp::Reply;
|
||||
|
||||
pub fn all_years<'mw>(
|
||||
req: &mut Request,
|
||||
res: Response<'mw>,
|
||||
) -> MiddlewareResult<'mw> {
|
||||
pub fn all_years(context: Context) -> impl Reply {
|
||||
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)>(
|
||||
"cast(extract(year from date) as int) y, count(*)",
|
||||
))
|
||||
.group_by(sql::<Nullable<Integer>>("y"))
|
||||
.order(sql::<Nullable<Integer>>("y").desc().nulls_last())
|
||||
.load::<(Option<i32>, i64)>(c)
|
||||
.load::<(Option<i32>, i64)>(context.db())
|
||||
.unwrap()
|
||||
.iter()
|
||||
.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()))
|
||||
.limit(1);
|
||||
let photo = if let Some(year) = year {
|
||||
@ -42,7 +38,7 @@ pub fn all_years<'mw>(
|
||||
} else {
|
||||
q.filter(date.is_null())
|
||||
};
|
||||
let photo = photo.first::<Photo>(c).unwrap();
|
||||
let photo = photo.first::<Photo>(context.db()).unwrap();
|
||||
PhotoLink {
|
||||
title: Some(
|
||||
year.map(|y| format!("{}", y))
|
||||
@ -56,23 +52,20 @@ pub fn all_years<'mw>(
|
||||
})
|
||||
.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 {
|
||||
NaiveDate::from_ymd(year, 1, 1).and_hms(0, 0, 0)
|
||||
}
|
||||
|
||||
pub fn months_in_year<'mw>(
|
||||
req: &mut Request,
|
||||
res: Response<'mw>,
|
||||
year: i32,
|
||||
) -> MiddlewareResult<'mw> {
|
||||
pub fn months_in_year(year: i32, context: Context) -> Response<Vec<u8>> {
|
||||
use crate::schema::photos::dsl::{date, grade};
|
||||
let c: &PgConnection = &req.db_conn();
|
||||
|
||||
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.lt(start_of_year(year + 1)))
|
||||
.select(sql::<(Integer, BigInt)>(
|
||||
@ -80,17 +73,17 @@ pub fn months_in_year<'mw>(
|
||||
))
|
||||
.group_by(sql::<Integer>("m"))
|
||||
.order(sql::<Integer>("m").desc().nulls_last())
|
||||
.load::<(i32, i64)>(c)
|
||||
.load::<(i32, i64)>(context.db())
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|&(month, count)| {
|
||||
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.lt(start_of_month(year, month + 1)))
|
||||
.order((grade.desc().nulls_last(), date.asc()))
|
||||
.limit(1)
|
||||
.first::<Photo>(c)
|
||||
.first::<Photo>(context.db())
|
||||
.unwrap();
|
||||
|
||||
PhotoLink {
|
||||
@ -104,17 +97,17 @@ pub fn months_in_year<'mw>(
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if groups.is_empty() {
|
||||
res.not_found("No such image")
|
||||
not_found(&context)
|
||||
} else {
|
||||
use crate::schema::positions::dsl::{
|
||||
latitude, longitude, photo_id, positions,
|
||||
};
|
||||
let pos = Photo::query(req.authorized_user().is_some())
|
||||
let pos = Photo::query(context.is_authorized())
|
||||
.inner_join(positions)
|
||||
.filter(date.ge(start_of_year(year)))
|
||||
.filter(date.lt(start_of_year(year + 1)))
|
||||
.select((photo_id, latitude, longitude))
|
||||
.load(c)
|
||||
.load(context.db())
|
||||
.map_err(|e| warn!("Failed to load positions: {}", e))
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
@ -122,7 +115,9 @@ pub fn months_in_year<'mw>(
|
||||
((lat, long).into(), p_id)
|
||||
})
|
||||
.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)
|
||||
}
|
||||
|
||||
pub fn days_in_month<'mw>(
|
||||
req: &mut Request,
|
||||
res: Response<'mw>,
|
||||
pub fn days_in_month(
|
||||
year: i32,
|
||||
month: u32,
|
||||
) -> MiddlewareResult<'mw> {
|
||||
context: Context,
|
||||
) -> Response<Vec<u8>> {
|
||||
use crate::schema::photos::dsl::{date, grade};
|
||||
let c: &PgConnection = &req.db_conn();
|
||||
|
||||
let lpath: Vec<Link> = vec![Link::year(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.lt(start_of_month(year, month + 1)))
|
||||
.select(sql::<(Integer, BigInt)>(
|
||||
@ -154,19 +147,19 @@ pub fn days_in_month<'mw>(
|
||||
))
|
||||
.group_by(sql::<Integer>("d"))
|
||||
.order(sql::<Integer>("d").desc().nulls_last())
|
||||
.load::<(i32, i64)>(c)
|
||||
.load::<(i32, i64)>(context.db())
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|&(day, count)| {
|
||||
let day = day as u32;
|
||||
let fromdate =
|
||||
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.lt(fromdate + ChDuration::days(1)))
|
||||
.order((grade.desc().nulls_last(), date.asc()))
|
||||
.limit(1)
|
||||
.first::<Photo>(c)
|
||||
.first::<Photo>(context.db())
|
||||
.unwrap();
|
||||
|
||||
PhotoLink {
|
||||
@ -180,17 +173,17 @@ pub fn days_in_month<'mw>(
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if groups.is_empty() {
|
||||
res.not_found("No such image")
|
||||
not_found(&context)
|
||||
} else {
|
||||
use crate::schema::positions::dsl::{
|
||||
latitude, longitude, photo_id, positions,
|
||||
};
|
||||
let pos = Photo::query(req.authorized_user().is_some())
|
||||
let pos = Photo::query(context.is_authorized())
|
||||
.inner_join(positions)
|
||||
.filter(date.ge(start_of_month(year, month)))
|
||||
.filter(date.lt(start_of_month(year, month + 1)))
|
||||
.select((photo_id, latitude, longitude))
|
||||
.load(c)
|
||||
.load(context.db())
|
||||
.map_err(|e| warn!("Failed to load positions: {}", e))
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
@ -198,28 +191,26 @@ pub fn days_in_month<'mw>(
|
||||
((lat, long).into(), p_id)
|
||||
})
|
||||
.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>(
|
||||
req: &mut Request,
|
||||
res: Response<'mw>,
|
||||
) -> MiddlewareResult<'mw> {
|
||||
pub fn all_null_date(context: Context) -> impl Reply {
|
||||
use crate::schema::photos::dsl::{date, path};
|
||||
|
||||
let c: &PgConnection = &req.db_conn();
|
||||
res.ok(|o| {
|
||||
Response::builder().html(|o| {
|
||||
templates::index(
|
||||
o,
|
||||
req,
|
||||
&context,
|
||||
"Photos without a date",
|
||||
&[],
|
||||
&Photo::query(req.authorized_user().is_some())
|
||||
&Photo::query(context.is_authorized())
|
||||
.filter(date.is_null())
|
||||
.order(path.asc())
|
||||
.limit(500)
|
||||
.load(c)
|
||||
.load(context.db())
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(PhotoLink::from)
|
||||
@ -229,28 +220,28 @@ pub fn all_null_date<'mw>(
|
||||
})
|
||||
}
|
||||
|
||||
pub fn all_for_day<'mw>(
|
||||
req: &mut Request,
|
||||
res: Response<'mw>,
|
||||
pub fn all_for_day(
|
||||
year: i32,
|
||||
month: u32,
|
||||
day: u32,
|
||||
) -> MiddlewareResult<'mw> {
|
||||
range: ImgRange,
|
||||
context: Context,
|
||||
) -> impl Reply {
|
||||
let thedate = NaiveDate::from_ymd(year, month, day).and_hms(0, 0, 0);
|
||||
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.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() {
|
||||
res.not_found("No such image")
|
||||
not_found(&context)
|
||||
} else {
|
||||
res.ok(|o| {
|
||||
Response::builder().html(|o| {
|
||||
templates::index(
|
||||
o,
|
||||
req,
|
||||
&context,
|
||||
&format!("Photos from {} {} {}", day, monthname(month), year),
|
||||
&[Link::year(year), Link::month(year, month)],
|
||||
&links,
|
||||
@ -260,41 +251,37 @@ pub fn all_for_day<'mw>(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_this_day<'mw>(
|
||||
req: &mut Request,
|
||||
res: Response<'mw>,
|
||||
) -> MiddlewareResult<'mw> {
|
||||
pub fn on_this_day(context: Context) -> impl Reply {
|
||||
use crate::schema::photos::dsl::{date, grade};
|
||||
use crate::schema::positions::dsl::{
|
||||
latitude, longitude, photo_id, positions,
|
||||
};
|
||||
let c: &PgConnection = &req.db_conn();
|
||||
|
||||
let (month, day) = {
|
||||
let now = time::now();
|
||||
(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)
|
||||
.filter(
|
||||
sql("extract(month from date)=").bind::<Integer, _>(month as i32),
|
||||
)
|
||||
.filter(sql("extract(day from date)=").bind::<Integer, _>(day as i32))
|
||||
.select((photo_id, latitude, longitude))
|
||||
.load(c)
|
||||
.load(context.db())
|
||||
.map_err(|e| warn!("Failed to load positions: {}", e))
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|(p_id, lat, long): (i32, i32, i32)| ((lat, long).into(), p_id))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
res.ok(|o| {
|
||||
Response::builder().html(|o| {
|
||||
templates::index(
|
||||
o,
|
||||
req,
|
||||
&context,
|
||||
&format!("Photos from {} {}", day, monthname(month)),
|
||||
&[],
|
||||
&Photo::query(req.authorized_user().is_some())
|
||||
&Photo::query(context.is_authorized())
|
||||
.select(sql::<(Integer, BigInt)>(
|
||||
"cast(extract(year from date) as int) y, count(*)",
|
||||
))
|
||||
@ -308,19 +295,19 @@ pub fn on_this_day<'mw>(
|
||||
.bind::<Integer, _>(day as i32),
|
||||
)
|
||||
.order(sql::<Integer>("y").desc())
|
||||
.load::<(i32, i64)>(c)
|
||||
.load::<(i32, i64)>(context.db())
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|&(year, count)| {
|
||||
let fromdate =
|
||||
NaiveDate::from_ymd(year, month as u32, 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.lt(fromdate + ChDuration::days(1)))
|
||||
.order((grade.desc().nulls_last(), date.asc()))
|
||||
.limit(1)
|
||||
.first::<Photo>(c)
|
||||
.first::<Photo>(context.db())
|
||||
.unwrap();
|
||||
|
||||
PhotoLink {
|
||||
@ -337,65 +324,48 @@ pub fn on_this_day<'mw>(
|
||||
})
|
||||
}
|
||||
|
||||
pub fn next_image<'mw>(
|
||||
req: &mut Request,
|
||||
res: Response<'mw>,
|
||||
) -> MiddlewareResult<'mw> {
|
||||
pub fn next_image(context: Context, param: FromParam) -> impl Reply {
|
||||
use crate::schema::photos::dsl::{date, id};
|
||||
if let Some((from_id, from_date)) = query_date(req, "from") {
|
||||
let q = Photo::query(req.authorized_user().is_some())
|
||||
if let Some(from_date) = date_of_img(context.db(), param.from) {
|
||||
let q = Photo::query(context.is_authorized())
|
||||
.select(id)
|
||||
.filter(
|
||||
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));
|
||||
let c: &PgConnection = &req.db_conn();
|
||||
if let Ok(photo) = q.first::<i32>(c) {
|
||||
return res.redirect(format!("/img/{}", photo)); // to photo_details
|
||||
if let Ok(photo) = q.first::<i32>(context.db()) {
|
||||
return redirect_to_img(photo);
|
||||
}
|
||||
}
|
||||
res.not_found("No such image")
|
||||
not_found(&context)
|
||||
}
|
||||
|
||||
pub fn prev_image<'mw>(
|
||||
req: &mut Request,
|
||||
res: Response<'mw>,
|
||||
) -> MiddlewareResult<'mw> {
|
||||
pub fn prev_image(context: Context, param: FromParam) -> impl Reply {
|
||||
use crate::schema::photos::dsl::{date, id};
|
||||
if let Some((from_id, from_date)) = query_date(req, "from") {
|
||||
let q = Photo::query(req.authorized_user().is_some())
|
||||
if let Some(from_date) = date_of_img(context.db(), param.from) {
|
||||
let q = Photo::query(context.is_authorized())
|
||||
.select(id)
|
||||
.filter(
|
||||
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()));
|
||||
let c: &PgConnection = &req.db_conn();
|
||||
if let Ok(photo) = q.first::<i32>(c) {
|
||||
return res.redirect(format!("/img/{}", photo)); // to photo_details
|
||||
if let Ok(photo) = q.first::<i32>(context.db()) {
|
||||
return redirect_to_img(photo);
|
||||
}
|
||||
}
|
||||
res.not_found("No such image")
|
||||
not_found(&context)
|
||||
}
|
||||
|
||||
pub fn query_date(
|
||||
req: &mut Request,
|
||||
name: &str,
|
||||
) -> Option<(i32, NaiveDateTime)> {
|
||||
req.query()
|
||||
.get(name)
|
||||
.and_then(|s| s.parse().ok())
|
||||
.and_then(|i: i32| {
|
||||
use crate::schema::photos::dsl::{date, photos};
|
||||
let c: &PgConnection = &req.db_conn();
|
||||
photos
|
||||
.find(i)
|
||||
.select(date)
|
||||
.first(c)
|
||||
.unwrap_or(None)
|
||||
.map(|d| (i, d))
|
||||
})
|
||||
#[derive(Deserialize)]
|
||||
pub struct FromParam {
|
||||
from: i32,
|
||||
}
|
||||
|
||||
pub fn date_of_img(db: &PgConnection, photo_id: i32) -> Option<NaiveDateTime> {
|
||||
use crate::schema::photos::dsl::{date, photos};
|
||||
photos.find(photo_id).select(date).first(db).unwrap_or(None)
|
||||
}
|
||||
|
||||
pub fn monthname(n: u32) -> &'static str {
|
||||
|
@ -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 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)
|
||||
@:page_base(req, "Photo details", lpath, {
|
||||
@(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, {
|
||||
<meta property='og:title' content='Photo @if let Some(d) = photo.date {(@d.format("%F"))}'>
|
||||
<meta property='og:type' content='image' />
|
||||
<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="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">
|
||||
@if req.authorized_user().is_some() {
|
||||
@if context.is_authorized() {
|
||||
<p><a href="/img/@photo.id-l.jpg">@photo.path</a></p>
|
||||
@if photo.is_public() {<p>This photo is public.</p>}
|
||||
else {<p>This photo is not public.</p>}
|
||||
|
37
templates/error.rs.html
Normal file
37
templates/error.rs.html
Normal 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>
|
@ -1,8 +1,6 @@
|
||||
@use nickel::Request;
|
||||
@use nickel_jwt_session::SessionRequestExtensions;
|
||||
@use crate::server::Link;
|
||||
@use crate::server::{Context, Link};
|
||||
|
||||
@(req: &Request, lpath: &[Link])
|
||||
@(context: &Context, lpath: &[Link])
|
||||
|
||||
<header>
|
||||
<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="/thisday">On this day</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>}
|
||||
else {<span class="user">(<a href="/login@if let Some(p) = req.path_without_query() {?next=@p}">log in</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?next=@context.path_without_query()">log in</a>)</span>}
|
||||
</header>
|
||||
|
@ -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 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)>
|
||||
@for p in photos {@:photo_link(p)}
|
||||
</div>
|
||||
|
@ -1,9 +1,9 @@
|
||||
@use nickel::Request;
|
||||
@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">
|
||||
@if let Some(message) = message {<p>@message</p>}
|
||||
<p><label for="user">User:</label>
|
||||
|
@ -1,13 +1,13 @@
|
||||
@use nickel::Request;
|
||||
@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", &[], {}, {
|
||||
<p>No page or photo match that url.</p>
|
||||
@if req.authorized_user().is_none() {
|
||||
@:page_base(context, code.canonical_reason().unwrap_or("error"), &[], {}, {
|
||||
<p>@message (@code.as_u16())</p>
|
||||
@if !context.is_authorized() {
|
||||
<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>
|
||||
}
|
||||
})
|
||||
|
@ -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 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>
|
||||
<html>
|
||||
@ -13,7 +11,7 @@
|
||||
<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"/>
|
||||
@if req.authorized_user().is_some() {
|
||||
@if context.is_authorized() {
|
||||
<script src="/static/@admin_js.name" type="text/javascript" defer>
|
||||
</script>
|
||||
}
|
||||
@ -22,7 +20,7 @@
|
||||
@:meta()
|
||||
</head>
|
||||
<body>
|
||||
@:head(req, lpath)
|
||||
@:head(context, lpath)
|
||||
<main>
|
||||
<h1>@title</h1>
|
||||
@:content()
|
||||
|
@ -1,9 +1,9 @@
|
||||
@use nickel::Request;
|
||||
@use crate::models::Person;
|
||||
@use super::page_base;
|
||||
@use crate::models::Person;
|
||||
@use crate::server::Context;
|
||||
|
||||
@(req: &Request, people: &[Person])
|
||||
@:page_base(req, "Photo people", &[], {}, {
|
||||
@(context: &Context, people: &[Person])
|
||||
@:page_base(context, "Photo people", &[], {}, {
|
||||
<ul class="allpeople">
|
||||
@for p in people {
|
||||
<li><a href="/person/@p.slug">@p.person_name</a>
|
||||
|
@ -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 crate::models::{Coord, Person};
|
||||
@use crate::server::{Context, PhotoLink};
|
||||
|
||||
@(req: &Request, photos: &[PhotoLink], coords: &[(Coord, i32)], person: &Person)
|
||||
@:page_base(req, &format!("Photos with {}", person.person_name), &[], {}, {
|
||||
@(context: &Context, photos: &[PhotoLink], coords: &[(Coord, i32)], person: &Person)
|
||||
@:page_base(context, &format!("Photos with {}", person.person_name), &[], {}, {
|
||||
<div class="group"@:data_positions(coords)>
|
||||
@for p in photos {@:photo_link(p)}
|
||||
</div>
|
||||
|
@ -1,10 +1,9 @@
|
||||
@use nickel::Request;
|
||||
@use crate::models::{Coord, Place};
|
||||
@use crate::server::PhotoLink;
|
||||
@use crate::server::{Context, PhotoLink};
|
||||
@use super::{data_positions, page_base, photo_link};
|
||||
|
||||
@(req: &Request, photos: &[PhotoLink], coords: &[(Coord, i32)], place: &Place)
|
||||
@:page_base(req, &format!("Photos from {}", place.place_name), &[], {}, {
|
||||
@(context: &Context, photos: &[PhotoLink], coords: &[(Coord, i32)], place: &Place)
|
||||
@:page_base(context, &format!("Photos from {}", place.place_name), &[], {}, {
|
||||
<div class="group"@:data_positions(coords)>
|
||||
@for p in photos {@:photo_link(p)}
|
||||
</div>
|
||||
|
@ -1,10 +1,10 @@
|
||||
@use nickel::Request;
|
||||
@use crate::models::Place;
|
||||
@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">
|
||||
@for p in places {
|
||||
<li><a href="/place/@p.slug">@p.place_name</a>
|
||||
|
@ -1,11 +1,10 @@
|
||||
@use nickel::Request;
|
||||
@use crate::models::{Coord, Tag};
|
||||
@use crate::server::PhotoLink;
|
||||
@use crate::server::{Context, PhotoLink};
|
||||
@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)>
|
||||
@for p in photos {@:photo_link(p)}
|
||||
</div>
|
||||
|
@ -1,9 +1,9 @@
|
||||
@use nickel::Request;
|
||||
@use crate::models::Tag;
|
||||
@use super::page_base;
|
||||
@use crate::models::Tag;
|
||||
@use crate::server::Context;
|
||||
|
||||
@(req: &Request, tags: &[Tag])
|
||||
@:page_base(req, "Photo tags", &[], {}, {
|
||||
@(context: &Context, tags: &[Tag])
|
||||
@:page_base(context, "Photo tags", &[], {}, {
|
||||
<ul class="alltags">
|
||||
@for tag in tags {
|
||||
<li><a href="/tag/@tag.slug">@tag.tag_name</a>
|
||||
|
Loading…
Reference in New Issue
Block a user