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 = "src/build.rs"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
ructe = { version = "^0.6.2", features = ["sass", "mime02"] }
|
ructe = { version = "^0.6.2", features = ["sass", "mime03"] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
nickel = "~0.10.0"
|
warp = "0.1.6"
|
||||||
nickel-jwt-session = "~0.10.0"
|
|
||||||
hyper = "~0.10.0"
|
|
||||||
env_logger = "*"
|
env_logger = "*"
|
||||||
libc = "*"
|
libc = "*"
|
||||||
log = "*"
|
log = "*"
|
||||||
chrono = "~0.4.0" # Must match version used by diesel
|
chrono = "~0.4.0" # Must match version used by diesel
|
||||||
clap = { version = "^2.19", features = [ "color", "wrap_help" ] }
|
clap = { version = "^2.19", features = [ "color", "wrap_help" ] }
|
||||||
typemap = "*"
|
|
||||||
plugin = "*"
|
|
||||||
image = "0.21"
|
image = "0.21"
|
||||||
|
jwt = "0.4.0"
|
||||||
time = "*"
|
time = "*"
|
||||||
kamadak-exif = "~0.3.0"
|
kamadak-exif = "~0.3.0"
|
||||||
diesel = { version = "1.4.0", features = ["r2d2", "chrono", "postgres"] }
|
diesel = { version = "1.4.0", features = ["r2d2", "chrono", "postgres"] }
|
||||||
dotenv = "0.13.0"
|
dotenv = "0.13.0"
|
||||||
djangohashers = "*"
|
djangohashers = "*"
|
||||||
rand = "0.6.5"
|
rand = "0.6.5"
|
||||||
|
rust-crypto = "0.2.36"
|
||||||
memcached-rs = "0.4.1"
|
memcached-rs = "0.4.1"
|
||||||
flate2 = "^1.0.0"
|
flate2 = "^1.0.0"
|
||||||
brotli2 = "*"
|
brotli2 = "*"
|
||||||
mime = "0.2.6"
|
mime = "0.3.0"
|
||||||
regex = "*"
|
regex = "*"
|
||||||
slug = "0.1"
|
slug = "0.1"
|
||||||
reqwest = "0.9"
|
reqwest = "0.9"
|
||||||
|
serde = { version = "1.0.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
@ -2,20 +2,14 @@
|
|||||||
#![recursion_limit = "128"]
|
#![recursion_limit = "128"]
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate diesel;
|
extern crate diesel;
|
||||||
#[macro_use]
|
|
||||||
extern crate nickel;
|
|
||||||
|
|
||||||
mod adm;
|
mod adm;
|
||||||
mod env;
|
mod env;
|
||||||
mod fetch_places;
|
mod fetch_places;
|
||||||
mod memcachemiddleware;
|
|
||||||
mod models;
|
mod models;
|
||||||
mod myexif;
|
mod myexif;
|
||||||
mod nickel_diesel;
|
|
||||||
mod photosdir;
|
mod photosdir;
|
||||||
mod photosdirmiddleware;
|
|
||||||
mod pidfiles;
|
mod pidfiles;
|
||||||
mod requestloggermiddleware;
|
|
||||||
mod schema;
|
mod schema;
|
||||||
mod server;
|
mod server;
|
||||||
|
|
||||||
|
@ -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,129 +1,123 @@
|
|||||||
//! Admin-only views, generally called by javascript.
|
//! Admin-only views, generally called by javascript.
|
||||||
use super::nickelext::MyResponse;
|
use super::{not_found, permission_denied, redirect_to_img, Context, SizeTag};
|
||||||
use super::SizeTag;
|
|
||||||
use crate::fetch_places::update_image_places;
|
use crate::fetch_places::update_image_places;
|
||||||
use crate::memcachemiddleware::MemcacheRequestExtensions;
|
|
||||||
use crate::models::{Coord, Photo};
|
use crate::models::{Coord, Photo};
|
||||||
use crate::nickel_diesel::DieselRequestExtensions;
|
use diesel::{self, prelude::*};
|
||||||
use diesel::prelude::*;
|
|
||||||
use log::{info, warn};
|
use log::{info, warn};
|
||||||
use nickel::extensions::Redirect;
|
use serde::Deserialize;
|
||||||
use nickel::status::StatusCode;
|
|
||||||
use nickel::{BodyError, FormBody, MiddlewareResult, Request, Response};
|
|
||||||
use nickel_jwt_session::SessionRequestExtensions;
|
|
||||||
use slug::slugify;
|
use slug::slugify;
|
||||||
|
use warp::filters::BoxedFilter;
|
||||||
|
use warp::http::Response;
|
||||||
|
use warp::{Filter, Reply};
|
||||||
|
|
||||||
pub fn rotate<'mw>(
|
pub fn routes(s: BoxedFilter<(Context,)>) -> BoxedFilter<(impl Reply,)> {
|
||||||
req: &mut Request,
|
use warp::{body::form, path, post2 as post};
|
||||||
res: Response<'mw>,
|
let route = path("grade")
|
||||||
) -> MiddlewareResult<'mw> {
|
.and(s.clone())
|
||||||
if req.authorized_user().is_none() {
|
.and(form())
|
||||||
return res.error(StatusCode::Unauthorized, "permission denied");
|
.map(set_grade)
|
||||||
|
.or(path("locate").and(s.clone()).and(form()).map(set_location))
|
||||||
|
.unify()
|
||||||
|
.or(path("person").and(s.clone()).and(form()).map(set_person))
|
||||||
|
.unify()
|
||||||
|
.or(path("rotate").and(s.clone()).and(form()).map(rotate))
|
||||||
|
.unify()
|
||||||
|
.or(path("tag").and(s.clone()).and(form()).map(set_tag))
|
||||||
|
.unify();
|
||||||
|
post().and(route).boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rotate(context: Context, form: RotateForm) -> Response<Vec<u8>> {
|
||||||
|
if !context.is_authorized() {
|
||||||
|
return permission_denied();
|
||||||
}
|
}
|
||||||
if let (Some(image), Some(angle)) = try_with!(res, rotate_params(req)) {
|
info!("Should rotate #{} by {}", form.image, form.angle);
|
||||||
info!("Should rotate #{} by {}", image, angle);
|
|
||||||
use crate::schema::photos::dsl::photos;
|
use crate::schema::photos::dsl::photos;
|
||||||
let c: &PgConnection = &req.db_conn();
|
let c = context.db();
|
||||||
if let Ok(mut image) = photos.find(image).first::<Photo>(c) {
|
if let Ok(mut image) = photos.find(form.image).first::<Photo>(c) {
|
||||||
let newvalue = (360 + image.rotation + angle) % 360;
|
let newvalue = (360 + image.rotation + form.angle) % 360;
|
||||||
info!("Rotation was {}, setting to {}", image.rotation, newvalue);
|
info!("Rotation was {}, setting to {}", image.rotation, newvalue);
|
||||||
image.rotation = newvalue;
|
image.rotation = newvalue;
|
||||||
match image.save_changes::<Photo>(c) {
|
match image.save_changes::<Photo>(c) {
|
||||||
Ok(image) => {
|
Ok(image) => {
|
||||||
req.clear_cache(&image.cache_key(SizeTag::Small));
|
context.clear_cache(&image.cache_key(SizeTag::Small));
|
||||||
req.clear_cache(&image.cache_key(SizeTag::Medium));
|
context.clear_cache(&image.cache_key(SizeTag::Medium));
|
||||||
return res.ok(|o| writeln!(o, "ok"));
|
return Response::builder().body(b"ok".to_vec()).unwrap();
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
warn!("Failed to save image #{}: {}", image.id, error);
|
warn!("Failed to save image #{}: {}", image.id, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
not_found(&context)
|
||||||
info!("Missing image and/or angle to rotate or image not found");
|
|
||||||
res.not_found("")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type QResult<T> = Result<T, (StatusCode, BodyError)>;
|
#[derive(Deserialize)]
|
||||||
|
struct RotateForm {
|
||||||
fn rotate_params(req: &mut Request) -> QResult<(Option<i32>, Option<i16>)> {
|
image: i32,
|
||||||
let data = req.form_body()?;
|
angle: i16,
|
||||||
Ok((
|
|
||||||
data.get("image").and_then(|s| s.parse().ok()),
|
|
||||||
data.get("angle").and_then(|s| s.parse().ok()),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_tag<'mw>(
|
fn set_tag(context: Context, form: TagForm) -> Response<Vec<u8>> {
|
||||||
req: &mut Request,
|
if !context.is_authorized() {
|
||||||
res: Response<'mw>,
|
return permission_denied();
|
||||||
) -> MiddlewareResult<'mw> {
|
|
||||||
if req.authorized_user().is_none() {
|
|
||||||
return res.error(StatusCode::Unauthorized, "permission denied");
|
|
||||||
}
|
}
|
||||||
if let (Some(image), Some(tag)) = try_with!(res, tag_params(req)) {
|
let c = context.db();
|
||||||
let c: &PgConnection = &req.db_conn();
|
|
||||||
use crate::models::{PhotoTag, Tag};
|
use crate::models::{PhotoTag, Tag};
|
||||||
use diesel;
|
use diesel;
|
||||||
let tag = {
|
let tag = {
|
||||||
use crate::schema::tags::dsl::*;
|
use crate::schema::tags::dsl::*;
|
||||||
tags.filter(tag_name.ilike(&tag))
|
tags.filter(tag_name.ilike(&form.tag))
|
||||||
.first::<Tag>(c)
|
.first::<Tag>(c)
|
||||||
.or_else(|_| {
|
.or_else(|_| {
|
||||||
diesel::insert_into(tags)
|
diesel::insert_into(tags)
|
||||||
.values((tag_name.eq(&tag), slug.eq(&slugify(&tag))))
|
.values((
|
||||||
|
tag_name.eq(&form.tag),
|
||||||
|
slug.eq(&slugify(&form.tag)),
|
||||||
|
))
|
||||||
.get_result::<Tag>(c)
|
.get_result::<Tag>(c)
|
||||||
})
|
})
|
||||||
.expect("Find or create tag")
|
.expect("Find or create tag")
|
||||||
};
|
};
|
||||||
use crate::schema::photo_tags::dsl::*;
|
use crate::schema::photo_tags::dsl::*;
|
||||||
let q = photo_tags
|
let q = photo_tags
|
||||||
.filter(photo_id.eq(image))
|
.filter(photo_id.eq(form.image))
|
||||||
.filter(tag_id.eq(tag.id));
|
.filter(tag_id.eq(tag.id));
|
||||||
if q.first::<PhotoTag>(c).is_ok() {
|
if q.first::<PhotoTag>(c).is_ok() {
|
||||||
info!("Photo #{} already has {:?}", image, tag);
|
info!("Photo #{} already has {:?}", form.image, form.tag);
|
||||||
} else {
|
} else {
|
||||||
info!("Add {:?} on photo #{}!", tag, image);
|
info!("Add {:?} on photo #{}!", form.tag, form.image);
|
||||||
diesel::insert_into(photo_tags)
|
diesel::insert_into(photo_tags)
|
||||||
.values((photo_id.eq(image), tag_id.eq(tag.id)))
|
.values((photo_id.eq(form.image), tag_id.eq(tag.id)))
|
||||||
.execute(c)
|
.execute(c)
|
||||||
.expect("Tag a photo");
|
.expect("Tag a photo");
|
||||||
}
|
}
|
||||||
return res.redirect(format!("/img/{}", image));
|
redirect_to_img(form.image)
|
||||||
}
|
|
||||||
info!("Missing image and/or angle to rotate or image not found");
|
|
||||||
res.not_found("")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tag_params(req: &mut Request) -> QResult<(Option<i32>, Option<String>)> {
|
#[derive(Deserialize)]
|
||||||
let data = req.form_body()?;
|
struct TagForm {
|
||||||
Ok((
|
image: i32,
|
||||||
data.get("image").and_then(|s| s.parse().ok()),
|
tag: String,
|
||||||
data.get("tag").map(String::from),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_person<'mw>(
|
fn set_person(context: Context, form: PersonForm) -> Response<Vec<u8>> {
|
||||||
req: &mut Request,
|
if !context.is_authorized() {
|
||||||
res: Response<'mw>,
|
return permission_denied();
|
||||||
) -> MiddlewareResult<'mw> {
|
|
||||||
if req.authorized_user().is_none() {
|
|
||||||
return res.error(StatusCode::Unauthorized, "permission denied");
|
|
||||||
}
|
}
|
||||||
if let (Some(image), Some(name)) = try_with!(res, person_params(req)) {
|
let c = context.db();
|
||||||
let c: &PgConnection = &req.db_conn();
|
|
||||||
use crate::models::{Person, PhotoPerson};
|
use crate::models::{Person, PhotoPerson};
|
||||||
use diesel;
|
use diesel;
|
||||||
let person = {
|
let person = {
|
||||||
use crate::schema::people::dsl::*;
|
use crate::schema::people::dsl::*;
|
||||||
people
|
people
|
||||||
.filter(person_name.ilike(&name))
|
.filter(person_name.ilike(&form.person))
|
||||||
.first::<Person>(c)
|
.first::<Person>(c)
|
||||||
.or_else(|_| {
|
.or_else(|_| {
|
||||||
diesel::insert_into(people)
|
diesel::insert_into(people)
|
||||||
.values((
|
.values((
|
||||||
person_name.eq(&name),
|
person_name.eq(&form.person),
|
||||||
slug.eq(&slugify(&name)),
|
slug.eq(&slugify(&form.person)),
|
||||||
))
|
))
|
||||||
.get_result::<Person>(c)
|
.get_result::<Person>(c)
|
||||||
})
|
})
|
||||||
@ -131,87 +125,74 @@ pub fn set_person<'mw>(
|
|||||||
};
|
};
|
||||||
use crate::schema::photo_people::dsl::*;
|
use crate::schema::photo_people::dsl::*;
|
||||||
let q = photo_people
|
let q = photo_people
|
||||||
.filter(photo_id.eq(image))
|
.filter(photo_id.eq(form.image))
|
||||||
.filter(person_id.eq(person.id));
|
.filter(person_id.eq(person.id));
|
||||||
if q.first::<PhotoPerson>(c).is_ok() {
|
if q.first::<PhotoPerson>(c).is_ok() {
|
||||||
info!("Photo #{} already has {:?}", image, person);
|
info!("Photo #{} already has {:?}", form.image, person);
|
||||||
} else {
|
} else {
|
||||||
info!("Add {:?} on photo #{}!", person, image);
|
info!("Add {:?} on photo #{}!", person, form.image);
|
||||||
diesel::insert_into(photo_people)
|
diesel::insert_into(photo_people)
|
||||||
.values((photo_id.eq(image), person_id.eq(person.id)))
|
.values((photo_id.eq(form.image), person_id.eq(person.id)))
|
||||||
.execute(c)
|
.execute(c)
|
||||||
.expect("Name person in photo");
|
.expect("Name person in photo");
|
||||||
}
|
}
|
||||||
return res.redirect(format!("/img/{}", image));
|
redirect_to_img(form.image)
|
||||||
}
|
|
||||||
info!("Missing image and/or angle to rotate or image not found");
|
|
||||||
res.not_found("")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn person_params(req: &mut Request) -> QResult<(Option<i32>, Option<String>)> {
|
#[derive(Deserialize)]
|
||||||
let data = req.form_body()?;
|
struct PersonForm {
|
||||||
Ok((
|
image: i32,
|
||||||
data.get("image").and_then(|s| s.parse().ok()),
|
person: String,
|
||||||
data.get("person").map(String::from),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_grade<'mw>(
|
fn set_grade(context: Context, form: GradeForm) -> Response<Vec<u8>> {
|
||||||
req: &mut Request,
|
if !context.is_authorized() {
|
||||||
res: Response<'mw>,
|
return permission_denied();
|
||||||
) -> MiddlewareResult<'mw> {
|
|
||||||
if req.authorized_user().is_none() {
|
|
||||||
return res.error(StatusCode::Unauthorized, "permission denied");
|
|
||||||
}
|
}
|
||||||
if let (Some(image), Some(newgrade)) = try_with!(res, grade_params(req)) {
|
if form.grade >= 0 && form.grade <= 100 {
|
||||||
if newgrade >= 0 && newgrade <= 100 {
|
info!("Should set grade of #{} to {}", form.image, form.grade);
|
||||||
info!("Should set grade of #{} to {}", image, newgrade);
|
|
||||||
use crate::schema::photos::dsl::{grade, photos};
|
use crate::schema::photos::dsl::{grade, photos};
|
||||||
use diesel;
|
let q =
|
||||||
let c: &PgConnection = &req.db_conn();
|
diesel::update(photos.find(form.image)).set(grade.eq(form.grade));
|
||||||
let q = diesel::update(photos.find(image)).set(grade.eq(newgrade));
|
match q.execute(context.db()) {
|
||||||
match q.execute(c) {
|
|
||||||
Ok(1) => {
|
Ok(1) => {
|
||||||
return res.redirect(format!("/img/{}", image));
|
return redirect_to_img(form.image);
|
||||||
}
|
}
|
||||||
Ok(0) => (),
|
Ok(0) => (),
|
||||||
Ok(n) => {
|
Ok(n) => {
|
||||||
warn!("Strange, updated {} images with id {}", n, image);
|
warn!("Strange, updated {} images with id {}", n, form.image);
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
warn!("Failed set grade of image #{}: {}", image, error);
|
warn!("Failed set grade of image #{}: {}", form.image, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
info!("Grade {} is out of range for image #{}", newgrade, image);
|
info!(
|
||||||
|
"Grade {} out of range for image #{}",
|
||||||
|
form.grade, form.image
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
not_found(&context)
|
||||||
info!("Missing image and/or angle to rotate or image not found");
|
|
||||||
res.not_found("")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn grade_params(req: &mut Request) -> QResult<(Option<i32>, Option<i16>)> {
|
#[derive(Deserialize)]
|
||||||
let data = req.form_body()?;
|
struct GradeForm {
|
||||||
Ok((
|
image: i32,
|
||||||
data.get("image").and_then(|s| s.parse().ok()),
|
grade: i16,
|
||||||
data.get("grade").and_then(|s| s.parse().ok()),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_location<'mw>(
|
fn set_location(context: Context, form: CoordForm) -> Response<Vec<u8>> {
|
||||||
req: &mut Request,
|
if !context.is_authorized() {
|
||||||
res: Response<'mw>,
|
return permission_denied();
|
||||||
) -> MiddlewareResult<'mw> {
|
|
||||||
if req.authorized_user().is_none() {
|
|
||||||
return res.error(StatusCode::Unauthorized, "permission denied");
|
|
||||||
}
|
}
|
||||||
if let (Some(image), Some(coord)) = try_with!(res, location_params(req)) {
|
let image = form.image;
|
||||||
|
let coord = form.coord();
|
||||||
info!("Should set location of #{} to {:?}.", image, coord);
|
info!("Should set location of #{} to {:?}.", image, coord);
|
||||||
|
|
||||||
let (lat, lng) = ((coord.x * 1e6) as i32, (coord.y * 1e6) as i32);
|
let (lat, lng) = ((coord.x * 1e6) as i32, (coord.y * 1e6) as i32);
|
||||||
use crate::schema::positions::dsl::*;
|
use crate::schema::positions::dsl::*;
|
||||||
use diesel::insert_into;
|
use diesel::insert_into;
|
||||||
let db: &PgConnection = &req.db_conn();
|
let db = context.db();
|
||||||
insert_into(positions)
|
insert_into(positions)
|
||||||
.values((photo_id.eq(image), latitude.eq(lat), longitude.eq(lng)))
|
.values((photo_id.eq(image), latitude.eq(lat), longitude.eq(lng)))
|
||||||
.on_conflict(photo_id)
|
.on_conflict(photo_id)
|
||||||
@ -220,30 +201,26 @@ pub fn set_location<'mw>(
|
|||||||
.execute(db)
|
.execute(db)
|
||||||
.expect("Insert image position");
|
.expect("Insert image position");
|
||||||
|
|
||||||
match update_image_places(db, image) {
|
match update_image_places(db, form.image) {
|
||||||
Ok(()) => (),
|
Ok(()) => (),
|
||||||
// TODO Tell the user something failed?
|
// TODO Tell the user something failed?
|
||||||
Err(err) => warn!("Failed to fetch places: {:?}", err),
|
Err(err) => warn!("Failed to fetch places: {:?}", err),
|
||||||
}
|
}
|
||||||
return res.redirect(format!("/img/{}", image));
|
redirect_to_img(form.image)
|
||||||
}
|
|
||||||
info!("Missing image and/or position to set, or image not found.");
|
|
||||||
res.not_found("")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn location_params(
|
#[derive(Deserialize)]
|
||||||
req: &mut Request,
|
struct CoordForm {
|
||||||
) -> QResult<(Option<i32>, Option<Coord>)> {
|
image: i32,
|
||||||
let data = req.form_body()?;
|
lat: f64,
|
||||||
Ok((
|
lng: f64,
|
||||||
data.get("image").and_then(|s| s.parse().ok()),
|
}
|
||||||
if let (Some(lat), Some(lng)) = (
|
|
||||||
data.get("lat").and_then(|s| s.parse().ok()),
|
impl CoordForm {
|
||||||
data.get("lng").and_then(|s| s.parse().ok()),
|
fn coord(&self) -> Coord {
|
||||||
) {
|
Coord {
|
||||||
Some(Coord { x: lat, y: lng })
|
x: self.lat,
|
||||||
} else {
|
y: self.lng,
|
||||||
None
|
}
|
||||||
},
|
}
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
227
src/server/context.rs
Normal file
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]
|
#[macro_use]
|
||||||
mod nickelext;
|
|
||||||
mod admin;
|
mod admin;
|
||||||
|
mod context;
|
||||||
|
mod render_ructe;
|
||||||
mod splitlist;
|
mod splitlist;
|
||||||
mod views_by_date;
|
mod views_by_date;
|
||||||
|
|
||||||
use self::nickelext::{far_expires, FromSlug, MyResponse};
|
use self::context::create_session_filter;
|
||||||
|
pub use self::context::Context;
|
||||||
|
use self::render_ructe::RenderRucte;
|
||||||
use self::splitlist::*;
|
use self::splitlist::*;
|
||||||
use self::views_by_date::*;
|
use self::views_by_date::*;
|
||||||
use crate::adm::result::Error;
|
use crate::adm::result::Error;
|
||||||
use crate::env::{dburl, env_or, jwt_key, photos_dir};
|
use crate::env::{dburl, env_or, jwt_key};
|
||||||
use crate::memcachemiddleware::{
|
|
||||||
MemcacheMiddleware, MemcacheRequestExtensions,
|
|
||||||
};
|
|
||||||
use crate::models::{Person, Photo, Place, Tag};
|
use crate::models::{Person, Photo, Place, Tag};
|
||||||
use crate::nickel_diesel::{DieselMiddleware, DieselRequestExtensions};
|
|
||||||
use crate::photosdirmiddleware::{
|
|
||||||
PhotosDirMiddleware, PhotosDirRequestExtensions,
|
|
||||||
};
|
|
||||||
use crate::pidfiles::handle_pid_file;
|
use crate::pidfiles::handle_pid_file;
|
||||||
use crate::requestloggermiddleware::RequestLoggerMiddleware;
|
use crate::templates::{self, Html};
|
||||||
use crate::templates::{self, statics, Html};
|
use chrono::{Datelike, Duration, Utc};
|
||||||
use chrono::Datelike;
|
|
||||||
use clap::ArgMatches;
|
use clap::ArgMatches;
|
||||||
use diesel::pg::PgConnection;
|
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use diesel::r2d2::NopErrorHandler;
|
|
||||||
use djangohashers;
|
use djangohashers;
|
||||||
use hyper::header::ContentType;
|
|
||||||
use image;
|
use image;
|
||||||
use log::{debug, info};
|
use log::info;
|
||||||
use nickel::extensions::response::Redirect;
|
use mime;
|
||||||
use nickel::status::StatusCode;
|
use serde::Deserialize;
|
||||||
use nickel::{
|
use std::net::SocketAddr;
|
||||||
Action, Continue, FormBody, Halt, HttpRouter, MediaType, MiddlewareResult,
|
use warp::filters::path::Tail;
|
||||||
Nickel, NickelError, QueryString, Request, Response,
|
use warp::http::{header, Response, StatusCode};
|
||||||
};
|
use warp::{self, reply, Filter, Rejection, Reply};
|
||||||
use nickel_jwt_session::{
|
|
||||||
SessionMiddleware, SessionRequestExtensions, SessionResponseExtensions,
|
/// Trait to easily add a far expires header to a response builder.
|
||||||
};
|
trait FarExpires {
|
||||||
|
fn far_expires(&mut self) -> &mut Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FarExpires for warp::http::response::Builder {
|
||||||
|
fn far_expires(&mut self) -> &mut Self {
|
||||||
|
let far_expires = Utc::now() + Duration::days(180);
|
||||||
|
self.header(header::EXPIRES, far_expires.to_rfc2822())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct PhotoLink {
|
pub struct PhotoLink {
|
||||||
pub title: Option<String>,
|
pub title: Option<String>,
|
||||||
@ -119,114 +120,166 @@ pub fn run(args: &ArgMatches) -> Result<(), Error> {
|
|||||||
if let Some(pidfile) = args.value_of("PIDFILE") {
|
if let Some(pidfile) = args.value_of("PIDFILE") {
|
||||||
handle_pid_file(pidfile, args.is_present("REPLACE")).unwrap()
|
handle_pid_file(pidfile, args.is_present("REPLACE")).unwrap()
|
||||||
}
|
}
|
||||||
|
let session_filter = create_session_filter(
|
||||||
let mut server = Nickel::new();
|
&dburl(),
|
||||||
server.utilize(RequestLoggerMiddleware);
|
env_or("MEMCACHED_SERVER", "tcp://127.0.0.1:11211"),
|
||||||
wrap3!(server.get "/static/",.. static_file);
|
jwt_key(),
|
||||||
server.utilize(MemcacheMiddleware::new(vec![(
|
|
||||||
"tcp://127.0.0.1:11211".into(),
|
|
||||||
1,
|
|
||||||
)]));
|
|
||||||
server.utilize(SessionMiddleware::new(&jwt_key()));
|
|
||||||
let dm: DieselMiddleware<PgConnection> =
|
|
||||||
DieselMiddleware::new(&dburl(), 5, Box::new(NopErrorHandler)).unwrap();
|
|
||||||
server.utilize(dm);
|
|
||||||
server.utilize(PhotosDirMiddleware::new(photos_dir()));
|
|
||||||
|
|
||||||
wrap3!(server.get "/login", login);
|
|
||||||
wrap3!(server.post "/login", do_login);
|
|
||||||
wrap3!(server.get "/logout", logout);
|
|
||||||
wrap3!(server.get "/", all_years);
|
|
||||||
use self::admin::{rotate, set_grade, set_location, set_person, set_tag};
|
|
||||||
wrap3!(server.get "/ac/tag", auto_complete_tag);
|
|
||||||
wrap3!(server.get "/ac/person", auto_complete_person);
|
|
||||||
wrap3!(server.post "/adm/grade", set_grade);
|
|
||||||
wrap3!(server.post "/adm/person", set_person);
|
|
||||||
wrap3!(server.post "/adm/rotate", rotate);
|
|
||||||
wrap3!(server.post "/adm/tag", set_tag);
|
|
||||||
wrap3!(server.post "/adm/locate", set_location);
|
|
||||||
wrap3!(server.get "/img/{}[-]{}\\.jpg", show_image: id, size);
|
|
||||||
wrap3!(server.get "/img/{}", photo_details: id);
|
|
||||||
wrap3!(server.get "/next", next_image);
|
|
||||||
wrap3!(server.get "/prev", prev_image);
|
|
||||||
wrap3!(server.get "/tag/", tag_all);
|
|
||||||
wrap3!(server.get "/tag/{}", tag_one: tag);
|
|
||||||
wrap3!(server.get "/place/", place_all);
|
|
||||||
wrap3!(server.get "/place/{}", place_one: slug);
|
|
||||||
wrap3!(server.get "/person/", person_all);
|
|
||||||
wrap3!(server.get "/person/{}", person_one: slug);
|
|
||||||
wrap3!(server.get "/random", random_image);
|
|
||||||
wrap3!(server.get "/0/", all_null_date);
|
|
||||||
wrap3!(server.get "/{}/", months_in_year: year);
|
|
||||||
wrap3!(server.get "/{}/{}/", days_in_month: year, month);
|
|
||||||
wrap3!(server.get "/{}/{}/{}", all_for_day: year, month, day);
|
|
||||||
wrap3!(server.get "/thisday", on_this_day);
|
|
||||||
|
|
||||||
server.handle_error(
|
|
||||||
custom_errors as fn(&mut NickelError, &mut Request) -> Action,
|
|
||||||
);
|
);
|
||||||
|
let s = move || session_filter.clone();
|
||||||
server
|
use warp::filters::query::query;
|
||||||
.listen(&*env_or("RPHOTOS_LISTEN", "127.0.0.1:6767"))
|
use warp::path::{end, param};
|
||||||
.map_err(|e| Error::Other(format!("listen: {}", e)))?;
|
use warp::{body, get2 as get, path, post2 as post};
|
||||||
|
let static_routes = path("static")
|
||||||
|
.and(get())
|
||||||
|
.and(path::tail())
|
||||||
|
.and_then(static_file);
|
||||||
|
#[rustfmt::skip]
|
||||||
|
let routes = warp::any()
|
||||||
|
.and(static_routes)
|
||||||
|
.or(get().and(path("login")).and(end()).and(s()).and(query()).map(login))
|
||||||
|
.or(post().and(path("login")).and(end()).and(s()).and(body::form()).map(do_login))
|
||||||
|
.or(path("logout").and(end()).and(s()).map(logout))
|
||||||
|
.or(get().and(end()).and(s()).map(all_years))
|
||||||
|
.or(get().and(path("img")).and(param()).and(end()).and(s()).map(photo_details))
|
||||||
|
.or(get().and(path("img")).and(param()).and(end()).and(s()).map(show_image))
|
||||||
|
.or(get().and(path("0")).and(end()).and(s()).map(all_null_date))
|
||||||
|
.or(get().and(param()).and(end()).and(s()).map(months_in_year))
|
||||||
|
.or(get().and(param()).and(param()).and(end()).and(s()).map(days_in_month))
|
||||||
|
.or(get().and(param()).and(param()).and(param()).and(end()).and(query()).and(s()).map(all_for_day))
|
||||||
|
.or(get().and(path("person")).and(end()).and(s()).map(person_all))
|
||||||
|
.or(get().and(path("person")).and(s()).and(param()).and(end()).and(query()).map(person_one))
|
||||||
|
.or(get().and(path("place")).and(end()).and(s()).map(place_all))
|
||||||
|
.or(get().and(path("place")).and(s()).and(param()).and(end()).and(query()).map(place_one))
|
||||||
|
.or(get().and(path("tag")).and(end()).and(s()).map(tag_all))
|
||||||
|
.or(get().and(path("tag")).and(s()).and(param()).and(end()).and(query()).map(tag_one))
|
||||||
|
.or(get().and(path("random")).and(end()).and(s()).map(random_image))
|
||||||
|
.or(get().and(path("thisday")).and(end()).and(s()).map(on_this_day))
|
||||||
|
.or(get().and(path("next")).and(end()).and(s()).and(query()).map(next_image))
|
||||||
|
.or(get().and(path("prev")).and(end()).and(s()).and(query()).map(prev_image))
|
||||||
|
.or(get().and(path("ac")).and(path("tag")).and(s()).and(query()).map(auto_complete_tag))
|
||||||
|
.or(get().and(path("ac")).and(path("person")).and(s()).and(query()).map(auto_complete_person))
|
||||||
|
.or(path("adm").and(admin::routes(s())));
|
||||||
|
let addr = env_or("RPHOTOS_LISTEN", "127.0.0.1:6767")
|
||||||
|
.parse::<SocketAddr>()
|
||||||
|
.map_err(|e| Error::Other(format!("{}", e)))?;
|
||||||
|
warp::serve(routes.recover(customize_error)).run(addr);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn custom_errors(err: &mut NickelError, req: &mut Request) -> Action {
|
/// Create custom error pages.
|
||||||
if let Some(ref mut res) = err.stream {
|
fn customize_error(err: Rejection) -> Result<impl Reply, Rejection> {
|
||||||
if res.status() == StatusCode::NotFound {
|
match err.status() {
|
||||||
templates::not_found(res, req).unwrap();
|
StatusCode::NOT_FOUND => {
|
||||||
return Halt(());
|
eprintln!("Got a 404: {:?}", err);
|
||||||
|
Ok(Response::builder().status(StatusCode::NOT_FOUND).html(|o| {
|
||||||
|
templates::error(
|
||||||
|
o,
|
||||||
|
StatusCode::NOT_FOUND,
|
||||||
|
"The resource you requested could not be located.",
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
code => {
|
||||||
|
eprintln!("Got a {}: {:?}", code.as_u16(), err);
|
||||||
|
Ok(Response::builder()
|
||||||
|
.status(code)
|
||||||
|
.html(|o| templates::error(o, code, "Something went wrong.")))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Continue(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn login<'mw>(
|
fn not_found(context: &Context) -> Response<Vec<u8>> {
|
||||||
req: &mut Request,
|
Response::builder().status(StatusCode::NOT_FOUND).html(|o| {
|
||||||
mut res: Response<'mw>,
|
templates::not_found(
|
||||||
) -> MiddlewareResult<'mw> {
|
o,
|
||||||
res.clear_jwt();
|
context,
|
||||||
let next = sanitize_next(req.query().get("next")).map(String::from);
|
StatusCode::NOT_FOUND,
|
||||||
res.ok(|o| templates::login(o, req, next, None))
|
"The resource you requested could not be located.",
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn do_login<'mw>(
|
fn redirect_to_img(image: i32) -> Response<Vec<u8>> {
|
||||||
req: &mut Request,
|
redirect(&format!("/img/{}", image))
|
||||||
mut res: Response<'mw>,
|
}
|
||||||
) -> MiddlewareResult<'mw> {
|
|
||||||
|
fn redirect(url: &str) -> Response<Vec<u8>> {
|
||||||
|
Response::builder()
|
||||||
|
.status(StatusCode::FOUND)
|
||||||
|
.header(header::LOCATION, url)
|
||||||
|
.body(format!("Please refer to {}", url).into_bytes())
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn permission_denied() -> Response<Vec<u8>> {
|
||||||
|
error_response(StatusCode::UNAUTHORIZED)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn error_response(err: StatusCode) -> Response<Vec<u8>> {
|
||||||
|
Response::builder()
|
||||||
|
.status(err)
|
||||||
|
.html(|o| templates::error(o, err, "Sorry about this."))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn login(context: Context, param: NextQ) -> Response<Vec<u8>> {
|
||||||
|
info!("Got request for login form. Param: {:?}", param);
|
||||||
|
let next = sanitize_next(param.next.as_ref().map(AsRef::as_ref))
|
||||||
|
.map(String::from);
|
||||||
|
Response::builder().html(|o| templates::login(o, &context, next, None))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Deserialize)]
|
||||||
|
struct NextQ {
|
||||||
|
next: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn do_login(context: Context, form: LoginForm) -> Response<Vec<u8>> {
|
||||||
let next = {
|
let next = {
|
||||||
let c: &PgConnection = &req.db_conn();
|
let next = sanitize_next(form.next.as_ref().map(AsRef::as_ref))
|
||||||
let form_data = try_with!(res, req.form_body());
|
.map(String::from);
|
||||||
let next = sanitize_next(form_data.get("next")).map(String::from);
|
|
||||||
if let (Some(user), Some(pw)) =
|
|
||||||
(form_data.get("user"), form_data.get("password"))
|
|
||||||
{
|
|
||||||
use crate::schema::users::dsl::*;
|
use crate::schema::users::dsl::*;
|
||||||
if let Ok(hash) = users
|
if let Ok(hash) = users
|
||||||
.filter(username.eq(user))
|
.filter(username.eq(&form.user))
|
||||||
.select(password)
|
.select(password)
|
||||||
.first::<String>(c)
|
.first::<String>(context.db())
|
||||||
{
|
{
|
||||||
debug!("Hash for {} is {}", user, hash);
|
if djangohashers::check_password_tolerant(&form.password, &hash) {
|
||||||
if djangohashers::check_password_tolerant(pw, &hash) {
|
info!("User {} logged in", form.user);
|
||||||
info!("User {} logged in", user);
|
let token = context.make_token(&form.user).unwrap();
|
||||||
res.set_jwt_user(user);
|
let url = next.unwrap_or_else(|| "/".into());
|
||||||
return res.redirect(next.unwrap_or_else(|| "/".into()));
|
return Response::builder()
|
||||||
|
.status(StatusCode::FOUND)
|
||||||
|
.header(header::LOCATION, url.clone())
|
||||||
|
.header(
|
||||||
|
header::SET_COOKIE,
|
||||||
|
format!(
|
||||||
|
"EXAUTH={}; SameSite=Strict; HttpOpnly",
|
||||||
|
token
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.body(format!("Please refer to {}", url).into_bytes())
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
info!(
|
info!(
|
||||||
"Login failed: Password verification failed for {:?}",
|
"Login failed: Password verification failed for {:?}",
|
||||||
user,
|
form.user,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
info!("Login failed: No hash found for {:?}", user);
|
info!("Login failed: No hash found for {:?}", form.user);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
next
|
next
|
||||||
};
|
};
|
||||||
let message = Some("Login failed, please try again");
|
let message = Some("Login failed, please try again");
|
||||||
res.ok(|o| templates::login(o, req, next, message))
|
Response::builder().html(|o| templates::login(o, &context, next, message))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The data submitted by the login form.
|
||||||
|
/// This does not derive Debug or Serialize, as the password is plain text.
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct LoginForm {
|
||||||
|
user: String,
|
||||||
|
password: String,
|
||||||
|
next: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sanitize_next(next: Option<&str>) -> Option<&str> {
|
fn sanitize_next(next: Option<&str>) -> Option<&str> {
|
||||||
@ -267,12 +320,17 @@ fn test_sanitize_good_2() {
|
|||||||
assert_eq!(Some("/2017/7/15"), sanitize_next(Some("/2017/7/15")))
|
assert_eq!(Some("/2017/7/15"), sanitize_next(Some("/2017/7/15")))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn logout<'mw>(
|
fn logout(_context: Context) -> Response<Vec<u8>> {
|
||||||
_req: &mut Request,
|
let url = "/";
|
||||||
mut res: Response<'mw>,
|
Response::builder()
|
||||||
) -> MiddlewareResult<'mw> {
|
.status(StatusCode::FOUND)
|
||||||
res.clear_jwt();
|
.header(header::LOCATION, url.to_string())
|
||||||
res.redirect("/")
|
.header(
|
||||||
|
header::SET_COOKIE,
|
||||||
|
"EXAUTH=; Max-Age=0; SameSite=Strict; HttpOpnly".to_string(),
|
||||||
|
)
|
||||||
|
.body(format!("Please refer to {}", url).into_bytes())
|
||||||
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
@ -291,63 +349,114 @@ impl SizeTag {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromSlug for SizeTag {
|
fn show_image(img: ImgName, context: Context) -> Response<Vec<u8>> {
|
||||||
fn parse(slug: &str) -> Option<Self> {
|
use crate::schema::photos::dsl::photos;
|
||||||
match slug {
|
if let Ok(tphoto) = photos.find(img.id).first::<Photo>(context.db()) {
|
||||||
"s" => Some(SizeTag::Small),
|
if context.is_authorized() || tphoto.is_public() {
|
||||||
"m" => Some(SizeTag::Medium),
|
if img.size == SizeTag::Large {
|
||||||
"l" => Some(SizeTag::Large),
|
if context.is_authorized() {
|
||||||
_ => None,
|
use std::fs::File;
|
||||||
|
use std::io::Read;
|
||||||
|
// TODO: This should be done in a more async-friendly way.
|
||||||
|
let path = context.photos().get_raw_path(tphoto);
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
if File::open(path)
|
||||||
|
.map(|mut f| f.read_to_end(&mut buf))
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
return Response::builder()
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.header(
|
||||||
|
header::CONTENT_TYPE,
|
||||||
|
mime::IMAGE_JPEG.as_ref(),
|
||||||
|
)
|
||||||
|
.far_expires()
|
||||||
|
.body(buf)
|
||||||
|
.unwrap();
|
||||||
|
} else {
|
||||||
|
return error_response(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
let data = get_image_data(context, &tphoto, img.size)
|
||||||
|
.expect("Get image data");
|
||||||
|
return Response::builder()
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.header(header::CONTENT_TYPE, mime::IMAGE_JPEG.as_ref())
|
||||||
|
.far_expires()
|
||||||
|
.body(data)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
not_found(&context)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A client-side / url file name for a file.
|
||||||
|
/// Someting like 4711-s.jpg
|
||||||
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
|
struct ImgName {
|
||||||
|
id: i32,
|
||||||
|
size: SizeTag,
|
||||||
|
}
|
||||||
|
use std::str::FromStr;
|
||||||
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
|
struct BadImgName {}
|
||||||
|
impl FromStr for ImgName {
|
||||||
|
type Err = BadImgName;
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
if let Some(pos) = s.find('-') {
|
||||||
|
let (num, rest) = s.split_at(pos);
|
||||||
|
let id = num.parse().map_err(|_| BadImgName {})?;
|
||||||
|
let size = match rest {
|
||||||
|
"-s.jpg" => SizeTag::Small,
|
||||||
|
"-m.jpg" => SizeTag::Medium,
|
||||||
|
"-l.jpg" => SizeTag::Large,
|
||||||
|
_ => return Err(BadImgName {}),
|
||||||
|
};
|
||||||
|
return Ok(ImgName { id, size });
|
||||||
|
}
|
||||||
|
Err(BadImgName {})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_image<'mw>(
|
#[test]
|
||||||
req: &Request,
|
fn parse_good_imgname() {
|
||||||
mut res: Response<'mw>,
|
assert_eq!(
|
||||||
the_id: i32,
|
"4711-s.jpg".parse(),
|
||||||
size: SizeTag,
|
Ok(ImgName {
|
||||||
) -> MiddlewareResult<'mw> {
|
id: 4711,
|
||||||
use crate::schema::photos::dsl::photos;
|
size: SizeTag::Small,
|
||||||
let c: &PgConnection = &req.db_conn();
|
})
|
||||||
if let Ok(tphoto) = photos.find(the_id).first::<Photo>(c) {
|
)
|
||||||
if req.authorized_user().is_some() || tphoto.is_public() {
|
}
|
||||||
if size == SizeTag::Large {
|
|
||||||
if req.authorized_user().is_some() {
|
#[test]
|
||||||
let path = req.photos().get_raw_path(tphoto);
|
fn parse_bad_imgname_1() {
|
||||||
res.set((MediaType::Jpeg, far_expires()));
|
assert_eq!("4711-q.jpg".parse::<ImgName>(), Err(BadImgName {}))
|
||||||
return res.send_file(path);
|
}
|
||||||
}
|
#[test]
|
||||||
} else {
|
fn parse_bad_imgname_2() {
|
||||||
let data = get_image_data(req, &tphoto, size)
|
assert_eq!("blurgel".parse::<ImgName>(), Err(BadImgName {}))
|
||||||
.expect("Get image data");
|
|
||||||
res.set((MediaType::Jpeg, far_expires()));
|
|
||||||
return res.send(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
res.not_found("No such image")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_image_data(
|
fn get_image_data(
|
||||||
req: &Request,
|
context: Context,
|
||||||
photo: &Photo,
|
photo: &Photo,
|
||||||
size: SizeTag,
|
size: SizeTag,
|
||||||
) -> Result<Vec<u8>, image::ImageError> {
|
) -> Result<Vec<u8>, image::ImageError> {
|
||||||
req.cached_or(&photo.cache_key(size), || {
|
context.cached_or(&photo.cache_key(size), || {
|
||||||
let size = size.px();
|
let size = size.px();
|
||||||
req.photos().scale_image(photo, size, size)
|
context.photos().scale_image(photo, size, size)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tag_all<'mw>(
|
fn tag_all(context: Context) -> Response<Vec<u8>> {
|
||||||
req: &mut Request,
|
|
||||||
res: Response<'mw>,
|
|
||||||
) -> MiddlewareResult<'mw> {
|
|
||||||
use crate::schema::tags::dsl::{id, tag_name, tags};
|
use crate::schema::tags::dsl::{id, tag_name, tags};
|
||||||
let c: &PgConnection = &req.db_conn();
|
|
||||||
let query = tags.into_boxed();
|
let query = tags.into_boxed();
|
||||||
let query = if req.authorized_user().is_some() {
|
let query = if context.is_authorized() {
|
||||||
query
|
query
|
||||||
} else {
|
} else {
|
||||||
use crate::schema::photo_tags::dsl as tp;
|
use crate::schema::photo_tags::dsl as tp;
|
||||||
@ -356,41 +465,39 @@ fn tag_all<'mw>(
|
|||||||
tp::photo_id.eq_any(p::photos.select(p::id).filter(p::is_public)),
|
tp::photo_id.eq_any(p::photos.select(p::id).filter(p::is_public)),
|
||||||
)))
|
)))
|
||||||
};
|
};
|
||||||
res.ok(|o| {
|
Response::builder().html(|o| {
|
||||||
templates::tags(
|
templates::tags(
|
||||||
o,
|
o,
|
||||||
req,
|
&context,
|
||||||
&query.order(tag_name).load(c).expect("List tags"),
|
&query.order(tag_name).load(context.db()).expect("List tags"),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tag_one<'mw>(
|
fn tag_one(
|
||||||
req: &mut Request,
|
context: Context,
|
||||||
res: Response<'mw>,
|
|
||||||
tslug: String,
|
tslug: String,
|
||||||
) -> MiddlewareResult<'mw> {
|
range: ImgRange,
|
||||||
|
) -> Response<Vec<u8>> {
|
||||||
use crate::schema::tags::dsl::{slug, tags};
|
use crate::schema::tags::dsl::{slug, tags};
|
||||||
let c: &PgConnection = &req.db_conn();
|
if let Ok(tag) = tags.filter(slug.eq(tslug)).first::<Tag>(context.db()) {
|
||||||
if let Ok(tag) = tags.filter(slug.eq(tslug)).first::<Tag>(c) {
|
|
||||||
use crate::schema::photo_tags::dsl::{photo_id, photo_tags, tag_id};
|
use crate::schema::photo_tags::dsl::{photo_id, photo_tags, tag_id};
|
||||||
use crate::schema::photos::dsl::id;
|
use crate::schema::photos::dsl::id;
|
||||||
let photos = Photo::query(req.authorized_user().is_some()).filter(
|
let photos = Photo::query(context.is_authorized()).filter(
|
||||||
id.eq_any(photo_tags.select(photo_id).filter(tag_id.eq(tag.id))),
|
id.eq_any(photo_tags.select(photo_id).filter(tag_id.eq(tag.id))),
|
||||||
);
|
);
|
||||||
let (links, coords) = links_by_time(req, photos);
|
let (links, coords) = links_by_time(&context, photos, range);
|
||||||
return res.ok(|o| templates::tag(o, req, &links, &coords, &tag));
|
Response::builder()
|
||||||
|
.html(|o| templates::tag(o, &context, &links, &coords, &tag))
|
||||||
|
} else {
|
||||||
|
not_found(&context)
|
||||||
}
|
}
|
||||||
res.not_found("Not a tag")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn place_all<'mw>(
|
fn place_all(context: Context) -> Response<Vec<u8>> {
|
||||||
req: &mut Request,
|
|
||||||
res: Response<'mw>,
|
|
||||||
) -> MiddlewareResult<'mw> {
|
|
||||||
use crate::schema::places::dsl::{id, place_name, places};
|
use crate::schema::places::dsl::{id, place_name, places};
|
||||||
let query = places.into_boxed();
|
let query = places.into_boxed();
|
||||||
let query = if req.authorized_user().is_some() {
|
let query = if context.is_authorized() {
|
||||||
query
|
query
|
||||||
} else {
|
} else {
|
||||||
use crate::schema::photo_places::dsl as pp;
|
use crate::schema::photo_places::dsl as pp;
|
||||||
@ -399,57 +506,63 @@ fn place_all<'mw>(
|
|||||||
pp::photo_id.eq_any(p::photos.select(p::id).filter(p::is_public)),
|
pp::photo_id.eq_any(p::photos.select(p::id).filter(p::is_public)),
|
||||||
)))
|
)))
|
||||||
};
|
};
|
||||||
let c: &PgConnection = &req.db_conn();
|
Response::builder().html(|o| {
|
||||||
res.ok(|o| {
|
|
||||||
templates::places(
|
templates::places(
|
||||||
o,
|
o,
|
||||||
req,
|
&context,
|
||||||
&query.order(place_name).load(c).expect("List places"),
|
&query
|
||||||
|
.order(place_name)
|
||||||
|
.load(context.db())
|
||||||
|
.expect("List places"),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn static_file<'mw>(
|
/// Handler for static files.
|
||||||
_req: &Request,
|
/// Create a response from the file data with a correct content type
|
||||||
mut res: Response<'mw>,
|
/// and a far expires header (or a 404 if the file does not exist).
|
||||||
path: &str,
|
fn static_file(name: Tail) -> Result<impl Reply, Rejection> {
|
||||||
) -> MiddlewareResult<'mw> {
|
use templates::statics::StaticFile;
|
||||||
if let Some(s) = statics::StaticFile::get(path) {
|
if let Some(data) = StaticFile::get(name.as_str()) {
|
||||||
res.set((ContentType(s.mime()), far_expires()));
|
Ok(Response::builder()
|
||||||
return res.send(s.content);
|
.status(StatusCode::OK)
|
||||||
|
.header(header::CONTENT_TYPE, data.mime.as_ref())
|
||||||
|
.far_expires()
|
||||||
|
.body(data.content))
|
||||||
|
} else {
|
||||||
|
println!("Static file {:?} not found", name);
|
||||||
|
Err(warp::reject::not_found())
|
||||||
}
|
}
|
||||||
res.not_found("No such file")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn place_one<'mw>(
|
fn place_one(
|
||||||
req: &mut Request,
|
context: Context,
|
||||||
res: Response<'mw>,
|
|
||||||
tslug: String,
|
tslug: String,
|
||||||
) -> MiddlewareResult<'mw> {
|
range: ImgRange,
|
||||||
|
) -> Response<Vec<u8>> {
|
||||||
use crate::schema::places::dsl::{places, slug};
|
use crate::schema::places::dsl::{places, slug};
|
||||||
let c: &PgConnection = &req.db_conn();
|
if let Ok(place) =
|
||||||
if let Ok(place) = places.filter(slug.eq(tslug)).first::<Place>(c) {
|
places.filter(slug.eq(tslug)).first::<Place>(context.db())
|
||||||
|
{
|
||||||
use crate::schema::photo_places::dsl::{
|
use crate::schema::photo_places::dsl::{
|
||||||
photo_id, photo_places, place_id,
|
photo_id, photo_places, place_id,
|
||||||
};
|
};
|
||||||
use crate::schema::photos::dsl::id;
|
use crate::schema::photos::dsl::id;
|
||||||
let photos =
|
let photos = Photo::query(context.is_authorized()).filter(id.eq_any(
|
||||||
Photo::query(req.authorized_user().is_some()).filter(id.eq_any(
|
|
||||||
photo_places.select(photo_id).filter(place_id.eq(place.id)),
|
photo_places.select(photo_id).filter(place_id.eq(place.id)),
|
||||||
));
|
));
|
||||||
let (links, coord) = links_by_time(req, photos);
|
let (links, coord) = links_by_time(&context, photos, range);
|
||||||
return res.ok(|o| templates::place(o, req, &links, &coord, &place));
|
Response::builder()
|
||||||
|
.html(|o| templates::place(o, &context, &links, &coord, &place))
|
||||||
|
} else {
|
||||||
|
not_found(&context)
|
||||||
}
|
}
|
||||||
res.not_found("Not a place")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn person_all<'mw>(
|
fn person_all(context: Context) -> Response<Vec<u8>> {
|
||||||
req: &mut Request,
|
|
||||||
res: Response<'mw>,
|
|
||||||
) -> MiddlewareResult<'mw> {
|
|
||||||
use crate::schema::people::dsl::{id, people, person_name};
|
use crate::schema::people::dsl::{id, people, person_name};
|
||||||
let query = people.into_boxed();
|
let query = people.into_boxed();
|
||||||
let query = if req.authorized_user().is_some() {
|
let query = if context.is_authorized() {
|
||||||
query
|
query
|
||||||
} else {
|
} else {
|
||||||
use crate::schema::photo_people::dsl as pp;
|
use crate::schema::photo_people::dsl as pp;
|
||||||
@ -458,73 +571,71 @@ fn person_all<'mw>(
|
|||||||
pp::photo_id.eq_any(p::photos.select(p::id).filter(p::is_public)),
|
pp::photo_id.eq_any(p::photos.select(p::id).filter(p::is_public)),
|
||||||
)))
|
)))
|
||||||
};
|
};
|
||||||
let c: &PgConnection = &req.db_conn();
|
Response::builder().html(|o| {
|
||||||
res.ok(|o| {
|
|
||||||
templates::people(
|
templates::people(
|
||||||
o,
|
o,
|
||||||
req,
|
&context,
|
||||||
&query.order(person_name).load(c).expect("list people"),
|
&query
|
||||||
|
.order(person_name)
|
||||||
|
.load(context.db())
|
||||||
|
.expect("list people"),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn person_one<'mw>(
|
fn person_one(
|
||||||
req: &mut Request,
|
context: Context,
|
||||||
res: Response<'mw>,
|
|
||||||
tslug: String,
|
tslug: String,
|
||||||
) -> MiddlewareResult<'mw> {
|
range: ImgRange,
|
||||||
|
) -> Response<Vec<u8>> {
|
||||||
use crate::schema::people::dsl::{people, slug};
|
use crate::schema::people::dsl::{people, slug};
|
||||||
let c: &PgConnection = &req.db_conn();
|
let c = context.db();
|
||||||
if let Ok(person) = people.filter(slug.eq(tslug)).first::<Person>(c) {
|
if let Ok(person) = people.filter(slug.eq(tslug)).first::<Person>(c) {
|
||||||
use crate::schema::photo_people::dsl::{
|
use crate::schema::photo_people::dsl::{
|
||||||
person_id, photo_id, photo_people,
|
person_id, photo_id, photo_people,
|
||||||
};
|
};
|
||||||
use crate::schema::photos::dsl::id;
|
use crate::schema::photos::dsl::id;
|
||||||
let photos = Photo::query(req.authorized_user().is_some()).filter(
|
let photos = Photo::query(context.is_authorized()).filter(
|
||||||
id.eq_any(
|
id.eq_any(
|
||||||
photo_people
|
photo_people
|
||||||
.select(photo_id)
|
.select(photo_id)
|
||||||
.filter(person_id.eq(person.id)),
|
.filter(person_id.eq(person.id)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
let (links, coords) = links_by_time(req, photos);
|
let (links, coords) = links_by_time(&context, photos, range);
|
||||||
res.ok(|o| templates::person(o, req, &links, &coords, &person))
|
Response::builder()
|
||||||
|
.html(|o| templates::person(o, &context, &links, &coords, &person))
|
||||||
} else {
|
} else {
|
||||||
res.not_found("Not a person")
|
not_found(&context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn random_image<'mw>(
|
fn random_image(context: Context) -> Response<Vec<u8>> {
|
||||||
req: &mut Request,
|
|
||||||
res: Response<'mw>,
|
|
||||||
) -> MiddlewareResult<'mw> {
|
|
||||||
use crate::schema::photos::dsl::id;
|
use crate::schema::photos::dsl::id;
|
||||||
use diesel::expression::dsl::sql;
|
use diesel::expression::dsl::sql;
|
||||||
use diesel::sql_types::Integer;
|
use diesel::sql_types::Integer;
|
||||||
let c: &PgConnection = &req.db_conn();
|
if let Ok(photo) = Photo::query(context.is_authorized())
|
||||||
let photo: i32 = Photo::query(req.authorized_user().is_some())
|
|
||||||
.select(id)
|
.select(id)
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.order(sql::<Integer>("random()"))
|
.order(sql::<Integer>("random()"))
|
||||||
.first(c)
|
.first(context.db())
|
||||||
.unwrap();
|
{
|
||||||
info!("Random: {:?}", photo);
|
info!("Random: {:?}", photo);
|
||||||
res.redirect(format!("/img/{}", photo)) // to photo_details
|
redirect_to_img(photo)
|
||||||
|
} else {
|
||||||
|
not_found(&context)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn photo_details<'mw>(
|
fn photo_details(id: i32, context: Context) -> Response<Vec<u8>> {
|
||||||
req: &mut Request,
|
|
||||||
res: Response<'mw>,
|
|
||||||
id: i32,
|
|
||||||
) -> MiddlewareResult<'mw> {
|
|
||||||
use crate::schema::photos::dsl::photos;
|
use crate::schema::photos::dsl::photos;
|
||||||
let c: &PgConnection = &req.db_conn();
|
let c = context.db();
|
||||||
if let Ok(tphoto) = photos.find(id).first::<Photo>(c) {
|
if let Ok(tphoto) = photos.find(id).first::<Photo>(c) {
|
||||||
if req.authorized_user().is_some() || tphoto.is_public() {
|
if context.is_authorized() || tphoto.is_public() {
|
||||||
return res.ok(|o| {
|
return Response::builder().html(|o| {
|
||||||
templates::details(
|
templates::details(
|
||||||
o,
|
o,
|
||||||
req,
|
&context,
|
||||||
&tphoto
|
&tphoto
|
||||||
.date
|
.date
|
||||||
.map(|d| {
|
.map(|d| {
|
||||||
@ -548,7 +659,7 @@ fn photo_details<'mw>(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
res.not_found("Photo not found")
|
not_found(&context)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Link = Html<String>;
|
pub type Link = Html<String>;
|
||||||
@ -595,38 +706,33 @@ impl Link {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn auto_complete_tag<'mw>(
|
fn auto_complete_tag(context: Context, query: AcQ) -> impl Reply {
|
||||||
req: &mut Request,
|
|
||||||
res: Response<'mw>,
|
|
||||||
) -> MiddlewareResult<'mw> {
|
|
||||||
if let Some(q) = req.query().get("q").map(String::from) {
|
|
||||||
use crate::schema::tags::dsl::{tag_name, tags};
|
use crate::schema::tags::dsl::{tag_name, tags};
|
||||||
let c: &PgConnection = &req.db_conn();
|
|
||||||
let q = tags
|
let q = tags
|
||||||
.select(tag_name)
|
.select(tag_name)
|
||||||
.filter(tag_name.ilike(q + "%"))
|
.filter(tag_name.ilike(query.q + "%"))
|
||||||
.order(tag_name)
|
.order(tag_name)
|
||||||
.limit(10);
|
.limit(10);
|
||||||
res.send(serde_json::to_string(&q.load::<String>(c).unwrap()).unwrap())
|
reply::json(&q.load::<String>(context.db()).unwrap())
|
||||||
} else {
|
|
||||||
res.error(StatusCode::BadRequest, "Missing 'q' parameter")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn auto_complete_person<'mw>(
|
fn auto_complete_person(context: Context, query: AcQ) -> impl Reply {
|
||||||
req: &mut Request,
|
|
||||||
res: Response<'mw>,
|
|
||||||
) -> MiddlewareResult<'mw> {
|
|
||||||
if let Some(q) = req.query().get("q").map(String::from) {
|
|
||||||
use crate::schema::people::dsl::{people, person_name};
|
use crate::schema::people::dsl::{people, person_name};
|
||||||
let c: &PgConnection = &req.db_conn();
|
|
||||||
let q = people
|
let q = people
|
||||||
.select(person_name)
|
.select(person_name)
|
||||||
.filter(person_name.ilike(q + "%"))
|
.filter(person_name.ilike(query.q + "%"))
|
||||||
.order(person_name)
|
.order(person_name)
|
||||||
.limit(10);
|
.limit(10);
|
||||||
res.send(serde_json::to_string(&q.load::<String>(c).unwrap()).unwrap())
|
reply::json(&q.load::<String>(context.db()).unwrap())
|
||||||
} else {
|
}
|
||||||
res.error(StatusCode::BadRequest, "Missing 'q' parameter")
|
|
||||||
}
|
#[derive(Deserialize)]
|
||||||
|
struct AcQ {
|
||||||
|
q: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Deserialize)]
|
||||||
|
pub struct ImgRange {
|
||||||
|
pub from: Option<i32>,
|
||||||
|
pub to: Option<i32>,
|
||||||
}
|
}
|
||||||
|
@ -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::views_by_date::date_of_img;
|
||||||
use super::PhotoLink;
|
use super::{Context, ImgRange, PhotoLink};
|
||||||
use crate::models::{Coord, Photo};
|
use crate::models::{Coord, Photo};
|
||||||
use crate::nickel_diesel::DieselRequestExtensions;
|
|
||||||
use crate::schema::photos;
|
use crate::schema::photos;
|
||||||
use diesel::pg::{Pg, PgConnection};
|
use diesel::pg::{Pg, PgConnection};
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use log::{debug, info, warn};
|
use log::{debug, info, warn};
|
||||||
use nickel::Request;
|
|
||||||
|
|
||||||
pub fn links_by_time<'a>(
|
pub fn links_by_time<'a>(
|
||||||
req: &mut Request,
|
context: &Context,
|
||||||
photos: photos::BoxedQuery<'a, Pg>,
|
photos: photos::BoxedQuery<'a, Pg>,
|
||||||
|
range: ImgRange,
|
||||||
) -> (Vec<PhotoLink>, Vec<(Coord, i32)>) {
|
) -> (Vec<PhotoLink>, Vec<(Coord, i32)>) {
|
||||||
let c: &PgConnection = &req.db_conn();
|
let c = context.db();
|
||||||
use crate::schema::photos::dsl::{date, id};
|
use crate::schema::photos::dsl::{date, id};
|
||||||
let photos = if let Some((_, from_date)) = query_date(req, "from") {
|
let photos = if let Some(from_date) = range.from.map(|i| date_of_img(c, i))
|
||||||
|
{
|
||||||
photos.filter(date.ge(from_date))
|
photos.filter(date.ge(from_date))
|
||||||
} else {
|
} else {
|
||||||
photos
|
photos
|
||||||
};
|
};
|
||||||
let photos = if let Some((_, to_date)) = query_date(req, "to") {
|
let photos = if let Some(to_date) = range.to.map(|i| date_of_img(c, i)) {
|
||||||
photos.filter(date.le(to_date))
|
photos.filter(date.le(to_date))
|
||||||
} else {
|
} else {
|
||||||
photos
|
photos
|
||||||
@ -30,7 +30,7 @@ pub fn links_by_time<'a>(
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
(
|
(
|
||||||
if let Some(groups) = split_to_groups(&photos) {
|
if let Some(groups) = split_to_groups(&photos) {
|
||||||
let path = req.path_without_query().unwrap_or("/");
|
let path = context.path_without_query();
|
||||||
groups
|
groups
|
||||||
.iter()
|
.iter()
|
||||||
.map(|g| PhotoLink::for_group(g, path))
|
.map(|g| PhotoLink::for_group(g, path))
|
||||||
|
@ -1,39 +1,35 @@
|
|||||||
use super::nickelext::MyResponse;
|
use super::render_ructe::RenderRucte;
|
||||||
use super::splitlist::links_by_time;
|
use super::splitlist::links_by_time;
|
||||||
use super::{Link, PhotoLink, SizeTag};
|
use super::{
|
||||||
|
not_found, redirect_to_img, Context, ImgRange, Link, PhotoLink, SizeTag,
|
||||||
|
};
|
||||||
use crate::models::Photo;
|
use crate::models::Photo;
|
||||||
use crate::nickel_diesel::DieselRequestExtensions;
|
|
||||||
use crate::templates;
|
use crate::templates;
|
||||||
use chrono::naive::{NaiveDate, NaiveDateTime};
|
use chrono::naive::{NaiveDate, NaiveDateTime};
|
||||||
use chrono::Duration as ChDuration;
|
use chrono::Duration as ChDuration;
|
||||||
use diesel::dsl::sql;
|
use diesel::dsl::sql;
|
||||||
use diesel::pg::PgConnection;
|
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use diesel::sql_types::{BigInt, Integer, Nullable};
|
use diesel::sql_types::{BigInt, Integer, Nullable};
|
||||||
use log::warn;
|
use log::warn;
|
||||||
use nickel::extensions::response::Redirect;
|
use serde::Deserialize;
|
||||||
use nickel::{MiddlewareResult, QueryString, Request, Response};
|
|
||||||
use nickel_jwt_session::SessionRequestExtensions;
|
|
||||||
use time;
|
use time;
|
||||||
|
use warp::http::Response;
|
||||||
|
use warp::Reply;
|
||||||
|
|
||||||
pub fn all_years<'mw>(
|
pub fn all_years(context: Context) -> impl Reply {
|
||||||
req: &mut Request,
|
|
||||||
res: Response<'mw>,
|
|
||||||
) -> MiddlewareResult<'mw> {
|
|
||||||
use crate::schema::photos::dsl::{date, grade};
|
use crate::schema::photos::dsl::{date, grade};
|
||||||
let c: &PgConnection = &req.db_conn();
|
|
||||||
|
|
||||||
let groups = Photo::query(req.authorized_user().is_some())
|
let groups = Photo::query(context.is_authorized())
|
||||||
.select(sql::<(Nullable<Integer>, BigInt)>(
|
.select(sql::<(Nullable<Integer>, BigInt)>(
|
||||||
"cast(extract(year from date) as int) y, count(*)",
|
"cast(extract(year from date) as int) y, count(*)",
|
||||||
))
|
))
|
||||||
.group_by(sql::<Nullable<Integer>>("y"))
|
.group_by(sql::<Nullable<Integer>>("y"))
|
||||||
.order(sql::<Nullable<Integer>>("y").desc().nulls_last())
|
.order(sql::<Nullable<Integer>>("y").desc().nulls_last())
|
||||||
.load::<(Option<i32>, i64)>(c)
|
.load::<(Option<i32>, i64)>(context.db())
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|&(year, count)| {
|
.map(|&(year, count)| {
|
||||||
let q = Photo::query(req.authorized_user().is_some())
|
let q = Photo::query(context.is_authorized())
|
||||||
.order((grade.desc().nulls_last(), date.asc()))
|
.order((grade.desc().nulls_last(), date.asc()))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
let photo = if let Some(year) = year {
|
let photo = if let Some(year) = year {
|
||||||
@ -42,7 +38,7 @@ pub fn all_years<'mw>(
|
|||||||
} else {
|
} else {
|
||||||
q.filter(date.is_null())
|
q.filter(date.is_null())
|
||||||
};
|
};
|
||||||
let photo = photo.first::<Photo>(c).unwrap();
|
let photo = photo.first::<Photo>(context.db()).unwrap();
|
||||||
PhotoLink {
|
PhotoLink {
|
||||||
title: Some(
|
title: Some(
|
||||||
year.map(|y| format!("{}", y))
|
year.map(|y| format!("{}", y))
|
||||||
@ -56,23 +52,20 @@ pub fn all_years<'mw>(
|
|||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
res.ok(|o| templates::index(o, req, "All photos", &[], &groups, &[]))
|
Response::builder().html(|o| {
|
||||||
|
templates::index(o, &context, "All photos", &[], &groups, &[])
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start_of_year(year: i32) -> NaiveDateTime {
|
fn start_of_year(year: i32) -> NaiveDateTime {
|
||||||
NaiveDate::from_ymd(year, 1, 1).and_hms(0, 0, 0)
|
NaiveDate::from_ymd(year, 1, 1).and_hms(0, 0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn months_in_year<'mw>(
|
pub fn months_in_year(year: i32, context: Context) -> Response<Vec<u8>> {
|
||||||
req: &mut Request,
|
|
||||||
res: Response<'mw>,
|
|
||||||
year: i32,
|
|
||||||
) -> MiddlewareResult<'mw> {
|
|
||||||
use crate::schema::photos::dsl::{date, grade};
|
use crate::schema::photos::dsl::{date, grade};
|
||||||
let c: &PgConnection = &req.db_conn();
|
|
||||||
|
|
||||||
let title: String = format!("Photos from {}", year);
|
let title: String = format!("Photos from {}", year);
|
||||||
let groups = Photo::query(req.authorized_user().is_some())
|
let groups = Photo::query(context.is_authorized())
|
||||||
.filter(date.ge(start_of_year(year)))
|
.filter(date.ge(start_of_year(year)))
|
||||||
.filter(date.lt(start_of_year(year + 1)))
|
.filter(date.lt(start_of_year(year + 1)))
|
||||||
.select(sql::<(Integer, BigInt)>(
|
.select(sql::<(Integer, BigInt)>(
|
||||||
@ -80,17 +73,17 @@ pub fn months_in_year<'mw>(
|
|||||||
))
|
))
|
||||||
.group_by(sql::<Integer>("m"))
|
.group_by(sql::<Integer>("m"))
|
||||||
.order(sql::<Integer>("m").desc().nulls_last())
|
.order(sql::<Integer>("m").desc().nulls_last())
|
||||||
.load::<(i32, i64)>(c)
|
.load::<(i32, i64)>(context.db())
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|&(month, count)| {
|
.map(|&(month, count)| {
|
||||||
let month = month as u32;
|
let month = month as u32;
|
||||||
let photo = Photo::query(req.authorized_user().is_some())
|
let photo = Photo::query(context.is_authorized())
|
||||||
.filter(date.ge(start_of_month(year, month)))
|
.filter(date.ge(start_of_month(year, month)))
|
||||||
.filter(date.lt(start_of_month(year, month + 1)))
|
.filter(date.lt(start_of_month(year, month + 1)))
|
||||||
.order((grade.desc().nulls_last(), date.asc()))
|
.order((grade.desc().nulls_last(), date.asc()))
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.first::<Photo>(c)
|
.first::<Photo>(context.db())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
PhotoLink {
|
PhotoLink {
|
||||||
@ -104,17 +97,17 @@ pub fn months_in_year<'mw>(
|
|||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
if groups.is_empty() {
|
if groups.is_empty() {
|
||||||
res.not_found("No such image")
|
not_found(&context)
|
||||||
} else {
|
} else {
|
||||||
use crate::schema::positions::dsl::{
|
use crate::schema::positions::dsl::{
|
||||||
latitude, longitude, photo_id, positions,
|
latitude, longitude, photo_id, positions,
|
||||||
};
|
};
|
||||||
let pos = Photo::query(req.authorized_user().is_some())
|
let pos = Photo::query(context.is_authorized())
|
||||||
.inner_join(positions)
|
.inner_join(positions)
|
||||||
.filter(date.ge(start_of_year(year)))
|
.filter(date.ge(start_of_year(year)))
|
||||||
.filter(date.lt(start_of_year(year + 1)))
|
.filter(date.lt(start_of_year(year + 1)))
|
||||||
.select((photo_id, latitude, longitude))
|
.select((photo_id, latitude, longitude))
|
||||||
.load(c)
|
.load(context.db())
|
||||||
.map_err(|e| warn!("Failed to load positions: {}", e))
|
.map_err(|e| warn!("Failed to load positions: {}", e))
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@ -122,7 +115,9 @@ pub fn months_in_year<'mw>(
|
|||||||
((lat, long).into(), p_id)
|
((lat, long).into(), p_id)
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
res.ok(|o| templates::index(o, req, &title, &[], &groups, &pos))
|
Response::builder().html(|o| {
|
||||||
|
templates::index(o, &context, &title, &[], &groups, &pos)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,18 +130,16 @@ fn start_of_month(year: i32, month: u32) -> NaiveDateTime {
|
|||||||
date.and_hms(0, 0, 0)
|
date.and_hms(0, 0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn days_in_month<'mw>(
|
pub fn days_in_month(
|
||||||
req: &mut Request,
|
|
||||||
res: Response<'mw>,
|
|
||||||
year: i32,
|
year: i32,
|
||||||
month: u32,
|
month: u32,
|
||||||
) -> MiddlewareResult<'mw> {
|
context: Context,
|
||||||
|
) -> Response<Vec<u8>> {
|
||||||
use crate::schema::photos::dsl::{date, grade};
|
use crate::schema::photos::dsl::{date, grade};
|
||||||
let c: &PgConnection = &req.db_conn();
|
|
||||||
|
|
||||||
let lpath: Vec<Link> = vec![Link::year(year)];
|
let lpath: Vec<Link> = vec![Link::year(year)];
|
||||||
let title: String = format!("Photos from {} {}", monthname(month), year);
|
let title: String = format!("Photos from {} {}", monthname(month), year);
|
||||||
let groups = Photo::query(req.authorized_user().is_some())
|
let groups = Photo::query(context.is_authorized())
|
||||||
.filter(date.ge(start_of_month(year, month)))
|
.filter(date.ge(start_of_month(year, month)))
|
||||||
.filter(date.lt(start_of_month(year, month + 1)))
|
.filter(date.lt(start_of_month(year, month + 1)))
|
||||||
.select(sql::<(Integer, BigInt)>(
|
.select(sql::<(Integer, BigInt)>(
|
||||||
@ -154,19 +147,19 @@ pub fn days_in_month<'mw>(
|
|||||||
))
|
))
|
||||||
.group_by(sql::<Integer>("d"))
|
.group_by(sql::<Integer>("d"))
|
||||||
.order(sql::<Integer>("d").desc().nulls_last())
|
.order(sql::<Integer>("d").desc().nulls_last())
|
||||||
.load::<(i32, i64)>(c)
|
.load::<(i32, i64)>(context.db())
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|&(day, count)| {
|
.map(|&(day, count)| {
|
||||||
let day = day as u32;
|
let day = day as u32;
|
||||||
let fromdate =
|
let fromdate =
|
||||||
NaiveDate::from_ymd(year, month, day).and_hms(0, 0, 0);
|
NaiveDate::from_ymd(year, month, day).and_hms(0, 0, 0);
|
||||||
let photo = Photo::query(req.authorized_user().is_some())
|
let photo = Photo::query(context.is_authorized())
|
||||||
.filter(date.ge(fromdate))
|
.filter(date.ge(fromdate))
|
||||||
.filter(date.lt(fromdate + ChDuration::days(1)))
|
.filter(date.lt(fromdate + ChDuration::days(1)))
|
||||||
.order((grade.desc().nulls_last(), date.asc()))
|
.order((grade.desc().nulls_last(), date.asc()))
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.first::<Photo>(c)
|
.first::<Photo>(context.db())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
PhotoLink {
|
PhotoLink {
|
||||||
@ -180,17 +173,17 @@ pub fn days_in_month<'mw>(
|
|||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
if groups.is_empty() {
|
if groups.is_empty() {
|
||||||
res.not_found("No such image")
|
not_found(&context)
|
||||||
} else {
|
} else {
|
||||||
use crate::schema::positions::dsl::{
|
use crate::schema::positions::dsl::{
|
||||||
latitude, longitude, photo_id, positions,
|
latitude, longitude, photo_id, positions,
|
||||||
};
|
};
|
||||||
let pos = Photo::query(req.authorized_user().is_some())
|
let pos = Photo::query(context.is_authorized())
|
||||||
.inner_join(positions)
|
.inner_join(positions)
|
||||||
.filter(date.ge(start_of_month(year, month)))
|
.filter(date.ge(start_of_month(year, month)))
|
||||||
.filter(date.lt(start_of_month(year, month + 1)))
|
.filter(date.lt(start_of_month(year, month + 1)))
|
||||||
.select((photo_id, latitude, longitude))
|
.select((photo_id, latitude, longitude))
|
||||||
.load(c)
|
.load(context.db())
|
||||||
.map_err(|e| warn!("Failed to load positions: {}", e))
|
.map_err(|e| warn!("Failed to load positions: {}", e))
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@ -198,28 +191,26 @@ pub fn days_in_month<'mw>(
|
|||||||
((lat, long).into(), p_id)
|
((lat, long).into(), p_id)
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
res.ok(|o| templates::index(o, req, &title, &lpath, &groups, &pos))
|
Response::builder().html(|o| {
|
||||||
|
templates::index(o, &context, &title, &lpath, &groups, &pos)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn all_null_date<'mw>(
|
pub fn all_null_date(context: Context) -> impl Reply {
|
||||||
req: &mut Request,
|
|
||||||
res: Response<'mw>,
|
|
||||||
) -> MiddlewareResult<'mw> {
|
|
||||||
use crate::schema::photos::dsl::{date, path};
|
use crate::schema::photos::dsl::{date, path};
|
||||||
|
|
||||||
let c: &PgConnection = &req.db_conn();
|
Response::builder().html(|o| {
|
||||||
res.ok(|o| {
|
|
||||||
templates::index(
|
templates::index(
|
||||||
o,
|
o,
|
||||||
req,
|
&context,
|
||||||
"Photos without a date",
|
"Photos without a date",
|
||||||
&[],
|
&[],
|
||||||
&Photo::query(req.authorized_user().is_some())
|
&Photo::query(context.is_authorized())
|
||||||
.filter(date.is_null())
|
.filter(date.is_null())
|
||||||
.order(path.asc())
|
.order(path.asc())
|
||||||
.limit(500)
|
.limit(500)
|
||||||
.load(c)
|
.load(context.db())
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.iter()
|
.iter()
|
||||||
.map(PhotoLink::from)
|
.map(PhotoLink::from)
|
||||||
@ -229,28 +220,28 @@ pub fn all_null_date<'mw>(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn all_for_day<'mw>(
|
pub fn all_for_day(
|
||||||
req: &mut Request,
|
|
||||||
res: Response<'mw>,
|
|
||||||
year: i32,
|
year: i32,
|
||||||
month: u32,
|
month: u32,
|
||||||
day: u32,
|
day: u32,
|
||||||
) -> MiddlewareResult<'mw> {
|
range: ImgRange,
|
||||||
|
context: Context,
|
||||||
|
) -> impl Reply {
|
||||||
let thedate = NaiveDate::from_ymd(year, month, day).and_hms(0, 0, 0);
|
let thedate = NaiveDate::from_ymd(year, month, day).and_hms(0, 0, 0);
|
||||||
use crate::schema::photos::dsl::date;
|
use crate::schema::photos::dsl::date;
|
||||||
|
|
||||||
let photos = Photo::query(req.authorized_user().is_some())
|
let photos = Photo::query(context.is_authorized())
|
||||||
.filter(date.ge(thedate))
|
.filter(date.ge(thedate))
|
||||||
.filter(date.lt(thedate + ChDuration::days(1)));
|
.filter(date.lt(thedate + ChDuration::days(1)));
|
||||||
let (links, coords) = links_by_time(req, photos);
|
let (links, coords) = links_by_time(&context, photos, range);
|
||||||
|
|
||||||
if links.is_empty() {
|
if links.is_empty() {
|
||||||
res.not_found("No such image")
|
not_found(&context)
|
||||||
} else {
|
} else {
|
||||||
res.ok(|o| {
|
Response::builder().html(|o| {
|
||||||
templates::index(
|
templates::index(
|
||||||
o,
|
o,
|
||||||
req,
|
&context,
|
||||||
&format!("Photos from {} {} {}", day, monthname(month), year),
|
&format!("Photos from {} {} {}", day, monthname(month), year),
|
||||||
&[Link::year(year), Link::month(year, month)],
|
&[Link::year(year), Link::month(year, month)],
|
||||||
&links,
|
&links,
|
||||||
@ -260,41 +251,37 @@ pub fn all_for_day<'mw>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_this_day<'mw>(
|
pub fn on_this_day(context: Context) -> impl Reply {
|
||||||
req: &mut Request,
|
|
||||||
res: Response<'mw>,
|
|
||||||
) -> MiddlewareResult<'mw> {
|
|
||||||
use crate::schema::photos::dsl::{date, grade};
|
use crate::schema::photos::dsl::{date, grade};
|
||||||
use crate::schema::positions::dsl::{
|
use crate::schema::positions::dsl::{
|
||||||
latitude, longitude, photo_id, positions,
|
latitude, longitude, photo_id, positions,
|
||||||
};
|
};
|
||||||
let c: &PgConnection = &req.db_conn();
|
|
||||||
|
|
||||||
let (month, day) = {
|
let (month, day) = {
|
||||||
let now = time::now();
|
let now = time::now();
|
||||||
(now.tm_mon as u32 + 1, now.tm_mday as u32)
|
(now.tm_mon as u32 + 1, now.tm_mday as u32)
|
||||||
};
|
};
|
||||||
let pos = Photo::query(req.authorized_user().is_some())
|
let pos = Photo::query(context.is_authorized())
|
||||||
.inner_join(positions)
|
.inner_join(positions)
|
||||||
.filter(
|
.filter(
|
||||||
sql("extract(month from date)=").bind::<Integer, _>(month as i32),
|
sql("extract(month from date)=").bind::<Integer, _>(month as i32),
|
||||||
)
|
)
|
||||||
.filter(sql("extract(day from date)=").bind::<Integer, _>(day as i32))
|
.filter(sql("extract(day from date)=").bind::<Integer, _>(day as i32))
|
||||||
.select((photo_id, latitude, longitude))
|
.select((photo_id, latitude, longitude))
|
||||||
.load(c)
|
.load(context.db())
|
||||||
.map_err(|e| warn!("Failed to load positions: {}", e))
|
.map_err(|e| warn!("Failed to load positions: {}", e))
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(p_id, lat, long): (i32, i32, i32)| ((lat, long).into(), p_id))
|
.map(|(p_id, lat, long): (i32, i32, i32)| ((lat, long).into(), p_id))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
res.ok(|o| {
|
Response::builder().html(|o| {
|
||||||
templates::index(
|
templates::index(
|
||||||
o,
|
o,
|
||||||
req,
|
&context,
|
||||||
&format!("Photos from {} {}", day, monthname(month)),
|
&format!("Photos from {} {}", day, monthname(month)),
|
||||||
&[],
|
&[],
|
||||||
&Photo::query(req.authorized_user().is_some())
|
&Photo::query(context.is_authorized())
|
||||||
.select(sql::<(Integer, BigInt)>(
|
.select(sql::<(Integer, BigInt)>(
|
||||||
"cast(extract(year from date) as int) y, count(*)",
|
"cast(extract(year from date) as int) y, count(*)",
|
||||||
))
|
))
|
||||||
@ -308,19 +295,19 @@ pub fn on_this_day<'mw>(
|
|||||||
.bind::<Integer, _>(day as i32),
|
.bind::<Integer, _>(day as i32),
|
||||||
)
|
)
|
||||||
.order(sql::<Integer>("y").desc())
|
.order(sql::<Integer>("y").desc())
|
||||||
.load::<(i32, i64)>(c)
|
.load::<(i32, i64)>(context.db())
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|&(year, count)| {
|
.map(|&(year, count)| {
|
||||||
let fromdate =
|
let fromdate =
|
||||||
NaiveDate::from_ymd(year, month as u32, day)
|
NaiveDate::from_ymd(year, month as u32, day)
|
||||||
.and_hms(0, 0, 0);
|
.and_hms(0, 0, 0);
|
||||||
let photo = Photo::query(req.authorized_user().is_some())
|
let photo = Photo::query(context.is_authorized())
|
||||||
.filter(date.ge(fromdate))
|
.filter(date.ge(fromdate))
|
||||||
.filter(date.lt(fromdate + ChDuration::days(1)))
|
.filter(date.lt(fromdate + ChDuration::days(1)))
|
||||||
.order((grade.desc().nulls_last(), date.asc()))
|
.order((grade.desc().nulls_last(), date.asc()))
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.first::<Photo>(c)
|
.first::<Photo>(context.db())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
PhotoLink {
|
PhotoLink {
|
||||||
@ -337,65 +324,48 @@ pub fn on_this_day<'mw>(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn next_image<'mw>(
|
pub fn next_image(context: Context, param: FromParam) -> impl Reply {
|
||||||
req: &mut Request,
|
|
||||||
res: Response<'mw>,
|
|
||||||
) -> MiddlewareResult<'mw> {
|
|
||||||
use crate::schema::photos::dsl::{date, id};
|
use crate::schema::photos::dsl::{date, id};
|
||||||
if let Some((from_id, from_date)) = query_date(req, "from") {
|
if let Some(from_date) = date_of_img(context.db(), param.from) {
|
||||||
let q = Photo::query(req.authorized_user().is_some())
|
let q = Photo::query(context.is_authorized())
|
||||||
.select(id)
|
.select(id)
|
||||||
.filter(
|
.filter(
|
||||||
date.gt(from_date)
|
date.gt(from_date)
|
||||||
.or(date.eq(from_date).and(id.gt(from_id))),
|
.or(date.eq(from_date).and(id.gt(param.from))),
|
||||||
)
|
)
|
||||||
.order((date, id));
|
.order((date, id));
|
||||||
let c: &PgConnection = &req.db_conn();
|
if let Ok(photo) = q.first::<i32>(context.db()) {
|
||||||
if let Ok(photo) = q.first::<i32>(c) {
|
return redirect_to_img(photo);
|
||||||
return res.redirect(format!("/img/{}", photo)); // to photo_details
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
res.not_found("No such image")
|
not_found(&context)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn prev_image<'mw>(
|
pub fn prev_image(context: Context, param: FromParam) -> impl Reply {
|
||||||
req: &mut Request,
|
|
||||||
res: Response<'mw>,
|
|
||||||
) -> MiddlewareResult<'mw> {
|
|
||||||
use crate::schema::photos::dsl::{date, id};
|
use crate::schema::photos::dsl::{date, id};
|
||||||
if let Some((from_id, from_date)) = query_date(req, "from") {
|
if let Some(from_date) = date_of_img(context.db(), param.from) {
|
||||||
let q = Photo::query(req.authorized_user().is_some())
|
let q = Photo::query(context.is_authorized())
|
||||||
.select(id)
|
.select(id)
|
||||||
.filter(
|
.filter(
|
||||||
date.lt(from_date)
|
date.lt(from_date)
|
||||||
.or(date.eq(from_date).and(id.lt(from_id))),
|
.or(date.eq(from_date).and(id.lt(param.from))),
|
||||||
)
|
)
|
||||||
.order((date.desc().nulls_last(), id.desc()));
|
.order((date.desc().nulls_last(), id.desc()));
|
||||||
let c: &PgConnection = &req.db_conn();
|
if let Ok(photo) = q.first::<i32>(context.db()) {
|
||||||
if let Ok(photo) = q.first::<i32>(c) {
|
return redirect_to_img(photo);
|
||||||
return res.redirect(format!("/img/{}", photo)); // to photo_details
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
res.not_found("No such image")
|
not_found(&context)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn query_date(
|
#[derive(Deserialize)]
|
||||||
req: &mut Request,
|
pub struct FromParam {
|
||||||
name: &str,
|
from: i32,
|
||||||
) -> Option<(i32, NaiveDateTime)> {
|
}
|
||||||
req.query()
|
|
||||||
.get(name)
|
pub fn date_of_img(db: &PgConnection, photo_id: i32) -> Option<NaiveDateTime> {
|
||||||
.and_then(|s| s.parse().ok())
|
|
||||||
.and_then(|i: i32| {
|
|
||||||
use crate::schema::photos::dsl::{date, photos};
|
use crate::schema::photos::dsl::{date, photos};
|
||||||
let c: &PgConnection = &req.db_conn();
|
photos.find(photo_id).select(date).first(db).unwrap_or(None)
|
||||||
photos
|
|
||||||
.find(i)
|
|
||||||
.select(date)
|
|
||||||
.first(c)
|
|
||||||
.unwrap_or(None)
|
|
||||||
.map(|d| (i, d))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn monthname(n: u32) -> &'static str {
|
pub fn monthname(n: u32) -> &'static str {
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
@use nickel::Request;
|
|
||||||
@use nickel_jwt_session::SessionRequestExtensions;
|
|
||||||
@use crate::models::{Photo, Person, Place, Tag, Camera, Coord};
|
|
||||||
@use crate::server::{Link, SizeTag};
|
|
||||||
@use super::page_base;
|
@use super::page_base;
|
||||||
|
@use crate::models::{Photo, Person, Place, Tag, Camera, Coord};
|
||||||
|
@use crate::server::{Context, Link, SizeTag};
|
||||||
|
|
||||||
@(req: &Request, lpath: &[Link], people: &[Person], places: &[Place], tags: &[Tag], position: &Option<Coord>, attribution: &Option<String>, camera: &Option<Camera>, photo: &Photo)
|
@(context: &Context, lpath: &[Link], people: &[Person], places: &[Place], tags: &[Tag], position: &Option<Coord>, attribution: &Option<String>, camera: &Option<Camera>, photo: &Photo)
|
||||||
@:page_base(req, "Photo details", lpath, {
|
@:page_base(context, "Photo details", lpath, {
|
||||||
<meta property='og:title' content='Photo @if let Some(d) = photo.date {(@d.format("%F"))}'>
|
<meta property='og:title' content='Photo @if let Some(d) = photo.date {(@d.format("%F"))}'>
|
||||||
<meta property='og:type' content='image' />
|
<meta property='og:type' content='image' />
|
||||||
<meta property='og:image' content='/img/@photo.id-m.jpg' />
|
<meta property='og:image' content='/img/@photo.id-m.jpg' />
|
||||||
@ -14,7 +12,7 @@
|
|||||||
<div class="details" data-imgid="@photo.id"@if let Some(g) = photo.grade { data-grade="@g"}@if let Some(ref p) = *position { data-position="[@p.x, @p.y]"}>
|
<div class="details" data-imgid="@photo.id"@if let Some(g) = photo.grade { data-grade="@g"}@if let Some(ref p) = *position { data-position="[@p.x, @p.y]"}>
|
||||||
<div class="item"><img src="/img/@photo.id-m.jpg"@if let Some((w,h)) = photo.get_size(SizeTag::Medium.px()) { width="@w" height="@h"}></div>
|
<div class="item"><img src="/img/@photo.id-m.jpg"@if let Some((w,h)) = photo.get_size(SizeTag::Medium.px()) { width="@w" height="@h"}></div>
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
@if req.authorized_user().is_some() {
|
@if context.is_authorized() {
|
||||||
<p><a href="/img/@photo.id-l.jpg">@photo.path</a></p>
|
<p><a href="/img/@photo.id-l.jpg">@photo.path</a></p>
|
||||||
@if photo.is_public() {<p>This photo is public.</p>}
|
@if photo.is_public() {<p>This photo is public.</p>}
|
||||||
else {<p>This photo is not public.</p>}
|
else {<p>This photo is not public.</p>}
|
||||||
|
37
templates/error.rs.html
Normal file
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 crate::server::{Context, Link};
|
||||||
@use nickel_jwt_session::SessionRequestExtensions;
|
|
||||||
@use crate::server::Link;
|
|
||||||
|
|
||||||
@(req: &Request, lpath: &[Link])
|
@(context: &Context, lpath: &[Link])
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
<span><a href="/" accesskey="h" title="Images from all years">Images</a>
|
<span><a href="/" accesskey="h" title="Images from all years">Images</a>
|
||||||
@ -13,6 +11,6 @@
|
|||||||
<span>· <a href="/place/">Places</a></span>
|
<span>· <a href="/place/">Places</a></span>
|
||||||
<span>· <a href="/thisday">On this day</a></span>
|
<span>· <a href="/thisday">On this day</a></span>
|
||||||
<span>· <a href="/random" accesskey="r">Random pic</a></span>
|
<span>· <a href="/random" accesskey="r">Random pic</a></span>
|
||||||
@if let Some(ref u) = req.authorized_user() {<span class="user">@u (<a href="/logout">log out</a>)</span>}
|
@if let Some(ref u) = context.authorized_user() {<span class="user">@u (<a href="/logout">log out</a>)</span>}
|
||||||
else {<span class="user">(<a href="/login@if let Some(p) = req.path_without_query() {?next=@p}">log in</a>)</span>}
|
else {<span class="user">(<a href="/login?next=@context.path_without_query()">log in</a>)</span>}
|
||||||
</header>
|
</header>
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
@use crate::models::Coord;
|
|
||||||
@use nickel::Request;
|
|
||||||
@use crate::server::{Link, PhotoLink};
|
|
||||||
@use super::{data_positions, page_base, photo_link};
|
@use super::{data_positions, page_base, photo_link};
|
||||||
|
@use crate::models::Coord;
|
||||||
|
@use crate::server::{Context, Link, PhotoLink};
|
||||||
|
|
||||||
@(req: &Request, title: &str, lpath: &[Link], photos: &[PhotoLink], coords: &[(Coord, i32)])
|
@(context: &Context, title: &str, lpath: &[Link], photos: &[PhotoLink], coords: &[(Coord, i32)])
|
||||||
|
|
||||||
@:page_base(req, title, lpath, {}, {
|
@:page_base(context, title, lpath, {}, {
|
||||||
<div class="group"@:data_positions(coords)>
|
<div class="group"@:data_positions(coords)>
|
||||||
@for p in photos {@:photo_link(p)}
|
@for p in photos {@:photo_link(p)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
@use nickel::Request;
|
|
||||||
@use super::page_base;
|
@use super::page_base;
|
||||||
|
@use crate::server::Context;
|
||||||
|
|
||||||
@(req: &Request, next: Option<String>, message: Option<&str>)
|
@(context: &Context, next: Option<String>, message: Option<&str>)
|
||||||
|
|
||||||
@:page_base(req, "login", &[], {}, {
|
@:page_base(context, "login", &[], {}, {
|
||||||
<form action="/login" method="post">
|
<form action="/login" method="post">
|
||||||
@if let Some(message) = message {<p>@message</p>}
|
@if let Some(message) = message {<p>@message</p>}
|
||||||
<p><label for="user">User:</label>
|
<p><label for="user">User:</label>
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
@use nickel::Request;
|
|
||||||
@use super::page_base;
|
@use super::page_base;
|
||||||
@use nickel_jwt_session::SessionRequestExtensions;
|
@use crate::server::Context;
|
||||||
|
@use warp::http::StatusCode;
|
||||||
|
|
||||||
@(req: &Request)
|
@(context: &Context, code: StatusCode, message: &str)
|
||||||
|
|
||||||
@:page_base(req, "Not found", &[], {}, {
|
@:page_base(context, code.canonical_reason().unwrap_or("error"), &[], {}, {
|
||||||
<p>No page or photo match that url.</p>
|
<p>@message (@code.as_u16())</p>
|
||||||
@if req.authorized_user().is_none() {
|
@if !context.is_authorized() {
|
||||||
<p>At least nothing publicly visible, you might try
|
<p>At least nothing publicly visible, you might try
|
||||||
<a href="/login@if let Some(p) = req.path_without_query() {?next=@p}">logging in</a>.</p>
|
<a href="/login?next=@context.path_without_query()">logging in</a>.</p>
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
@use nickel::Request;
|
|
||||||
@use nickel_jwt_session::SessionRequestExtensions;
|
|
||||||
@use crate::server::Link;
|
|
||||||
@use super::head;
|
|
||||||
@use super::statics::{photos_css, admin_js, ux_js};
|
@use super::statics::{photos_css, admin_js, ux_js};
|
||||||
|
@use crate::server::{Context, Link};
|
||||||
|
@use crate::templates::head;
|
||||||
|
|
||||||
@(req: &Request, title: &str, lpath: &[Link], meta: Content, content: Content)
|
@(context: &Context, title: &str, lpath: &[Link], meta: Content, content: Content)
|
||||||
|
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
@ -13,7 +11,7 @@
|
|||||||
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
|
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||||
<link rel="stylesheet" href="/static/@photos_css.name" type="text/css"/>
|
<link rel="stylesheet" href="/static/@photos_css.name" type="text/css"/>
|
||||||
@if req.authorized_user().is_some() {
|
@if context.is_authorized() {
|
||||||
<script src="/static/@admin_js.name" type="text/javascript" defer>
|
<script src="/static/@admin_js.name" type="text/javascript" defer>
|
||||||
</script>
|
</script>
|
||||||
}
|
}
|
||||||
@ -22,7 +20,7 @@
|
|||||||
@:meta()
|
@:meta()
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@:head(req, lpath)
|
@:head(context, lpath)
|
||||||
<main>
|
<main>
|
||||||
<h1>@title</h1>
|
<h1>@title</h1>
|
||||||
@:content()
|
@:content()
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
@use nickel::Request;
|
|
||||||
@use crate::models::Person;
|
|
||||||
@use super::page_base;
|
@use super::page_base;
|
||||||
|
@use crate::models::Person;
|
||||||
|
@use crate::server::Context;
|
||||||
|
|
||||||
@(req: &Request, people: &[Person])
|
@(context: &Context, people: &[Person])
|
||||||
@:page_base(req, "Photo people", &[], {}, {
|
@:page_base(context, "Photo people", &[], {}, {
|
||||||
<ul class="allpeople">
|
<ul class="allpeople">
|
||||||
@for p in people {
|
@for p in people {
|
||||||
<li><a href="/person/@p.slug">@p.person_name</a>
|
<li><a href="/person/@p.slug">@p.person_name</a>
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
@use nickel::Request;
|
|
||||||
@use crate::models::{Coord, Person};
|
|
||||||
@use crate::server::PhotoLink;
|
|
||||||
@use super::{data_positions, page_base, photo_link};
|
@use super::{data_positions, page_base, photo_link};
|
||||||
|
@use crate::models::{Coord, Person};
|
||||||
|
@use crate::server::{Context, PhotoLink};
|
||||||
|
|
||||||
@(req: &Request, photos: &[PhotoLink], coords: &[(Coord, i32)], person: &Person)
|
@(context: &Context, photos: &[PhotoLink], coords: &[(Coord, i32)], person: &Person)
|
||||||
@:page_base(req, &format!("Photos with {}", person.person_name), &[], {}, {
|
@:page_base(context, &format!("Photos with {}", person.person_name), &[], {}, {
|
||||||
<div class="group"@:data_positions(coords)>
|
<div class="group"@:data_positions(coords)>
|
||||||
@for p in photos {@:photo_link(p)}
|
@for p in photos {@:photo_link(p)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
@use nickel::Request;
|
|
||||||
@use crate::models::{Coord, Place};
|
@use crate::models::{Coord, Place};
|
||||||
@use crate::server::PhotoLink;
|
@use crate::server::{Context, PhotoLink};
|
||||||
@use super::{data_positions, page_base, photo_link};
|
@use super::{data_positions, page_base, photo_link};
|
||||||
|
|
||||||
@(req: &Request, photos: &[PhotoLink], coords: &[(Coord, i32)], place: &Place)
|
@(context: &Context, photos: &[PhotoLink], coords: &[(Coord, i32)], place: &Place)
|
||||||
@:page_base(req, &format!("Photos from {}", place.place_name), &[], {}, {
|
@:page_base(context, &format!("Photos from {}", place.place_name), &[], {}, {
|
||||||
<div class="group"@:data_positions(coords)>
|
<div class="group"@:data_positions(coords)>
|
||||||
@for p in photos {@:photo_link(p)}
|
@for p in photos {@:photo_link(p)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
@use nickel::Request;
|
|
||||||
@use crate::models::Place;
|
|
||||||
@use super::page_base;
|
@use super::page_base;
|
||||||
|
@use crate::models::Place;
|
||||||
|
@use crate::server::Context;
|
||||||
|
|
||||||
@(req: &Request, places: &[Place])
|
@(context: &Context, places: &[Place])
|
||||||
|
|
||||||
@:page_base(req, "Photo places", &[], {}, {
|
@:page_base(context, "Photo places", &[], {}, {
|
||||||
<ul class="allplaces">
|
<ul class="allplaces">
|
||||||
@for p in places {
|
@for p in places {
|
||||||
<li><a href="/place/@p.slug">@p.place_name</a>
|
<li><a href="/place/@p.slug">@p.place_name</a>
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
@use nickel::Request;
|
|
||||||
@use crate::models::{Coord, Tag};
|
@use crate::models::{Coord, Tag};
|
||||||
@use crate::server::PhotoLink;
|
@use crate::server::{Context, PhotoLink};
|
||||||
@use super::{data_positions, page_base, photo_link};
|
@use super::{data_positions, page_base, photo_link};
|
||||||
|
|
||||||
@(req: &Request, photos: &[PhotoLink], coords: &[(Coord, i32)], tag: &Tag)
|
@(context: &Context, photos: &[PhotoLink], coords: &[(Coord, i32)], tag: &Tag)
|
||||||
|
|
||||||
@:page_base(req, &format!("Photos tagged {}", tag.tag_name), &[], {}, {
|
@:page_base(context, &format!("Photos tagged {}", tag.tag_name), &[], {}, {
|
||||||
<div class="group"@:data_positions(coords)>
|
<div class="group"@:data_positions(coords)>
|
||||||
@for p in photos {@:photo_link(p)}
|
@for p in photos {@:photo_link(p)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
@use nickel::Request;
|
|
||||||
@use crate::models::Tag;
|
|
||||||
@use super::page_base;
|
@use super::page_base;
|
||||||
|
@use crate::models::Tag;
|
||||||
|
@use crate::server::Context;
|
||||||
|
|
||||||
@(req: &Request, tags: &[Tag])
|
@(context: &Context, tags: &[Tag])
|
||||||
@:page_base(req, "Photo tags", &[], {}, {
|
@:page_base(context, "Photo tags", &[], {}, {
|
||||||
<ul class="alltags">
|
<ul class="alltags">
|
||||||
@for tag in tags {
|
@for tag in tags {
|
||||||
<li><a href="/tag/@tag.slug">@tag.tag_name</a>
|
<li><a href="/tag/@tag.slug">@tag.tag_name</a>
|
||||||
|
Loading…
Reference in New Issue
Block a user