//! Abstractions over different storage backends. //! //! The main trait to use here is [`Storage`], which provides the necessary interface to store //! tiles. Usually you want to have a `dyn Storage`, and then instantiate it with a concrete //! implementation (either [`Folder`] or [`Sqlite`]), depending on the command line flags or //! similar. use color_eyre::{ eyre::{bail, WrapErr}, Result, }; use rusqlite::{params, Connection}; use std::{ fs, io::ErrorKind, path::{Path, PathBuf}, }; /// The trait that provides the interface for storing tiles. pub trait Storage { /// Prepare the storage. /// /// This can be used to e.g. ensure the directory exists, or to create the database. fn prepare(&mut self) -> Result<()>; /// Prepare for a given zoom level. /// /// This function is called once per zoom, and can be used e.g. to set up the inner folder for /// the level. This can avoid unnecesary syscalls if this setup would be done in /// [`Storage::store`] instead. fn prepare_zoom(&mut self, zoom: u32) -> Result<()>; /// Store the given data for the tile. fn store(&mut self, zoom: u32, x: u64, y: u64, data: &[u8]) -> Result<()>; /// Finish the storing operation. /// /// This can flush any buffers, commit database changes, and so on. fn finish(&mut self) -> Result<()>; } /// Folder-based storage. /// /// This stores the tiles according to the [slippy map /// tilenames](https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames). #[derive(Debug)] pub struct Folder { base_dir: PathBuf, } impl Folder { /// Create a new folder based storage. /// /// The given directory is the "root" directory, so a tile would be saved as /// `base_dir/{zoom}/{x}/{y}.png`. pub fn new(base_dir: PathBuf) -> Self { Folder { base_dir } } } impl Storage for Folder { fn prepare(&mut self) -> Result<()> { let path = &self.base_dir; let metadata = fs::metadata(path); match metadata { Err(e) if e.kind() == ErrorKind::NotFound => { let parent = path.parent().unwrap_or_else(|| Path::new("/")); fs::create_dir(path) .context(format!("Could not create output directory at {parent:?}"))? } Err(e) => Err(e).context("Error while checking output directory")?, Ok(m) if m.is_dir() => (), Ok(_) => bail!("Output directory is not a directory"), } Ok(()) } fn prepare_zoom(&mut self, zoom: u32) -> Result<()> { let target = [&self.base_dir, &zoom.to_string().into()] .iter() .collect::(); fs::create_dir(target)?; Ok(()) } fn store(&mut self, zoom: u32, x: u64, y: u64, data: &[u8]) -> Result<()> { let folder = self.base_dir.join(zoom.to_string()).join(x.to_string()); let metadata = folder.metadata(); match metadata { Err(_) => fs::create_dir(&folder)?, Ok(m) if !m.is_dir() => bail!("Output path is not a directory"), _ => {} } let file = folder.join(format!("{y}.png")); fs::write(file, data)?; Ok(()) } fn finish(&mut self) -> Result<()> { Ok(()) } } /// SQLite based storage. /// /// This stores tiles in a SQLite database. The database will have a single table: /// /// ```sql /// CREATE TABLE tiles ( /// zoom INTEGER, /// x INTEGER, /// y INTEGER, /// data BLOB, /// PRIMARY KEY (zoom, x, y) /// ); /// ``` #[derive(Debug)] pub struct Sqlite { connection: Connection, } impl Sqlite { /// Create a new SQLite backed tile store. /// /// The database will be saved at the given location. Note that the database must not yet /// exist. pub fn connect>(file: P) -> Result { let path = file.as_ref(); if fs::metadata(path).is_ok() { bail!("Path {path:?} already exists, refusing to open") } let connection = Connection::open(path)?; Ok(Sqlite { connection }) } } impl Storage for Sqlite { fn prepare(&mut self) -> Result<()> { self.connection.execute( "CREATE TABLE tiles ( zoom INTEGER, x INTEGER, y INTEGER, data BLOB, PRIMARY KEY (zoom, x, y) );", (), )?; self.connection.execute("BEGIN;", ())?; Ok(()) } fn prepare_zoom(&mut self, _zoom: u32) -> Result<()> { Ok(()) } fn store(&mut self, zoom: u32, x: u64, y: u64, data: &[u8]) -> Result<()> { self.connection.execute( "INSERT INTO tiles (zoom, x, y, data) VALUES (?, ?, ?, ?)", params![zoom, x, y, data], )?; Ok(()) } fn finish(&mut self) -> Result<()> { self.connection.execute("COMMIT;", ())?; Ok(()) } }