use rocket::fs::NamedFile;
use rocket::http::Status;
use rocket::request::Request;
use rocket::response::{Responder, Response};
use rocket::tokio::fs::create_dir_all;
use rocket_etag_if_none_match::{EtagIfNoneMatch, entity_tag::EntityTag};
use std::borrow::Cow;
use std::fs;
use std::fs::File;
use std::io;
use std::io::Error;
use std::io::ErrorKind;
use std::io::Write;
use std::os::unix::fs::MetadataExt;
use std::path::{Path, PathBuf};
use crate::config::Config;
use crate::tile_request::TileRequest;
use crate::tile_request::WmtsRequest;
use crate::tile_request::XyzRequest;
pub struct CachedFile {
named_file: Option<NamedFile>,
etag_value: String,
}
impl CachedFile {
pub async fn from_path<P: AsRef<Path>>(
etag_if_none_match: EtagIfNoneMatch<'_>,
path: P,
) -> Option<CachedFile> {
if path.as_ref().exists() && !path.as_ref().is_dir() {
let etag_value = CachedFile::etag_value(path.as_ref());
let et = unsafe { EntityTag::new_unchecked(false, Cow::Borrowed(&etag_value)) };
if etag_if_none_match.strong_eq(&et) {
return Some(CachedFile {
etag_value,
named_file: None,
});
}
if let Ok(f) = CachedFile::open(path).await {
return Some(f);
}
}
None
}
async fn open<P: AsRef<Path>>(path: P) -> io::Result<CachedFile> {
let etag_value = CachedFile::etag_value(path.as_ref());
let named_file = NamedFile::open(path).await?;
Ok(CachedFile {
named_file: Some(named_file),
etag_value,
})
}
pub fn etag_value<P: AsRef<Path>>(path: P) -> String {
let metadata = fs::metadata(path.as_ref()).unwrap();
let content_length = metadata.len();
let mtime = metadata.mtime();
format!(r#"{}-{}"#, mtime, content_length)
}
}
impl<'r> Responder<'r, 'static> for CachedFile {
fn respond_to(self, req: &Request) -> rocket::response::Result<'static> {
if let Some(f) = self.named_file {
return Response::build_from(f.respond_to(req)?)
.raw_header("Etag", self.etag_value)
.ok();
}
Err(Status::NotModified)
}
}
pub struct Cache {
config: Config,
}
impl Cache {
async fn create_dir(&self, dirpath: PathBuf) -> Option<Error> {
if !dirpath.exists() {
if let Err(error) = create_dir_all(dirpath).await {
info!("Failed to create directory, error={}", error);
return Some(Error::new(ErrorKind::Other, "!"));
}
}
None
}
fn get_wmts_url_template(&self, alias: &str) -> Option<String> {
for conf in self.config.clone().wmts {
info!("conf.alias={}, alias={}", conf.alias, alias);
if conf.alias.eq(alias) {
return Some(conf.url);
}
}
info!("No template found for {}", alias);
None
}
fn get_xyz_url_template(&self, alias: &str) -> Option<String> {
for conf in self.config.clone().xyz {
info!("conf.alias={}, alias={}", conf.alias, alias);
if conf.alias.eq(alias) {
return Some(conf.url);
}
}
info!("No template found for {}", alias);
None
}
pub async fn get_or_download_wmts(
&self,
etag_if_none_match: EtagIfNoneMatch<'_>,
request: WmtsRequest,
) -> std::io::Result<CachedFile> {
let path = request.filepath(self.config.directory.as_str());
if let Some(f) = CachedFile::from_path(etag_if_none_match, path.clone()).await {
return Ok(f);
}
let dirpath = request.dirpath(self.config.directory.as_str());
if let Some(error) = self.create_dir(dirpath).await {
return Err(error);
}
if let Some(url_template) = self.get_wmts_url_template(request.alias.as_str()) {
let url = url_template
.replace("{layer}", request.layer.as_str())
.replace("{style}", request.style.as_str())
.replace("{tilematrixset}", request.tilematrixset.as_str())
.replace("{Service}", request.service.as_str())
.replace("{Request}", request.request.as_str())
.replace("{Version}", request.version.as_str())
.replace("{Format}", request.format.as_str())
.replace("{TileMatrix}", request.tile_matrix.as_str())
.replace("{TileCol}", request.tile_col.as_str())
.replace("{TileRow}", request.tile_row.as_str());
if let Some(filepath) = path.to_str() {
match Self::download(url.as_str(), filepath).await {
Ok(_) => return CachedFile::open(path).await,
Err(error) => {
info!("Failed to download or copy, error={}", error);
return Err(Error::new(ErrorKind::NotFound, "!"));
}
}
}
}
info!("No template found for {}", request.get_alias());
Err(Error::new(ErrorKind::InvalidInput, "!"))
}
pub async fn get_or_download_xyz(
&self,
etag_if_none_match: EtagIfNoneMatch<'_>,
request: XyzRequest,
) -> std::io::Result<CachedFile> {
let path = request.filepath(self.config.directory.as_str());
if let Some(f) = CachedFile::from_path(etag_if_none_match, path.clone()).await {
return Ok(f);
}
if path.exists() && !path.is_dir() {
return CachedFile::open(path).await;
}
let dirpath = request.dirpath(self.config.directory.as_str());
if let Some(error) = self.create_dir(dirpath).await {
return Err(error);
}
if let Some(url_template) = self.get_xyz_url_template(request.alias.as_str()) {
let url = url_template
.replace("{a}", request.a.as_str())
.replace("{x}", request.x.as_str())
.replace("{y}", request.y.as_str())
.replace("{z}", request.z.as_str());
if let Some(filepath) = path.to_str() {
match Self::download(url.as_str(), filepath).await {
Ok(_) => return CachedFile::open(path).await,
Err(error) => {
info!("Failed to download or copy, error={}", error);
return Err(Error::new(ErrorKind::NotFound, "!"));
}
}
}
}
info!("No template found for {}", request.alias);
Err(Error::new(ErrorKind::InvalidInput, "!"))
}
async fn download(url: &str, filepath: &str) -> Result<(), String> {
info!("download {}", url);
match reqwest::get(url).await {
Err(download_error) => Err(download_error.to_string()),
Ok(response) => {
let file_creation = File::create(filepath);
if file_creation.is_err() {
return Err(format!(
"Failed to create the file, error={}",
file_creation.err().unwrap()
));
}
let mut file = file_creation.ok().unwrap();
let file_reading = response.bytes().await;
if file_reading.is_err() {
return Err(format!(
"Failed to read, error={}",
file_reading.err().unwrap()
));
}
let content = file_reading.ok().unwrap();
info!("Writing {}", filepath);
file.write_all(&content)
.map_err(|e| panic!("Failed to copy, error={}", e))
}
}
}
pub fn new(app_config: Config) -> Self {
Cache {
config: app_config.clone(),
}
}
}