Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion martin/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "martin"
version = "0.16.0"
version = "0.17.0"
authors = ["Stepan Kuzmin <to.stepan.kuzmin@gmail.com>", "Yuri Astrakhan <YuriAstrakhan@gmail.com>", "MapLibre contributors"]
description = "Blazing fast and lightweight tile server with PostGIS, MBTiles, and PMTiles support"
keywords = ["maps", "tiles", "mbtiles", "pmtiles", "postgis"]
Expand Down Expand Up @@ -111,6 +111,7 @@ tokio = { workspace = true, features = ["io-std"] }
tokio-postgres-rustls = { workspace = true, optional = true }
url.workspace = true
walkdir = { workspace = true, optional = true }
xxhash-rust.workspace = true

[build-dependencies]
walkdir = { workspace = true, optional = true }
Expand Down
2 changes: 1 addition & 1 deletion martin/benches/bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ impl Source for NullSource {
}

async fn process_tile(sources: &TileSources) {
let src = DynTileSource::new(sources, "null", Some(0), "", None, None, None).unwrap();
let src = DynTileSource::new(sources, "null", Some(0), "", None, None, None, None).unwrap();
src.get_http_response(TileCoord { z: 0, x: 0, y: 0 })
.await
.unwrap();
Expand Down
1 change: 1 addition & 0 deletions martin/src/bin/martin-cp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ async fn run_tile_copy(args: CopyArgs, state: ServerState) -> MartinCpResult<()>
Some(parse_encoding(args.encoding.as_str())?),
None,
None,
None,
)?;
// parallel async below uses move, so we must only use copyable types
let src = &src;
Expand Down
83 changes: 71 additions & 12 deletions martin/src/srv/tiles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ use actix_http::ContentEncoding;
use actix_http::header::Quality;
use actix_web::error::{ErrorBadRequest, ErrorNotAcceptable, ErrorNotFound};
use actix_web::http::header::{
AcceptEncoding, CONTENT_ENCODING, Encoding as HeaderEnc, Preference,
AcceptEncoding, CONTENT_ENCODING, ETAG, Encoding as HeaderEnc, EntityTag, IfNoneMatch,
Preference,
};
use actix_web::web::{Data, Path, Query};
use actix_web::{HttpMessage, HttpRequest, HttpResponse, Result as ActixResult, route};
Expand Down Expand Up @@ -49,6 +50,7 @@ async fn get_tile(
Some(path.z),
req.query_string(),
req.get_header::<AcceptEncoding>(),
req.get_header::<IfNoneMatch>(),
srv_config.preferred_encoding,
cache.as_ref().as_ref(),
)?;
Expand All @@ -67,17 +69,20 @@ pub struct DynTileSource<'a> {
pub query_str: Option<&'a str>,
pub query_obj: Option<UrlQuery>,
pub accept_enc: Option<AcceptEncoding>,
pub if_none_match: Option<IfNoneMatch>,
pub preferred_enc: Option<PreferredEncoding>,
pub cache: Option<&'a MainCache>,
}

impl<'a> DynTileSource<'a> {
#[expect(clippy::too_many_arguments)]
pub fn new(
sources: &'a TileSources,
source_ids: &str,
zoom: Option<u8>,
query: &'a str,
accept_enc: Option<AcceptEncoding>,
if_none_match: Option<IfNoneMatch>,
preferred_enc: Option<PreferredEncoding>,
cache: Option<&'a MainCache>,
) -> ActixResult<Self> {
Expand All @@ -100,24 +105,34 @@ impl<'a> DynTileSource<'a> {
query_str,
query_obj,
accept_enc,
if_none_match,
preferred_enc,
cache,
})
}

