use actix_http::header::{HeaderMap, ACCEPT};
use actix_web::{HttpRequest};
use ant_core::data::XorName;
use chrono::DateTime;
use log::{debug, info};
use crate::model::archive::Archive;
use crate::service::html_directory_renderer::HtmlDirectoryRenderer;
use crate::service::resolver_service::ResolvedAddress;
#[derive(Clone)]
pub struct ArchiveHelper {
archive: Archive
}
#[derive(Clone)]
pub struct ArchiveInfo {
pub path_string: String,
pub resolved_xor_addr: XorName,
pub action: ArchiveAction,
pub is_modified: bool,
pub offset: u64,
pub size: u64,
pub limit: u64,
pub modified_time: u64,
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum ArchiveAction {
Data, Listing, Redirect, NotFound
}
impl ArchiveInfo {
pub fn new(path_string: String, resolved_xor_addr: XorName, action: ArchiveAction, is_modified: bool, offset: u64, size: u64, modified_time: u64) -> ArchiveInfo {
let limit = if size > 0 { size - 1 } else { 0 };
ArchiveInfo { path_string, resolved_xor_addr, action, is_modified, offset, size, limit, modified_time }
}
}
impl ArchiveHelper {
pub fn new(archive: Archive) -> ArchiveHelper {
ArchiveHelper { archive }
}
pub fn list_files(&self, path: String, header_map: &HeaderMap) -> String {
if header_map.contains_key(ACCEPT)
&& header_map.get(ACCEPT).unwrap().to_str().unwrap_or("").to_string().contains( "json") {
self.list_files_json(path)
} else {
self.list_files_html(path)
}
}
fn list_files_html(&self, path: String) -> String {
HtmlDirectoryRenderer::render(&self.archive, path)
}
fn list_files_json(&self, path: String) -> String {
let mut output = "[\n".to_string();
let list_dir = self.archive.list_dir(path);
let mut i = 1;
let count = list_dir.len();
for path_detail in list_dir {
let mtime_datetime = DateTime::from_timestamp_millis( i64::try_from(path_detail.modified)
.unwrap_or(0) * 1000)
.unwrap_or(DateTime::default());
let mtime_iso = mtime_datetime.format("%+");
output.push_str("{");
output.push_str(
&format!("\"name\": \"{}\", \"type\": \"{:?}\", \"mtime\": \"{}\", \"size\": \"{}\"",
path_detail.display, path_detail.path_type, mtime_iso, path_detail.size));
output.push_str("}");
if i < count {
output.push_str(",");
}
output.push_str("\n");
i+=1;
}
output.push_str("]");
debug!("list_files_json: {}", output);
output
}
pub async fn resolve_archive_info(&self, resolved_address: &ResolvedAddress, request: &HttpRequest, resolved_route_path: &String, has_route_map: bool) -> ArchiveInfo {
let request_path = request.path();
if self.has_moved_permanently(request_path, &resolved_route_path) {
debug!("has moved permanently");
ArchiveInfo::new(resolved_route_path.clone(), XorName::default(), ArchiveAction::Redirect, true, 0, 0, 0)
} else if has_route_map {
debug!("retrieve route map index");
match self.archive.find_file(resolved_route_path) {
Some(data_address_offset) => {
info!("Resolved path [{}] to xor address [{}]", resolved_route_path, hex::encode(data_address_offset.data_address));
ArchiveInfo::new(
data_address_offset.path.clone(),
data_address_offset.data_address,
ArchiveAction::Data,
resolved_address.is_modified,
data_address_offset.offset,
data_address_offset.size,
data_address_offset.modified
)
}
None => ArchiveInfo::new(resolved_route_path.clone(), XorName::default(), ArchiveAction::NotFound, true, 0, 0, 0)
}
} else if !resolved_route_path.is_empty() {
debug!("retrieve path and data address");
match self.archive.find_file(&resolved_address.file_path) {
Some(data_address_offset) => {
info!("Resolved path [{}] to xor address [{}]", resolved_route_path, hex::encode(data_address_offset.data_address));
ArchiveInfo::new(
resolved_route_path.clone(),
data_address_offset.data_address,
ArchiveAction::Data,
resolved_address.is_modified,
data_address_offset.offset,
data_address_offset.size,
data_address_offset.modified
)
}
None => if !self.archive.list_dir(resolved_address.file_path.clone()).is_empty() {
if resolved_address.file_path.to_string().chars().last() != Some('/') {
ArchiveInfo::new(format!("{}/", resolved_address.file_path.clone()), XorName::default(), ArchiveAction::Redirect, true, 0, 0, 0)
} else {
let default_index = format!("{}index.html", resolved_address.file_path.clone());
debug!("Lookup default index: {}", default_index);
match self.archive.find_file(&default_index) {
Some(data_address_offset) => {
info!("Resolved path [{}] to xor address [{}] to default [{}]", resolved_route_path, hex::encode(data_address_offset.data_address), default_index);
ArchiveInfo::new(
resolved_route_path.clone(),
data_address_offset.data_address,
ArchiveAction::Data,
resolved_address.is_modified,
data_address_offset.offset,
data_address_offset.size,
data_address_offset.modified
)
}
None => {
debug!("default index not found, retrieve file listing");
ArchiveInfo::new(resolved_address.file_path.clone(), XorName::default(), ArchiveAction::Listing, true, 0, 0, 0)
}
}
}
} else {
ArchiveInfo::new(resolved_route_path.clone(), XorName::default(), ArchiveAction::NotFound, true, 0, 0, 0)
}
}
} else {
let default_index = format!("{}index.html", resolved_address.file_path.clone());
debug!("Lookup default index: {}", default_index);
match self.archive.find_file(&default_index) {
Some(data_address_offset) => {
info!("Resolved path [{}] to xor address [{}] to default [{}]", resolved_route_path, hex::encode(data_address_offset.data_address), default_index);
ArchiveInfo::new(
resolved_route_path.clone(),
data_address_offset.data_address,
ArchiveAction::Data,
resolved_address.is_modified,
data_address_offset.offset,
data_address_offset.size,
data_address_offset.modified
)
}
None => {
debug!("default index not found, retrieve file listing");
ArchiveInfo::new(resolved_address.file_path.clone(), XorName::default(), ArchiveAction::Listing, true, 0, 0, 0)
}
}
}
}
fn has_moved_permanently(&self, request_path: &str, resolved_relative_path_route: &String) -> bool {
resolved_relative_path_route.is_empty() && request_path.to_string().chars().last() != Some('/')
}
}
#[cfg(test)]
mod tests {
use super::*;
use actix_web::test::TestRequest;
use std::collections::HashMap;
use crate::model::archive::DataAddressOffset;
use actix_http::header::HeaderName;
use hex::FromHex;
fn create_test_archive() -> Archive {
let mut map = HashMap::new();
let mut vec = Vec::new();
let file1 = DataAddressOffset {
data_address: XorName::from_hex("0000000000000000000000000000000000000000000000000000000000000000").unwrap(),
path: "index.html".to_string(),
offset: 0,
size: 100,
modified: 1,
signature: None,
};
map.insert("index.html".to_string(), file1.clone());
vec.push(file1);
let file2 = DataAddressOffset {
data_address: XorName::from_hex("0000000000000000000000000000000000000000000000000000000000000000").unwrap(),
path: "style.css".to_string(),
offset: 100,
size: 50,
modified: 2,
signature: None,
};
map.insert("style.css".to_string(), file2.clone());
vec.push(file2);
let file3 = DataAddressOffset {
data_address: XorName::from_hex("0000000000000000000000000000000000000000000000000000000000000000").unwrap(),
path: "sub/test.txt".to_string(),
offset: 150,
size: 20,
modified: 3,
signature: None,
};
map.insert("sub/test.txt".to_string(), file3.clone());
vec.push(file3);
Archive::new(map, vec, crate::model::archive::ArchiveType::Public)
}
fn create_resolved_address(file_path: &str) -> ResolvedAddress {
ResolvedAddress::new(
true,
None,
XorName::default(),
file_path.to_string(),
false,
false,
true,
5
)
}
#[test]
fn test_list_files_html() {
let archive = create_test_archive();
let helper = ArchiveHelper::new(archive);
let header_map = HeaderMap::new();
let output = helper.list_files("".to_string(), &header_map);
assert!(output.contains("Index of /"));
assert!(output.contains("index.html"));
assert!(output.contains("style.css"));
assert!(output.contains("sub/"));
}
#[test]
fn test_list_files_json() {
let archive = create_test_archive();
let helper = ArchiveHelper::new(archive);
let mut header_map = HeaderMap::new();
header_map.insert(HeaderName::from_static("accept"), "application/json".parse().unwrap());
let output = helper.list_files("".to_string(), &header_map);
assert!(output.contains("["));
assert!(output.contains("\"name\": \"index.html\""));
assert!(output.contains("\"name\": \"style.css\""));
assert!(output.contains("]"));
}
#[actix_web::test]
async fn test_resolve_file() {
let archive = create_test_archive();
let helper = ArchiveHelper::new(archive);
let req = TestRequest::with_uri("/index.html").to_http_request();
let resolved_addr = create_resolved_address("index.html");
let info = helper.resolve_archive_info(&resolved_addr, &req, &"index.html".to_string(), false).await;
assert_eq!(info.action, ArchiveAction::Data);
assert_eq!(info.path_string, "index.html");
assert_eq!(info.size, 100);
}
#[actix_web::test]
async fn test_resolve_directory_redirect() {
let archive = create_test_archive();
let helper = ArchiveHelper::new(archive);
let req = TestRequest::with_uri("/sub").to_http_request();
let resolved_addr = create_resolved_address("sub");
let info = helper.resolve_archive_info(&resolved_addr, &req, &"sub".to_string(), false).await;
assert_eq!(info.action, ArchiveAction::Redirect);
assert_eq!(info.path_string, "sub/");
}
#[actix_web::test]
async fn test_resolve_directory_index() {
let archive = create_test_archive();
let helper = ArchiveHelper::new(archive);
let req = TestRequest::with_uri("/").to_http_request();
let resolved_addr = create_resolved_address("");
let info = helper.resolve_archive_info(&resolved_addr, &req, &"".to_string(), false).await;
assert_eq!(info.action, ArchiveAction::Data);
assert_eq!(info.path_string, ""); assert_eq!(info.size, 100);
}
#[actix_web::test]
async fn test_resolve_directory_listing() {
let archive = create_test_archive();
let helper = ArchiveHelper::new(archive);
let req = TestRequest::with_uri("/sub/").to_http_request();
let resolved_addr = create_resolved_address("sub/");
let info = helper.resolve_archive_info(&resolved_addr, &req, &"".to_string(), false).await;
assert_eq!(info.action, ArchiveAction::Listing);
assert_eq!(info.path_string, "sub/");
}
#[actix_web::test]
async fn test_resolve_not_found() {
let archive = create_test_archive();
let helper = ArchiveHelper::new(archive);
let req = TestRequest::with_uri("/missing.txt").to_http_request();
let resolved_addr = create_resolved_address("missing.txt");
let info = helper.resolve_archive_info(&resolved_addr, &req, &"missing.txt".to_string(), false).await;
assert_eq!(info.action, ArchiveAction::NotFound);
}
}