Content resolving
This commit is contained in:
@@ -37,60 +37,60 @@ use handlers::{ContentHandler, ContentFetcherHandler, ContentValidator};
|
||||
use endpoint::{Endpoint, EndpointPath, Handler};
|
||||
use apps::cache::{ContentCache, ContentStatus};
|
||||
use apps::manifest::{MANIFEST_FILENAME, deserialize_manifest, serialize_manifest, Manifest};
|
||||
use apps::urlhint::{URLHintContract, URLHint};
|
||||
use apps::urlhint::{URLHintContract, URLHint, URLHintResult};
|
||||
|
||||
const MAX_CACHED_DAPPS: usize = 10;
|
||||
|
||||
pub struct AppFetcher<R: URLHint = URLHintContract> {
|
||||
pub struct ContentFetcher<R: URLHint = URLHintContract> {
|
||||
dapps_path: PathBuf,
|
||||
resolver: R,
|
||||
dapps: Arc<Mutex<ContentCache>>,
|
||||
cache: Arc<Mutex<ContentCache>>,
|
||||
}
|
||||
|
||||
impl<R: URLHint> Drop for AppFetcher<R> {
|
||||
impl<R: URLHint> Drop for ContentFetcher<R> {
|
||||
fn drop(&mut self) {
|
||||
// Clear cache path
|
||||
let _ = fs::remove_dir_all(&self.dapps_path);
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: URLHint> AppFetcher<R> {
|
||||
impl<R: URLHint> ContentFetcher<R> {
|
||||
|
||||
pub fn new(resolver: R) -> Self {
|
||||
let mut dapps_path = env::temp_dir();
|
||||
dapps_path.push(random_filename());
|
||||
|
||||
AppFetcher {
|
||||
ContentFetcher {
|
||||
dapps_path: dapps_path,
|
||||
resolver: resolver,
|
||||
dapps: Arc::new(Mutex::new(ContentCache::default())),
|
||||
cache: Arc::new(Mutex::new(ContentCache::default())),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn set_status(&self, app_id: &str, status: ContentStatus) {
|
||||
self.dapps.lock().insert(app_id.to_owned(), status);
|
||||
fn set_status(&self, content_id: &str, status: ContentStatus) {
|
||||
self.cache.lock().insert(content_id.to_owned(), status);
|
||||
}
|
||||
|
||||
pub fn contains(&self, app_id: &str) -> bool {
|
||||
let mut dapps = self.dapps.lock();
|
||||
match dapps.get(app_id) {
|
||||
pub fn contains(&self, content_id: &str) -> bool {
|
||||
let mut cache = self.cache.lock();
|
||||
match cache.get(content_id) {
|
||||
// Check if we already have the app
|
||||
Some(_) => true,
|
||||
// fallback to resolver
|
||||
None => match app_id.from_hex() {
|
||||
Ok(app_id) => self.resolver.resolve(app_id).is_some(),
|
||||
None => match content_id.from_hex() {
|
||||
Ok(content_id) => self.resolver.resolve(content_id).is_some(),
|
||||
_ => false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_async_handler(&self, path: EndpointPath, control: hyper::Control) -> Box<Handler> {
|
||||
let mut dapps = self.dapps.lock();
|
||||
let app_id = path.app_id.clone();
|
||||
let mut cache = self.cache.lock();
|
||||
let content_id = path.app_id.clone();
|
||||
|
||||
let (new_status, handler) = {
|
||||
let status = dapps.get(&app_id);
|
||||
let status = cache.get(&content_id);
|
||||
match status {
|
||||
// Just server dapp
|
||||
Some(&mut ContentStatus::Ready(ref endpoint)) => {
|
||||
@@ -109,28 +109,42 @@ impl<R: URLHint> AppFetcher<R> {
|
||||
},
|
||||
// We need to start fetching app
|
||||
None => {
|
||||
let app_hex = app_id.from_hex().expect("to_handler is called only when `contains` returns true.");
|
||||
let app_hex = content_id.from_hex().expect("to_handler is called only when `contains` returns true.");
|
||||
let app = self.resolver.resolve(app_hex).expect("to_handler is called only when `contains` returns true.");
|
||||
let abort = Arc::new(AtomicBool::new(false));
|
||||
|
||||
(Some(ContentStatus::Fetching(abort.clone())), Box::new(ContentFetcherHandler::new(
|
||||
app,
|
||||
abort,
|
||||
control,
|
||||
path.using_dapps_domains,
|
||||
DappInstaller {
|
||||
dapp_id: app_id.clone(),
|
||||
dapps_path: self.dapps_path.clone(),
|
||||
dapps: self.dapps.clone(),
|
||||
}
|
||||
)) as Box<Handler>)
|
||||
(Some(ContentStatus::Fetching(abort.clone())), match app {
|
||||
URLHintResult::Dapp(dapp) => Box::new(ContentFetcherHandler::new(
|
||||
dapp.url(),
|
||||
abort,
|
||||
control,
|
||||
path.using_dapps_domains,
|
||||
DappInstaller {
|
||||
id: content_id.clone(),
|
||||
dapps_path: self.dapps_path.clone(),
|
||||
cache: self.cache.clone(),
|
||||
}
|
||||
)) as Box<Handler>,
|
||||
URLHintResult::Content(content) => Box::new(ContentFetcherHandler::new(
|
||||
content.url,
|
||||
abort,
|
||||
control,
|
||||
path.using_dapps_domains,
|
||||
ContentInstaller {
|
||||
id: content_id.clone(),
|
||||
mime: content.mime,
|
||||
content_path: self.dapps_path.clone(),
|
||||
cache: self.cache.clone(),
|
||||
}
|
||||
)) as Box<Handler>,
|
||||
})
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(status) = new_status {
|
||||
dapps.clear_garbage(MAX_CACHED_DAPPS);
|
||||
dapps.insert(app_id, status);
|
||||
cache.clear_garbage(MAX_CACHED_DAPPS);
|
||||
cache.insert(content_id, status);
|
||||
}
|
||||
|
||||
handler
|
||||
@@ -176,10 +190,51 @@ impl From<zip::result::ZipError> for ValidationError {
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentInstaller {
|
||||
id: String,
|
||||
mime: String,
|
||||
content_path: PathBuf,
|
||||
cache: Arc<Mutex<ContentCache>>,
|
||||
}
|
||||
|
||||
impl ContentValidator for ContentInstaller {
|
||||
type Error = ValidationError;
|
||||
type Result = PathBuf;
|
||||
|
||||
fn validate_and_install(&self, path: PathBuf) -> Result<(String, PathBuf), ValidationError> {
|
||||
let filename = path.file_name().expect("We always fetch a file.");
|
||||
let mut content_path = self.content_path.clone();
|
||||
content_path.push(&filename);
|
||||
|
||||
if content_path.exists() {
|
||||
try!(fs::remove_dir_all(&content_path))
|
||||
}
|
||||
|
||||
try!(fs::copy(&path, &content_path));
|
||||
|
||||
Ok((self.id.clone(), content_path))
|
||||
}
|
||||
|
||||
fn done(&self, result: Option<&PathBuf>) {
|
||||
let mut cache = self.cache.lock();
|
||||
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);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct DappInstaller {
|
||||
dapp_id: String,
|
||||
id: String,
|
||||
dapps_path: PathBuf,
|
||||
dapps: Arc<Mutex<ContentCache>>,
|
||||
cache: Arc<Mutex<ContentCache>>,
|
||||
}
|
||||
|
||||
impl DappInstaller {
|
||||
@@ -216,15 +271,16 @@ impl DappInstaller {
|
||||
|
||||
impl ContentValidator for DappInstaller {
|
||||
type Error = ValidationError;
|
||||
type Result = Manifest;
|
||||
|
||||
fn validate_and_install(&self, app_path: PathBuf) -> Result<Manifest, ValidationError> {
|
||||
fn validate_and_install(&self, app_path: PathBuf) -> Result<(String, Manifest), ValidationError> {
|
||||
trace!(target: "dapps", "Opening dapp bundle at {:?}", app_path);
|
||||
let mut file_reader = io::BufReader::new(try!(fs::File::open(app_path)));
|
||||
let hash = try!(sha3(&mut file_reader));
|
||||
let dapp_id = try!(self.dapp_id.as_str().parse().map_err(|_| ValidationError::InvalidDappId));
|
||||
if dapp_id != hash {
|
||||
let id = try!(self.id.as_str().parse().map_err(|_| ValidationError::InvalidDappId));
|
||||
if id != hash {
|
||||
return Err(ValidationError::HashMismatch {
|
||||
expected: dapp_id,
|
||||
expected: id,
|
||||
got: hash,
|
||||
});
|
||||
}
|
||||
@@ -234,7 +290,7 @@ impl ContentValidator for DappInstaller {
|
||||
// First find manifest file
|
||||
let (mut manifest, manifest_dir) = try!(Self::find_manifest(&mut zip));
|
||||
// Overwrite id to match hash
|
||||
manifest.id = self.dapp_id.clone();
|
||||
manifest.id = self.id.clone();
|
||||
|
||||
let target = self.dapp_target_path(&manifest);
|
||||
|
||||
@@ -272,20 +328,20 @@ impl ContentValidator for DappInstaller {
|
||||
try!(manifest_file.write_all(manifest_str.as_bytes()));
|
||||
|
||||
// Return modified app manifest
|
||||
Ok(manifest)
|
||||
Ok((manifest.id.clone(), manifest))
|
||||
}
|
||||
|
||||
fn done(&self, manifest: Option<&Manifest>) {
|
||||
let mut dapps = self.dapps.lock();
|
||||
let mut cache = self.cache.lock();
|
||||
match manifest {
|
||||
Some(manifest) => {
|
||||
let path = self.dapp_target_path(manifest);
|
||||
let app = LocalPageEndpoint::new(path, manifest.clone().into());
|
||||
dapps.insert(self.dapp_id.clone(), ContentStatus::Ready(app));
|
||||
cache.insert(self.id.clone(), ContentStatus::Ready(app));
|
||||
},
|
||||
// In case of error
|
||||
None => {
|
||||
dapps.remove(&self.dapp_id);
|
||||
cache.remove(&self.id);
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -298,12 +354,12 @@ mod tests {
|
||||
use endpoint::EndpointInfo;
|
||||
use page::LocalPageEndpoint;
|
||||
use apps::cache::ContentStatus;
|
||||
use apps::urlhint::{GithubApp, URLHint};
|
||||
use super::AppFetcher;
|
||||
use apps::urlhint::{URLHint, URLHintResult};
|
||||
use super::ContentFetcher;
|
||||
|
||||
struct FakeResolver;
|
||||
impl URLHint for FakeResolver {
|
||||
fn resolve(&self, _app_id: Bytes) -> Option<GithubApp> {
|
||||
fn resolve(&self, _id: Bytes) -> Option<URLHintResult> {
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -312,7 +368,7 @@ mod tests {
|
||||
fn should_true_if_contains_the_app() {
|
||||
// given
|
||||
let path = env::temp_dir();
|
||||
let fetcher = AppFetcher::new(FakeResolver);
|
||||
let fetcher = ContentFetcher::new(FakeResolver);
|
||||
let handler = LocalPageEndpoint::new(path, EndpointInfo {
|
||||
name: "fake".into(),
|
||||
description: "".into(),
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
use rustc_serialize::hex::ToHex;
|
||||
use mime_guess;
|
||||
|
||||
use ethabi::{Interface, Contract, Token};
|
||||
use util::{Address, Bytes, Hashable};
|
||||
@@ -52,6 +53,13 @@ impl GithubApp {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct Content {
|
||||
pub url: String,
|
||||
pub mime: String,
|
||||
pub owner: Address,
|
||||
}
|
||||
|
||||
/// RAW Contract interface.
|
||||
/// Should execute transaction using current blockchain state.
|
||||
pub trait ContractClient: Send + Sync {
|
||||
@@ -61,10 +69,19 @@ pub trait ContractClient: Send + Sync {
|
||||
fn call(&self, address: Address, data: Bytes) -> Result<Bytes, String>;
|
||||
}
|
||||
|
||||
/// Result of resolving id to URL
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum URLHintResult {
|
||||
/// Dapp
|
||||
Dapp(GithubApp),
|
||||
/// Content
|
||||
Content(Content),
|
||||
}
|
||||
|
||||
/// URLHint Contract interface
|
||||
pub trait URLHint {
|
||||
/// Resolves given id to registrar entry.
|
||||
fn resolve(&self, app_id: Bytes) -> Option<GithubApp>;
|
||||
fn resolve(&self, id: Bytes) -> Option<URLHintResult>;
|
||||
}
|
||||
|
||||
pub struct URLHintContract {
|
||||
@@ -110,10 +127,10 @@ impl URLHintContract {
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_urlhint_call(&self, app_id: Bytes) -> Option<Bytes> {
|
||||
fn encode_urlhint_call(&self, id: Bytes) -> Option<Bytes> {
|
||||
let call = self.urlhint
|
||||
.function("entries".into())
|
||||
.and_then(|f| f.encode_call(vec![Token::FixedBytes(app_id)]));
|
||||
.and_then(|f| f.encode_call(vec![Token::FixedBytes(id)]));
|
||||
|
||||
match call {
|
||||
Ok(res) => {
|
||||
@@ -126,7 +143,7 @@ impl URLHintContract {
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_urlhint_output(&self, output: Bytes) -> Option<GithubApp> {
|
||||
fn decode_urlhint_output(&self, output: Bytes) -> Option<URLHintResult> {
|
||||
trace!(target: "dapps", "Output: {:?}", output.to_hex());
|
||||
let output = self.urlhint
|
||||
.function("entries".into())
|
||||
@@ -149,6 +166,17 @@ impl URLHintContract {
|
||||
if owner == Address::default() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let commit = GithubApp::commit(&commit);
|
||||
if commit == Some(Default::default()) {
|
||||
let mime = guess_mime_type(&account_slash_repo).unwrap_or("application/octet-stream".into());
|
||||
return Some(URLHintResult::Content(Content {
|
||||
url: account_slash_repo,
|
||||
mime: mime,
|
||||
owner: owner,
|
||||
}));
|
||||
}
|
||||
|
||||
let (account, repo) = {
|
||||
let mut it = account_slash_repo.split('/');
|
||||
match (it.next(), it.next()) {
|
||||
@@ -157,12 +185,12 @@ impl URLHintContract {
|
||||
}
|
||||
};
|
||||
|
||||
GithubApp::commit(&commit).map(|commit| GithubApp {
|
||||
commit.map(|commit| URLHintResult::Dapp(GithubApp {
|
||||
account: account,
|
||||
repo: repo,
|
||||
commit: commit,
|
||||
owner: owner,
|
||||
})
|
||||
}))
|
||||
},
|
||||
e => {
|
||||
warn!(target: "dapps", "Invalid contract output parameters: {:?}", e);
|
||||
@@ -177,10 +205,10 @@ impl URLHintContract {
|
||||
}
|
||||
|
||||
impl URLHint for URLHintContract {
|
||||
fn resolve(&self, app_id: Bytes) -> Option<GithubApp> {
|
||||
fn resolve(&self, id: Bytes) -> Option<URLHintResult> {
|
||||
self.urlhint_address().and_then(|address| {
|
||||
// Prepare contract call
|
||||
self.encode_urlhint_call(app_id)
|
||||
self.encode_urlhint_call(id)
|
||||
.and_then(|data| {
|
||||
let call = self.client.call(address, data);
|
||||
if let Err(ref e) = call {
|
||||
@@ -193,6 +221,34 @@ impl URLHint for URLHintContract {
|
||||
}
|
||||
}
|
||||
|
||||
fn guess_mime_type(url: &str) -> Option<String> {
|
||||
const CONTENT_TYPE: &'static str = "content-type=";
|
||||
|
||||
let mut it = url.split('#');
|
||||
// skip url
|
||||
let url = it.next();
|
||||
// get meta headers
|
||||
let metas = it.next();
|
||||
if let Some(metas) = metas {
|
||||
for meta in metas.split('&') {
|
||||
let meta = meta.to_lowercase();
|
||||
if meta.starts_with(CONTENT_TYPE) {
|
||||
return Some(meta[CONTENT_TYPE.len()..].to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
url.and_then(|url| {
|
||||
url.split('.').last()
|
||||
}).and_then(|extension| {
|
||||
mime_guess::get_mime_type_str(extension).map(Into::into)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn test_guess_mime_type(url: &str) -> Option<String> {
|
||||
guess_mime_type(url)
|
||||
}
|
||||
|
||||
fn as_string<T: fmt::Debug>(e: T) -> String {
|
||||
format!("{:?}", e)
|
||||
}
|
||||
@@ -201,7 +257,7 @@ fn as_string<T: fmt::Debug>(e: T) -> String {
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
use std::str::FromStr;
|
||||
use rustc_serialize::hex::{ToHex, FromHex};
|
||||
use rustc_serialize::hex::FromHex;
|
||||
|
||||
use super::*;
|
||||
use util::{Bytes, Address, Mutex, ToPretty};
|
||||
@@ -279,12 +335,12 @@ mod tests {
|
||||
let res = urlhint.resolve("test".bytes().collect());
|
||||
|
||||
// then
|
||||
assert_eq!(res, Some(GithubApp {
|
||||
assert_eq!(res, Some(URLHintResult::Dapp(GithubApp {
|
||||
account: "ethcore".into(),
|
||||
repo: "dao.claim".into(),
|
||||
commit: GithubApp::commit(&"ec4c1fe06c808fe3739858c347109b1f5f1ed4b5".from_hex().unwrap()).unwrap(),
|
||||
owner: Address::from_str("deadcafebeefbeefcafedeaddeedfeedffffffff").unwrap(),
|
||||
}))
|
||||
})))
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -303,4 +359,20 @@ mod tests {
|
||||
// then
|
||||
assert_eq!(url, "https://codeload.github.com/test/xyz/zip/000102030405060708090a0b0c0d0e0f10111213".to_owned());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_guess_mime_type_from_url() {
|
||||
let url1 = "https://ethcore.io/parity";
|
||||
let url2 = "https://ethcore.io/parity#content-type=image/png";
|
||||
let url3 = "https://ethcore.io/parity#something&content-type=image/png";
|
||||
let url4 = "https://ethcore.io/parity.png#content-type=image/jpeg";
|
||||
let url5 = "https://ethcore.io/parity.png";
|
||||
|
||||
|
||||
assert_eq!(test_guess_mime_type(url1), None);
|
||||
assert_eq!(test_guess_mime_type(url2), Some("image/png".into()));
|
||||
assert_eq!(test_guess_mime_type(url3), Some("image/png".into()));
|
||||
assert_eq!(test_guess_mime_type(url4), Some("image/jpeg".into()));
|
||||
assert_eq!(test_guess_mime_type(url5), Some("image/png".into()));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user