pub async fn get_http_response(&self, xyz: TileCoord) -> ActixResult<HttpResponse> {
let tile = self.get_tile_content(xyz).await?;

Ok(if tile.data.is_empty() {
HttpResponse::NoContent().finish()
} else {
let mut response = HttpResponse::Ok();
response.content_type(tile.info.format.content_type());
if let Some(val) = tile.info.encoding.content_encoding() {
response.insert_header((CONTENT_ENCODING, val));
if tile.data.is_empty() {
Comment thread
CommanderStorm marked this conversation as resolved.
return Ok(HttpResponse::NoContent().finish());
}
let hash = xxhash_rust::xxh3::xxh3_128(&tile.data);
let etag = EntityTag::new_strong(hash.to_string());
if let Some(IfNoneMatch::Items(expected_etags)) = &self.if_none_match {
for expected_etag in expected_etags {
if etag.strong_eq(expected_etag) {
return Ok(HttpResponse::NotModified().finish());
}
}
response.body(tile.data)
})
}

let mut response = HttpResponse::Ok();
response.content_type(tile.info.format.content_type());
response.insert_header((ETAG, etag));
if let Some(val) = tile.info.encoding.content_encoding() {
response.insert_header((CONTENT_ENCODING, val));
}
Ok(response.body(tile.data))
}

pub async fn get_tile_content(&self, xyz: TileCoord) -> ActixResult<Tile> {
Expand Down Expand Up @@ -294,6 +309,7 @@ pub fn to_encoding(val: ContentEncoding) -> Option<Encoding> {

#[cfg(test)]
mod tests {
use actix_http::header::TryIntoHeaderValue;
use rstest::rstest;
use tilejson::tilejson;

Expand Down Expand Up @@ -335,6 +351,7 @@ mod tests {
None,
"",
accept_enc,
None,
preferred_enc,
None,
)
Expand All @@ -345,6 +362,47 @@ mod tests {
assert_eq!(tile.info.encoding, expected_enc);
}

#[rstest]
#[case(200, None, Some(EntityTag::new_strong("229249875805521414007261281044017345339".to_string())))]
#[case(304, Some(IfNoneMatch::Items(vec![EntityTag::new_strong("229249875805521414007261281044017345339".to_string())])), None)]
#[case(200, Some(IfNoneMatch::Items(vec![EntityTag::new_strong("incorrect_etag".to_string())])), Some(EntityTag::new_strong("229249875805521414007261281044017345339".to_string())))]
#[actix_rt::test]
async fn test_etag(
#[case] expected_status: u16,
#[case] if_none_match: Option<IfNoneMatch>,
#[case] expected_etag: Option<EntityTag>,
) {
let source_id = "source1";
let source1 = TestSource {
id: source_id,
tj: tilejson! { tiles: vec![] },
data: vec![1_u8, 2, 3],
};
let sources = TileSources::new(vec![vec![Box::new(source1)]]);

let src = DynTileSource::new(
&sources,
source_id,
None,
"",
None,
if_none_match,
None,
None,
)
.unwrap();
let resp = &src
.get_http_response(TileCoord { z: 0, x: 0, y: 0 })
.await
.unwrap();
assert_eq!(resp.status().as_u16(), expected_status);
let etag = resp.headers().get(ETAG);
assert_eq!(
etag,
expected_etag.map(|e| e.try_into_value().unwrap()).as_ref()
);
}

#[actix_rt::test]
async fn test_tile_content() {
let non_empty_source = TestSource {
Expand Down Expand Up @@ -372,7 +430,8 @@ mod tests {
("empty,non-empty", vec![1_u8, 2, 3]),
("empty,non-empty,empty", vec![1_u8, 2, 3]),
] {
let src = DynTileSource::new(&sources, source_id, None, "", None, None, None).unwrap();
let src =
DynTileSource::new(&sources, source_id, None, "", None, None, None, None).unwrap();
let xyz = TileCoord { z: 0, x: 0, y: 0 };
assert_eq!(expected, &src.get_tile_content(xyz).await.unwrap().data);
}
Expand Down
Loading