use std::{
fs::File,
io::Write,
path::{Path, PathBuf},
sync::Arc,
};
use anyhow::Result;
use async_trait::async_trait;
use futures::lock::Mutex;
use tar::{Builder, Header};
use versatiles_core::{compression::compress, io::DataWriterTrait};
use versatiles_derive::context;
use crate::{TileSource, TileSourceTraverseExt, TilesRuntime, TilesWriter, Traversal};
struct DataWriterAdapter<'a>(&'a mut dyn DataWriterTrait);
impl Write for DataWriterAdapter<'_> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let blob = versatiles_core::Blob::from(buf.to_vec());
self
.0
.append(&blob)
.and_then(|range| usize::try_from(range.length).map_err(Into::into))
.map_err(std::io::Error::other)
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
pub struct TarTilesWriter {}
impl TarTilesWriter {
async fn write_tar<W: Write + Send>(reader: &mut dyn TileSource, sink: W, runtime: TilesRuntime) -> Result<()> {
let mut builder = Builder::new(sink);
let parameters = reader.metadata();
let tile_format = *parameters.tile_format();
let tile_compression = *reader.metadata().tile_compression();
let extension_format = tile_format.as_extension();
let extension_compression = tile_compression.as_extension();
let meta_data = compress(reader.tilejson().into(), &tile_compression)?;
let filename = format!("tiles.json{extension_compression}");
let mut header = Header::new_gnu();
header.set_size(meta_data.len() as u64);
header.set_mode(0o644);
builder.append_data(&mut header, Path::new(&filename), meta_data.as_slice())?;
let builder_mutex = Arc::new(Mutex::new(builder));
reader
.traverse_all_tiles(
&Traversal::ANY,
|_bbox, mut stream| {
let builder_mutex = Arc::clone(&builder_mutex);
Box::pin(async move {
let mut builder = builder_mutex.lock().await;
while let Some((coord, tile)) = stream.next().await {
let filename = format!(
"./{}/{}/{}{}{}",
coord.level, coord.x, coord.y, extension_format, extension_compression
);
let path = PathBuf::from(&filename);
let blob = tile.into_blob(&tile_compression)?;
let mut header = Header::new_gnu();
header.set_size(blob.len());
header.set_mode(0o644);
builder.append_data(&mut header, path, blob.as_slice())?;
}
Ok(())
})
},
runtime.clone(),
)
.await?;
builder_mutex.lock().await.finish()?;
Ok(())
}
}
#[async_trait]
impl TilesWriter for TarTilesWriter {
#[context("writing tar to path '{}'", path.display())]
async fn write_to_path(reader: &mut dyn TileSource, path: &Path, runtime: TilesRuntime) -> Result<()> {
let file = File::create(path)?;
Self::write_tar(reader, file, runtime).await
}
#[context("writing tar to DataWriter")]
async fn write_to_writer(
reader: &mut dyn TileSource,
writer: &mut dyn DataWriterTrait,
runtime: TilesRuntime,
) -> Result<()> {
let adapter = DataWriterAdapter(writer);
Self::write_tar(reader, adapter, runtime).await
}
}
#[cfg(test)]
mod tests {
use assert_fs::NamedTempFile;
use versatiles_core::*;
use super::*;
use crate::{MockReader, MockWriter, TarTilesReader, TileSourceMetadata};
#[tokio::test]
async fn read_write() -> Result<()> {
let mut mock_reader = MockReader::new_mock(
TilePyramid::new_full_up_to(4),
TileSourceMetadata::new(TileFormat::MVT, TileCompression::Gzip, Traversal::ANY, None),
)?;
let temp_path = NamedTempFile::new("test_output.tar")?;
TarTilesWriter::write_to_path(&mut mock_reader, &temp_path, TilesRuntime::default()).await?;
let mut reader = TarTilesReader::open(&temp_path)?;
MockWriter::write(&mut reader).await?;
Ok(())
}
#[tokio::test]
async fn test_meta_data() -> Result<()> {
let mut mock_reader = MockReader::new_mock(
TilePyramid::new_full_up_to(1),
TileSourceMetadata::new(TileFormat::JSON, TileCompression::Uncompressed, Traversal::ANY, None),
)?;
let temp_path = NamedTempFile::new("test_meta_output.tar")?;
TarTilesWriter::write_to_path(&mut mock_reader, &temp_path, TilesRuntime::default()).await?;
let reader = TarTilesReader::open(&temp_path)?;
assert_eq!(
reader.tilejson().stringify(),
"{\"tilejson\":\"3.0.0\",\"type\":\"dummy\"}"
);
Ok(())
}
#[tokio::test]
async fn test_empty_tiles() -> Result<()> {
let mut mock_reader = MockReader::new_mock(
TilePyramid::new_empty(),
TileSourceMetadata::new(TileFormat::JSON, TileCompression::Uncompressed, Traversal::ANY, None),
)?;
let temp_path = NamedTempFile::new("test_empty_tiles.tar")?;
TarTilesWriter::write_to_path(&mut mock_reader, &temp_path, TilesRuntime::default()).await?;
assert_eq!(
TarTilesReader::open(&temp_path)
.unwrap_err()
.chain()
.last()
.unwrap()
.to_string(),
"no tiles found in tar"
);
Ok(())
}
#[tokio::test]
async fn test_invalid_path() -> Result<()> {
let mut mock_reader = MockReader::new_mock(
TilePyramid::new_full_up_to(2),
TileSourceMetadata::new(TileFormat::MVT, TileCompression::Gzip, Traversal::ANY, None),
)?;
let invalid_path = Path::new("/invalid/path/output.tar");
let result = TarTilesWriter::write_to_path(&mut mock_reader, invalid_path, TilesRuntime::default()).await;
assert!(result.is_err());
Ok(())
}
#[tokio::test]
async fn test_large_tile_set() -> Result<()> {
let mut mock_reader = MockReader::new_mock(
TilePyramid::new_full_up_to(7),
TileSourceMetadata::new(TileFormat::PNG, TileCompression::Uncompressed, Traversal::ANY, None),
)?;
let temp_path = NamedTempFile::new("test_large_tiles.tar")?;
TarTilesWriter::write_to_path(&mut mock_reader, &temp_path, TilesRuntime::default()).await?;
let reader = TarTilesReader::open(&temp_path)?;
assert_eq!(reader.tile_pyramid().await?.count_tiles(), 21845);
Ok(())
}
#[tokio::test]
async fn test_different_compressions() -> Result<()> {
let compressions = vec![
TileCompression::Uncompressed,
TileCompression::Gzip,
TileCompression::Brotli,
];
for tile_compression in compressions {
let mut mock_reader = MockReader::new_mock(
TilePyramid::new_full_up_to(2),
TileSourceMetadata::new(TileFormat::MVT, tile_compression, Traversal::ANY, None),
)?;
let temp_path = NamedTempFile::new(format!("test_compression_{tile_compression:?}.tar"))?;
TarTilesWriter::write_to_path(&mut mock_reader, &temp_path, TilesRuntime::default()).await?;
let reader = TarTilesReader::open(&temp_path)?;
assert_eq!(reader.metadata().tile_compression(), &tile_compression);
}
Ok(())
}
#[tokio::test]
async fn test_correct_zxy_scheme() -> Result<()> {
let mut tile_pyramid = TilePyramid::new_empty();
tile_pyramid.insert_coord(&TileCoord::new(3, 1, 2)?);
let mut mock_reader = MockReader::new_mock(
tile_pyramid,
TileSourceMetadata::new(TileFormat::PNG, TileCompression::Uncompressed, Traversal::ANY, None),
)?;
let temp_path = NamedTempFile::new("test_zxy_scheme.tar")?;
TarTilesWriter::write_to_path(&mut mock_reader, &temp_path, TilesRuntime::default()).await?;
let mut filenames = tar::Archive::new(File::open(&temp_path)?)
.entries()?
.map(|entry| entry.unwrap().path().unwrap().to_str().unwrap().to_string())
.collect::<Vec<_>>();
filenames.sort();
assert_eq!(filenames, vec!["3/1/2.png", "tiles.json"]);
Ok(())
}
}