Merge pull request #2055 from ethcore/dapp-norefresh

Get rid of 'Dapp is being downloaded' page
This commit is contained in:
Robert Habermeier 2016-09-12 11:36:37 +02:00 committed by GitHub
commit bc9b7cbcc1
9 changed files with 183 additions and 118 deletions

View File

@ -18,13 +18,13 @@
use std::fs; use std::fs;
use std::sync::{Arc}; use std::sync::{Arc};
use std::sync::atomic::{AtomicBool, Ordering};
use linked_hash_map::LinkedHashMap; use linked_hash_map::LinkedHashMap;
use page::LocalPageEndpoint; use page::LocalPageEndpoint;
use handlers::FetchControl;
pub enum ContentStatus { pub enum ContentStatus {
Fetching(Arc<AtomicBool>), Fetching(Arc<FetchControl>),
Ready(LocalPageEndpoint), Ready(LocalPageEndpoint),
} }
@ -57,10 +57,10 @@ impl ContentCache {
while len > expected_size { while len > expected_size {
let entry = self.cache.pop_front().unwrap(); let entry = self.cache.pop_front().unwrap();
match entry.1 { match entry.1 {
ContentStatus::Fetching(ref abort) => { ContentStatus::Fetching(ref fetch) => {
trace!(target: "dapps", "Aborting {} because of limit.", entry.0); trace!(target: "dapps", "Aborting {} because of limit.", entry.0);
// Mark as aborted // Mark as aborted
abort.store(true, Ordering::SeqCst); fetch.abort()
}, },
ContentStatus::Ready(ref endpoint) => { ContentStatus::Ready(ref endpoint) => {
trace!(target: "dapps", "Removing {} because of limit.", entry.0); trace!(target: "dapps", "Removing {} because of limit.", entry.0);

View File

@ -23,7 +23,6 @@ use std::{fs, env, fmt};
use std::io::{self, Read, Write}; use std::io::{self, Read, Write};
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::{AtomicBool};
use rustc_serialize::hex::FromHex; use rustc_serialize::hex::FromHex;
use hyper; use hyper;
@ -76,10 +75,12 @@ impl<R: URLHint> ContentFetcher<R> {
} }
pub fn contains(&self, content_id: &str) -> bool { pub fn contains(&self, content_id: &str) -> bool {
let mut cache = self.cache.lock(); {
// Check if we already have the app let mut cache = self.cache.lock();
if cache.get(content_id).is_some() { // Check if we already have the app
return true; if cache.get(content_id).is_some() {
return true;
}
} }
// fallback to resolver // fallback to resolver
if let Ok(content_id) = content_id.from_hex() { if let Ok(content_id) = content_id.from_hex() {
@ -115,49 +116,60 @@ impl<R: URLHint> ContentFetcher<R> {
(None, endpoint.to_async_handler(path, control)) (None, endpoint.to_async_handler(path, control))
}, },
// App is already being fetched // App is already being fetched
Some(&mut ContentStatus::Fetching(_)) => { Some(&mut ContentStatus::Fetching(ref fetch_control)) => {
(None, Box::new(ContentHandler::error_with_refresh( trace!(target: "dapps", "Content fetching in progress. Waiting...");
StatusCode::ServiceUnavailable, (None, fetch_control.to_handler(control))
"Download In Progress",
"This dapp is already being downloaded. Please wait...",
None,
)) as Box<Handler>)
}, },
// We need to start fetching app // We need to start fetching app
None => { None => {
trace!(target: "dapps", "Content unavailable. Fetching...");
let content_hex = content_id.from_hex().expect("to_handler is called only when `contains` returns true."); let content_hex = content_id.from_hex().expect("to_handler is called only when `contains` returns true.");
let content = self.resolver.resolve(content_hex); let content = self.resolver.resolve(content_hex);
let abort = Arc::new(AtomicBool::new(false));
let cache = self.cache.clone();
let on_done = move |id: String, result: Option<LocalPageEndpoint>| {
let mut cache = cache.lock();
match result {
Some(endpoint) => {
cache.insert(id, ContentStatus::Ready(endpoint));
},
// In case of error
None => {
cache.remove(&id);
},
}
};
match content { match content {
Some(URLHintResult::Dapp(dapp)) => ( Some(URLHintResult::Dapp(dapp)) => {
Some(ContentStatus::Fetching(abort.clone())), let (handler, fetch_control) = ContentFetcherHandler::new(
Box::new(ContentFetcherHandler::new(
dapp.url(), dapp.url(),
abort,
control, control,
path.using_dapps_domains, path.using_dapps_domains,
DappInstaller { DappInstaller {
id: content_id.clone(), id: content_id.clone(),
dapps_path: self.dapps_path.clone(), dapps_path: self.dapps_path.clone(),
cache: self.cache.clone(), on_done: Box::new(on_done),
})) as Box<Handler> }
), );
Some(URLHintResult::Content(content)) => (
Some(ContentStatus::Fetching(abort.clone())), (Some(ContentStatus::Fetching(fetch_control)), Box::new(handler) as Box<Handler>)
Box::new(ContentFetcherHandler::new( },
Some(URLHintResult::Content(content)) => {
let (handler, fetch_control) = ContentFetcherHandler::new(
content.url, content.url,
abort,
control, control,
path.using_dapps_domains, path.using_dapps_domains,
ContentInstaller { ContentInstaller {
id: content_id.clone(), id: content_id.clone(),
mime: content.mime, mime: content.mime,
content_path: self.dapps_path.clone(), content_path: self.dapps_path.clone(),
cache: self.cache.clone(), on_done: Box::new(on_done),
} }
)) as Box<Handler>, );
),
(Some(ContentStatus::Fetching(fetch_control)), Box::new(handler) as Box<Handler>)
},
None => { None => {
// This may happen when sync status changes in between // This may happen when sync status changes in between
// `contains` and `to_handler` // `contains` and `to_handler`
@ -225,14 +237,13 @@ struct ContentInstaller {
id: String, id: String,
mime: String, mime: String,
content_path: PathBuf, content_path: PathBuf,
cache: Arc<Mutex<ContentCache>>, on_done: Box<Fn(String, Option<LocalPageEndpoint>) + Send>,
} }
impl ContentValidator for ContentInstaller { impl ContentValidator for ContentInstaller {
type Error = ValidationError; type Error = ValidationError;
type Result = PathBuf;
fn validate_and_install(&self, path: PathBuf) -> Result<(String, PathBuf), ValidationError> { fn validate_and_install(&self, path: PathBuf) -> Result<(String, LocalPageEndpoint), ValidationError> {
// Create dir // Create dir
try!(fs::create_dir_all(&self.content_path)); try!(fs::create_dir_all(&self.content_path));
@ -247,21 +258,11 @@ impl ContentValidator for ContentInstaller {
try!(fs::copy(&path, &content_path)); try!(fs::copy(&path, &content_path));
Ok((self.id.clone(), content_path)) Ok((self.id.clone(), LocalPageEndpoint::single_file(content_path, self.mime.clone())))
} }
fn done(&self, result: Option<&PathBuf>) { fn done(&self, endpoint: Option<LocalPageEndpoint>) {
let mut cache = self.cache.lock(); (self.on_done)(self.id.clone(), endpoint)
match result {
Some(result) => {
let page = LocalPageEndpoint::single_file(result.clone(), self.mime.clone());
cache.insert(self.id.clone(), ContentStatus::Ready(page));
},
// In case of error
None => {
cache.remove(&self.id);
},
}
} }
} }
@ -269,7 +270,7 @@ impl ContentValidator for ContentInstaller {
struct DappInstaller { struct DappInstaller {
id: String, id: String,
dapps_path: PathBuf, dapps_path: PathBuf,
cache: Arc<Mutex<ContentCache>>, on_done: Box<Fn(String, Option<LocalPageEndpoint>) + Send>,
} }
impl DappInstaller { impl DappInstaller {
@ -306,9 +307,8 @@ impl DappInstaller {
impl ContentValidator for DappInstaller { impl ContentValidator for DappInstaller {
type Error = ValidationError; type Error = ValidationError;
type Result = Manifest;
fn validate_and_install(&self, app_path: PathBuf) -> Result<(String, Manifest), ValidationError> { fn validate_and_install(&self, app_path: PathBuf) -> Result<(String, LocalPageEndpoint), ValidationError> {
trace!(target: "dapps", "Opening dapp bundle at {:?}", app_path); trace!(target: "dapps", "Opening dapp bundle at {:?}", app_path);
let mut file_reader = io::BufReader::new(try!(fs::File::open(app_path))); let mut file_reader = io::BufReader::new(try!(fs::File::open(app_path)));
let hash = try!(sha3(&mut file_reader)); let hash = try!(sha3(&mut file_reader));
@ -362,23 +362,15 @@ impl ContentValidator for DappInstaller {
let mut manifest_file = try!(fs::File::create(manifest_path)); let mut manifest_file = try!(fs::File::create(manifest_path));
try!(manifest_file.write_all(manifest_str.as_bytes())); try!(manifest_file.write_all(manifest_str.as_bytes()));
// Create endpoint
let app = LocalPageEndpoint::new(target, manifest.clone().into());
// Return modified app manifest // Return modified app manifest
Ok((manifest.id.clone(), manifest)) Ok((manifest.id.clone(), app))
} }
fn done(&self, manifest: Option<&Manifest>) { fn done(&self, endpoint: Option<LocalPageEndpoint>) {
let mut cache = self.cache.lock(); (self.on_done)(self.id.clone(), endpoint)
match manifest {
Some(manifest) => {
let path = self.dapp_target_path(manifest);
let app = LocalPageEndpoint::new(path, manifest.clone().into());
cache.insert(self.id.clone(), ContentStatus::Ready(app));
},
// In case of error
None => {
cache.remove(&self.id);
},
}
} }
} }

View File

@ -3,7 +3,6 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width"> <meta name="viewport" content="width=device-width">
{meta}
<title>{title}</title> <title>{title}</title>
<link rel="stylesheet" href="/parity-utils/styles.css"> <link rel="stylesheet" href="/parity-utils/styles.css">
</head> </head>

View File

@ -23,6 +23,7 @@ use hyper::status::StatusCode;
use util::version; use util::version;
#[derive(Clone)]
pub struct ContentHandler { pub struct ContentHandler {
code: StatusCode, code: StatusCode,
content: String, content: String,
@ -57,18 +58,6 @@ impl ContentHandler {
Self::html(code, format!( Self::html(code, format!(
include_str!("../error_tpl.html"), include_str!("../error_tpl.html"),
title=title, title=title,
meta="",
message=message,
details=details.unwrap_or_else(|| ""),
version=version(),
))
}
pub fn error_with_refresh(code: StatusCode, title: &str, message: &str, details: Option<&str>) -> Self {
Self::html(code, format!(
include_str!("../error_tpl.html"),
title=title,
meta="<meta http-equiv=\"refresh\" content=\"1\">",
message=message, message=message,
details=details.unwrap_or_else(|| ""), details=details.unwrap_or_else(|| ""),
version=version(), version=version(),

View File

@ -19,41 +19,122 @@
use std::{fs, fmt}; use std::{fs, fmt};
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::{mpsc, Arc}; use std::sync::{mpsc, Arc};
use std::sync::atomic::AtomicBool; use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Instant, Duration}; use std::time::{Instant, Duration};
use util::Mutex;
use hyper::{header, server, Decoder, Encoder, Next, Method, Control}; use hyper::{server, Decoder, Encoder, Next, Method, Control};
use hyper::net::HttpStream; use hyper::net::HttpStream;
use hyper::status::StatusCode; use hyper::status::StatusCode;
use handlers::ContentHandler; use handlers::{ContentHandler, Redirection};
use handlers::client::{Client, FetchResult}; use handlers::client::{Client, FetchResult};
use apps::redirection_address; use apps::redirection_address;
use page::LocalPageEndpoint;
const FETCH_TIMEOUT: u64 = 30; const FETCH_TIMEOUT: u64 = 30;
enum FetchState<T: fmt::Debug> { enum FetchState {
NotStarted(String), NotStarted(String),
Error(ContentHandler), Error(ContentHandler),
InProgress { InProgress(mpsc::Receiver<FetchResult>),
deadline: Instant, Done(String, LocalPageEndpoint, Redirection),
receiver: mpsc::Receiver<FetchResult>,
},
Done((String, T)),
} }
pub trait ContentValidator { pub trait ContentValidator {
type Error: fmt::Debug + fmt::Display; type Error: fmt::Debug + fmt::Display;
type Result: fmt::Debug;
fn validate_and_install(&self, app: PathBuf) -> Result<(String, Self::Result), Self::Error>; fn validate_and_install(&self, app: PathBuf) -> Result<(String, LocalPageEndpoint), Self::Error>;
fn done(&self, Option<&Self::Result>); fn done(&self, Option<LocalPageEndpoint>);
}
pub struct FetchControl {
abort: Arc<AtomicBool>,
listeners: Mutex<Vec<(Control, mpsc::Sender<FetchState>)>>,
deadline: Instant,
}
impl Default for FetchControl {
fn default() -> Self {
FetchControl {
abort: Arc::new(AtomicBool::new(false)),
listeners: Mutex::new(Vec::new()),
deadline: Instant::now() + Duration::from_secs(FETCH_TIMEOUT),
}
}
}
impl FetchControl {
fn notify<F: Fn() -> FetchState>(&self, status: F) {
let mut listeners = self.listeners.lock();
for (control, sender) in listeners.drain(..) {
if let Err(e) = sender.send(status()) {
trace!(target: "dapps", "Waiting listener notification failed: {:?}", e);
} else {
let _ = control.ready(Next::read());
}
}
}
fn set_status(&self, status: &FetchState) {
match *status {
FetchState::Error(ref handler) => self.notify(|| FetchState::Error(handler.clone())),
FetchState::Done(ref id, ref endpoint, ref handler) => self.notify(|| FetchState::Done(id.clone(), endpoint.clone(), handler.clone())),
FetchState::NotStarted(_) | FetchState::InProgress(_) => {},
}
}
pub fn abort(&self) {
self.abort.store(true, Ordering::SeqCst);
}
pub fn to_handler(&self, control: Control) -> Box<server::Handler<HttpStream> + Send> {
let (tx, rx) = mpsc::channel();
self.listeners.lock().push((control, tx));
Box::new(WaitingHandler {
receiver: rx,
state: None,
})
}
}
pub struct WaitingHandler {
receiver: mpsc::Receiver<FetchState>,
state: Option<FetchState>,
}
impl server::Handler<HttpStream> for WaitingHandler {
fn on_request(&mut self, _request: server::Request<HttpStream>) -> Next {
Next::wait()
}
fn on_request_readable(&mut self, _decoder: &mut Decoder<HttpStream>) -> Next {
self.state = self.receiver.try_recv().ok();
Next::write()
}
fn on_response(&mut self, res: &mut server::Response) -> Next {
match self.state {
Some(FetchState::Done(_, _, ref mut handler)) => handler.on_response(res),
Some(FetchState::Error(ref mut handler)) => handler.on_response(res),
_ => Next::end(),
}
}
fn on_response_writable(&mut self, encoder: &mut Encoder<HttpStream>) -> Next {
match self.state {
Some(FetchState::Done(_, _, ref mut handler)) => handler.on_response_writable(encoder),
Some(FetchState::Error(ref mut handler)) => handler.on_response_writable(encoder),
_ => Next::end(),
}
}
} }
pub struct ContentFetcherHandler<H: ContentValidator> { pub struct ContentFetcherHandler<H: ContentValidator> {
abort: Arc<AtomicBool>, fetch_control: Arc<FetchControl>,
control: Option<Control>, control: Option<Control>,
status: FetchState<H::Result>, status: FetchState,
client: Option<Client>, client: Option<Client>,
using_dapps_domains: bool, using_dapps_domains: bool,
installer: H, installer: H,
@ -62,7 +143,7 @@ pub struct ContentFetcherHandler<H: ContentValidator> {
impl<H: ContentValidator> Drop for ContentFetcherHandler<H> { impl<H: ContentValidator> Drop for ContentFetcherHandler<H> {
fn drop(&mut self) { fn drop(&mut self) {
let result = match self.status { let result = match self.status {
FetchState::Done((_, ref result)) => Some(result), FetchState::Done(_, ref result, _) => Some(result.clone()),
_ => None, _ => None,
}; };
self.installer.done(result); self.installer.done(result);
@ -73,20 +154,22 @@ impl<H: ContentValidator> ContentFetcherHandler<H> {
pub fn new( pub fn new(
url: String, url: String,
abort: Arc<AtomicBool>,
control: Control, control: Control,
using_dapps_domains: bool, using_dapps_domains: bool,
handler: H) -> Self { handler: H) -> (Self, Arc<FetchControl>) {
let fetch_control = Arc::new(FetchControl::default());
let client = Client::new(); let client = Client::new();
ContentFetcherHandler { let handler = ContentFetcherHandler {
abort: abort, fetch_control: fetch_control.clone(),
control: Some(control), control: Some(control),
client: Some(client), client: Some(client),
status: FetchState::NotStarted(url), status: FetchState::NotStarted(url),
using_dapps_domains: using_dapps_domains, using_dapps_domains: using_dapps_domains,
installer: handler, installer: handler,
} };
(handler, fetch_control)
} }
fn close_client(client: &mut Option<Client>) { fn close_client(client: &mut Option<Client>) {
@ -95,7 +178,6 @@ impl<H: ContentValidator> ContentFetcherHandler<H> {
.close(); .close();
} }
fn fetch_content(client: &mut Client, url: &str, abort: Arc<AtomicBool>, control: Control) -> Result<mpsc::Receiver<FetchResult>, String> { fn fetch_content(client: &mut Client, url: &str, abort: Arc<AtomicBool>, control: Control) -> Result<mpsc::Receiver<FetchResult>, String> {
client.request(url, abort, Box::new(move || { client.request(url, abort, Box::new(move || {
trace!(target: "dapps", "Fetching finished."); trace!(target: "dapps", "Fetching finished.");
@ -114,12 +196,9 @@ impl<H: ContentValidator> server::Handler<HttpStream> for ContentFetcherHandler<
trace!(target: "dapps", "Fetching content from: {:?}", url); trace!(target: "dapps", "Fetching content from: {:?}", url);
let control = self.control.take().expect("on_request is called only once, thus control is always Some"); let control = self.control.take().expect("on_request is called only once, thus control is always Some");
let client = self.client.as_mut().expect("on_request is called before client is closed."); let client = self.client.as_mut().expect("on_request is called before client is closed.");
let fetch = Self::fetch_content(client, url, self.abort.clone(), control); let fetch = Self::fetch_content(client, url, self.fetch_control.abort.clone(), control);
match fetch { match fetch {
Ok(receiver) => FetchState::InProgress { Ok(receiver) => FetchState::InProgress(receiver),
deadline: Instant::now() + Duration::from_secs(FETCH_TIMEOUT),
receiver: receiver,
},
Err(e) => FetchState::Error(ContentHandler::error( Err(e) => FetchState::Error(ContentHandler::error(
StatusCode::BadGateway, StatusCode::BadGateway,
"Unable To Start Dapp Download", "Unable To Start Dapp Download",
@ -139,6 +218,7 @@ impl<H: ContentValidator> server::Handler<HttpStream> for ContentFetcherHandler<
} else { None }; } else { None };
if let Some(status) = status { if let Some(status) = status {
self.fetch_control.set_status(&status);
self.status = status; self.status = status;
} }
@ -148,7 +228,7 @@ impl<H: ContentValidator> server::Handler<HttpStream> for ContentFetcherHandler<
fn on_request_readable(&mut self, decoder: &mut Decoder<HttpStream>) -> Next { fn on_request_readable(&mut self, decoder: &mut Decoder<HttpStream>) -> Next {
let (status, next) = match self.status { let (status, next) = match self.status {
// Request may time out // Request may time out
FetchState::InProgress { ref deadline, .. } if *deadline < Instant::now() => { FetchState::InProgress(_) if self.fetch_control.deadline < Instant::now() => {
trace!(target: "dapps", "Fetching dapp failed because of timeout."); trace!(target: "dapps", "Fetching dapp failed because of timeout.");
let timeout = ContentHandler::error( let timeout = ContentHandler::error(
StatusCode::GatewayTimeout, StatusCode::GatewayTimeout,
@ -159,7 +239,7 @@ impl<H: ContentValidator> server::Handler<HttpStream> for ContentFetcherHandler<
Self::close_client(&mut self.client); Self::close_client(&mut self.client);
(Some(FetchState::Error(timeout)), Next::write()) (Some(FetchState::Error(timeout)), Next::write())
}, },
FetchState::InProgress { ref receiver, .. } => { FetchState::InProgress(ref receiver) => {
// Check if there is an answer // Check if there is an answer
let rec = receiver.try_recv(); let rec = receiver.try_recv();
match rec { match rec {
@ -178,7 +258,10 @@ impl<H: ContentValidator> server::Handler<HttpStream> for ContentFetcherHandler<
Some(&format!("{:?}", e)) Some(&format!("{:?}", e))
)) ))
}, },
Ok(result) => FetchState::Done(result) Ok((id, result)) => {
let address = redirection_address(self.using_dapps_domains, &id);
FetchState::Done(id, result, Redirection::new(&address))
},
}; };
// Remove temporary zip file // Remove temporary zip file
let _ = fs::remove_file(path); let _ = fs::remove_file(path);
@ -203,6 +286,7 @@ impl<H: ContentValidator> server::Handler<HttpStream> for ContentFetcherHandler<
}; };
if let Some(status) = status { if let Some(status) = status {
self.fetch_control.set_status(&status);
self.status = status; self.status = status;
} }
@ -211,12 +295,7 @@ impl<H: ContentValidator> server::Handler<HttpStream> for ContentFetcherHandler<
fn on_response(&mut self, res: &mut server::Response) -> Next { fn on_response(&mut self, res: &mut server::Response) -> Next {
match self.status { match self.status {
FetchState::Done((ref id, _)) => { FetchState::Done(_, _, ref mut handler) => handler.on_response(res),
trace!(target: "dapps", "Fetching content finished. Redirecting to {}", id);
res.set_status(StatusCode::Found);
res.headers_mut().set(header::Location(redirection_address(self.using_dapps_domains, id)));
Next::write()
},
FetchState::Error(ref mut handler) => handler.on_response(res), FetchState::Error(ref mut handler) => handler.on_response(res),
_ => Next::end(), _ => Next::end(),
} }
@ -224,9 +303,9 @@ impl<H: ContentValidator> server::Handler<HttpStream> for ContentFetcherHandler<
fn on_response_writable(&mut self, encoder: &mut Encoder<HttpStream>) -> Next { fn on_response_writable(&mut self, encoder: &mut Encoder<HttpStream>) -> Next {
match self.status { match self.status {
FetchState::Done(_, _, ref mut handler) => handler.on_response_writable(encoder),
FetchState::Error(ref mut handler) => handler.on_response_writable(encoder), FetchState::Error(ref mut handler) => handler.on_response_writable(encoder),
_ => Next::end(), _ => Next::end(),
} }
} }
} }

View File

@ -27,7 +27,7 @@ pub use self::auth::AuthRequiredHandler;
pub use self::echo::EchoHandler; pub use self::echo::EchoHandler;
pub use self::content::ContentHandler; pub use self::content::ContentHandler;
pub use self::redirect::Redirection; pub use self::redirect::Redirection;
pub use self::fetch::{ContentFetcherHandler, ContentValidator}; pub use self::fetch::{ContentFetcherHandler, ContentValidator, FetchControl};
use url::Url; use url::Url;
use hyper::{server, header, net, uri}; use hyper::{server, header, net, uri};

View File

@ -20,15 +20,20 @@ use hyper::{header, server, Decoder, Encoder, Next};
use hyper::net::HttpStream; use hyper::net::HttpStream;
use hyper::status::StatusCode; use hyper::status::StatusCode;
#[derive(Clone)]
pub struct Redirection { pub struct Redirection {
to_url: String to_url: String
} }
impl Redirection { impl Redirection {
pub fn new(url: &str) -> Box<Self> { pub fn new(url: &str) -> Self {
Box::new(Redirection { Redirection {
to_url: url.to_owned() to_url: url.to_owned()
}) }
}
pub fn boxed(url: &str) -> Box<Self> {
Box::new(Self::new(url))
} }
} }

View File

@ -21,6 +21,7 @@ use std::path::{Path, PathBuf};
use page::handler; use page::handler;
use endpoint::{Endpoint, EndpointInfo, EndpointPath, Handler}; use endpoint::{Endpoint, EndpointInfo, EndpointPath, Handler};
#[derive(Debug, Clone)]
pub struct LocalPageEndpoint { pub struct LocalPageEndpoint {
path: PathBuf, path: PathBuf,
mime: Option<String>, mime: Option<String>,

View File

@ -104,7 +104,7 @@ impl<A: Authorization + 'static> server::Handler<HttpStream> for Router<A> {
// Redirect any GET request to home. // Redirect any GET request to home.
_ if *req.method() == hyper::method::Method::Get => { _ if *req.method() == hyper::method::Method::Get => {
let address = apps::redirection_address(false, self.main_page); let address = apps::redirection_address(false, self.main_page);
Redirection::new(address.as_str()) Redirection::boxed(address.as_str())
}, },
// RPC by default // RPC by default
_ => { _ => {