tiles_proxy/
cache.rs

1//! Tile cache management.
2
3use rocket::fs::NamedFile;
4use rocket::http::Status;
5use rocket::request::Request;
6use rocket::response::{Responder, Response};
7use rocket::tokio::fs::create_dir_all;
8use rocket_etag_if_none_match::{EtagIfNoneMatch, entity_tag::EntityTag};
9use std::borrow::Cow;
10use std::fs;
11use std::fs::File;
12use std::io;
13use std::io::Error;
14use std::io::ErrorKind;
15use std::io::Write;
16use std::os::unix::fs::MetadataExt;
17use std::path::{Path, PathBuf};
18
19use crate::config::Config;
20use crate::tile_request::TileRequest;
21use crate::tile_request::WmtsRequest;
22use crate::tile_request::XyzRequest;
23
24/// CachedFile is a NamedFile with ETag added in HTTP headers
25///
26/// See <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag>
27pub struct CachedFile {
28    named_file: Option<NamedFile>,
29    etag_value: String,
30}
31
32impl CachedFile {
33    pub async fn from_path<P: AsRef<Path>>(
34        etag_if_none_match: EtagIfNoneMatch<'_>,
35        path: P,
36    ) -> Option<CachedFile> {
37        if path.as_ref().exists() && !path.as_ref().is_dir() {
38            let etag_value = CachedFile::etag_value(path.as_ref());
39            let et = unsafe { EntityTag::new_unchecked(false, Cow::Borrowed(&etag_value)) };
40            if etag_if_none_match.strong_eq(&et) {
41                return Some(CachedFile {
42                    etag_value,
43                    named_file: None,
44                });
45            }
46            if let Ok(f) = CachedFile::open(path).await {
47                return Some(f);
48            }
49        }
50        None
51    }
52    async fn open<P: AsRef<Path>>(path: P) -> io::Result<CachedFile> {
53        let etag_value = CachedFile::etag_value(path.as_ref());
54        let named_file = NamedFile::open(path).await?;
55        Ok(CachedFile {
56            named_file: Some(named_file),
57            etag_value,
58        })
59    }
60
61    pub fn etag_value<P: AsRef<Path>>(path: P) -> String {
62        let metadata = fs::metadata(path.as_ref()).unwrap();
63        let content_length = metadata.len();
64        let mtime = metadata.mtime();
65        format!(r#"{}-{}"#, mtime, content_length)
66    }
67}
68impl<'r> Responder<'r, 'static> for CachedFile {
69    fn respond_to(self, req: &Request) -> rocket::response::Result<'static> {
70        if let Some(f) = self.named_file {
71            return Response::build_from(f.respond_to(req)?)
72                .raw_header("Etag", self.etag_value)
73                .ok();
74        }
75        Err(Status::NotModified)
76    }
77}
78
79/// Cache management, a managed State for requests.
80pub struct Cache {
81    config: Config,
82}
83impl Cache {
84    async fn create_dir(&self, dirpath: PathBuf) -> Option<Error> {
85        if !dirpath.exists() {
86            if let Err(error) = create_dir_all(dirpath).await {
87                info!("Failed to create directory, error={}", error);
88                return Some(Error::new(ErrorKind::Other, "!"));
89            }
90        }
91        None
92    }
93    fn get_wmts_url_template(&self, alias: &str) -> Option<String> {
94        for conf in self.config.clone().wmts {
95            info!("conf.alias={}, alias={}", conf.alias, alias);
96            if conf.alias.eq(alias) {
97                return Some(conf.url);
98            }
99        }
100        info!("No template found for {}", alias);
101        None
102    }
103
104    fn get_xyz_url_template(&self, alias: &str) -> Option<String> {
105        for conf in self.config.clone().xyz {
106            info!("conf.alias={}, alias={}", conf.alias, alias);
107            if conf.alias.eq(alias) {
108                return Some(conf.url);
109            }
110        }
111        info!("No template found for {}", alias);
112        None
113    }
114
115    pub async fn get_or_download_wmts(
116        &self,
117        etag_if_none_match: EtagIfNoneMatch<'_>,
118        request: WmtsRequest,
119    ) -> std::io::Result<CachedFile> {
120        let path = request.filepath(self.config.directory.as_str());
121        if let Some(f) = CachedFile::from_path(etag_if_none_match, path.clone()).await {
122            return Ok(f);
123        }
124        let dirpath = request.dirpath(self.config.directory.as_str());
125        if let Some(error) = self.create_dir(dirpath).await {
126            return Err(error);
127        }
128        if let Some(url_template) = self.get_wmts_url_template(request.alias.as_str()) {
129            // TODO request.resolve_url(url_template)
130            let url = url_template
131                .replace("{layer}", request.layer.as_str())
132                .replace("{style}", request.style.as_str())
133                .replace("{tilematrixset}", request.tilematrixset.as_str())
134                .replace("{Service}", request.service.as_str())
135                .replace("{Request}", request.request.as_str())
136                .replace("{Version}", request.version.as_str())
137                .replace("{Format}", request.format.as_str())
138                .replace("{TileMatrix}", request.tile_matrix.as_str())
139                .replace("{TileCol}", request.tile_col.as_str())
140                .replace("{TileRow}", request.tile_row.as_str());
141            if let Some(filepath) = path.to_str() {
142                // download
143                match Self::download(url.as_str(), filepath).await {
144                    Ok(_) => return CachedFile::open(path).await,
145                    Err(error) => {
146                        info!("Failed to download or copy, error={}", error);
147                        return Err(Error::new(ErrorKind::NotFound, "!"));
148                    }
149                }
150            }
151        }
152        info!("No template found for {}", request.get_alias());
153        Err(Error::new(ErrorKind::InvalidInput, "!"))
154    }
155
156    pub async fn get_or_download_xyz(
157        &self,
158        etag_if_none_match: EtagIfNoneMatch<'_>,
159        request: XyzRequest,
160    ) -> std::io::Result<CachedFile> {
161        let path = request.filepath(self.config.directory.as_str());
162        if let Some(f) = CachedFile::from_path(etag_if_none_match, path.clone()).await {
163            return Ok(f);
164        }
165
166        if path.exists() && !path.is_dir() {
167            return CachedFile::open(path).await;
168        }
169        let dirpath = request.dirpath(self.config.directory.as_str());
170        if let Some(error) = self.create_dir(dirpath).await {
171            return Err(error);
172        }
173        if let Some(url_template) = self.get_xyz_url_template(request.alias.as_str()) {
174            // TODO request.resolve_url(url_template)
175            let url = url_template
176                .replace("{a}", request.a.as_str())
177                .replace("{x}", request.x.as_str())
178                .replace("{y}", request.y.as_str())
179                .replace("{z}", request.z.as_str());
180            if let Some(filepath) = path.to_str() {
181                // download
182                match Self::download(url.as_str(), filepath).await {
183                    Ok(_) => return CachedFile::open(path).await,
184                    Err(error) => {
185                        info!("Failed to download or copy, error={}", error);
186                        return Err(Error::new(ErrorKind::NotFound, "!"));
187                    }
188                }
189            }
190        }
191        info!("No template found for {}", request.alias);
192        Err(Error::new(ErrorKind::InvalidInput, "!"))
193    }
194
195    async fn download(url: &str, filepath: &str) -> Result<(), String> {
196        info!("download {}", url);
197        // Send an HTTP GET request to the URL
198        match reqwest::get(url).await {
199            Err(download_error) => Err(download_error.to_string()),
200            Ok(response) => {
201                // Create a new file to write the downloaded image to
202                let file_creation = File::create(filepath);
203                if file_creation.is_err() {
204                    return Err(format!(
205                        "Failed to create the file, error={}",
206                        file_creation.err().unwrap()
207                    ));
208                }
209                let mut file = file_creation.ok().unwrap();
210                // Copy the contents of the response to the file
211                let file_reading = response.bytes().await;
212                if file_reading.is_err() {
213                    return Err(format!(
214                        "Failed to read, error={}",
215                        file_reading.err().unwrap()
216                    ));
217                }
218                let content = file_reading.ok().unwrap();
219                info!("Writing {}", filepath);
220                file.write_all(&content)
221                    .map_err(|e| panic!("Failed to copy, error={}", e))
222            }
223        }
224    }
225    pub fn new(app_config: Config) -> Self {
226        Cache {
227            config: app_config.clone(),
228        }
229    }
230}