tiles_proxy/
main.rs

1//! TilesProxy: a cache proxy for tiles.
2//!
3//! Supported standards for tiles:
4//! - [OGC](https://www.ogc.org/) WMTS: [OpenGIS Web Map Tile Service](https://www.ogc.org/standard/wmts/)
5//! - XYZ (aka Slippy Map Tilenames): the de facto OpenStreetMap standard
6//!
7//! Configuration of origin tiles servers is done in a TOML file.
8//!
9//! To enable browser caching, HTTP headers are added: [ETag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) and [Cache-Control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control).
10//! [CORS headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) are also added to enable canvas export (eg.: in OpenLayers) from browser.
11
12#[macro_use]
13extern crate rocket;
14
15mod cache;
16mod config;
17mod http_headers;
18#[cfg(test)]
19mod tests;
20mod tile_request;
21
22use cache::Cache;
23use cache::CachedFile;
24use config::Config;
25use figment::Figment;
26use rocket::Build;
27use rocket::Request;
28use rocket::Rocket;
29use rocket::State;
30use rocket_etag_if_none_match::EtagIfNoneMatch;
31use std::env;
32use tile_request::WmtsRequest;
33use tile_request::XyzRequest;
34
35/// Catcher for 404 errors.
36#[catch(404)]
37fn general_not_found(req: &Request) -> String {
38    format!("Sorry, '{}' is not a valid path.", req.uri())
39}
40
41/// URL for WMTS tile.
42#[get("/wmts/<alias>?<request..>")]
43pub async fn wmts_tile(
44    app_cache: &State<Cache>,
45    etag_if_none_match: EtagIfNoneMatch<'_>,
46    alias: &str,
47    request: WmtsRequest,
48) -> Option<CachedFile> {
49    let req = request.with_alias(alias);
50    app_cache
51        .get_or_download_wmts(etag_if_none_match, req)
52        .await
53        .ok()
54}
55
56/// Catcher for 404 errors about WMTS tile.
57#[catch(404)]
58fn wmts_tile_not_found(req: &Request) -> String {
59    format!("Sorry, the WMTS tile for '{}' was not found.", req.uri())
60}
61
62/// URL for XYZ tile.
63#[get("/xyz/<alias>/<a>/<x>/<y>/<z>")]
64pub async fn xyz_tile<'a>(
65    app_cache: &State<Cache>,
66    etag_if_none_match: EtagIfNoneMatch<'_>,
67    alias: &str,
68    a: &str,
69    x: &str,
70    y: &str,
71    z: &str,
72) -> Option<CachedFile> {
73    let request = XyzRequest::new(alias, a, x, y, z);
74    app_cache
75        .get_or_download_xyz(etag_if_none_match, request)
76        .await
77        .ok()
78}
79
80/// Catcher for 404 errors about XYZ tile.
81#[catch(404)]
82fn xyz_tile_not_found(req: &Request) -> String {
83    format!("Sorry, the XYZ tile for '{}' was not found.", req.uri())
84}
85
86/// Root url.
87#[get("/")]
88fn index() -> &'static str {
89    "Hello, there is nothing here! Read the fully described manual ;)"
90}
91
92/// Rocket launch.
93#[launch]
94fn rocket() -> _ {
95    match run(env::args().collect()) {
96        Ok(r) => r,
97        Err(e) => panic!("Error while launching: {}", e),
98    }
99}
100
101/// Run function, also used by tests.
102fn run(args: Vec<String>) -> Result<Rocket<Build>, String> {
103    if let Some(filepath) = args.get(1) {
104        let app_config: Config = Config::from_file(filepath);
105        let cache: Cache = Cache::new(app_config.clone());
106        dbg!(app_config.clone());
107        let new_figment = Figment::new()
108            .join(("ident", false)) // remove HTTP header "Server"
109            .join(("port", app_config.port));
110        let figment = rocket::Config::figment().merge(new_figment);
111
112        Ok(rocket::custom(figment)
113            .attach(http_headers::CacheControl)
114            .attach(http_headers::Cors)
115            .manage(cache)
116            .mount("/", routes![index, wmts_tile, xyz_tile])
117            .register("/", catchers![general_not_found])
118            .register("/wmts", catchers![wmts_tile_not_found])
119            .register("/xyz", catchers![xyz_tile_not_found]))
120    } else {
121        Err("No configuration!".to_string())
122    }
123}