diff options
Diffstat (limited to 'hittekaart/src/storage.rs')
-rw-r--r-- | hittekaart/src/storage.rs | 165 |
1 files changed, 165 insertions, 0 deletions
diff --git a/hittekaart/src/storage.rs b/hittekaart/src/storage.rs new file mode 100644 index 0000000..c21627f --- /dev/null +++ b/hittekaart/src/storage.rs @@ -0,0 +1,165 @@ +//! 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 rusqlite::{params, Connection}; +use std::{ + fs, + io::ErrorKind, + path::{Path, PathBuf}, +}; + +use super::error::{Error, Result}; + +/// 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 => { + fs::create_dir(path) + .map_err(|e| Error::Io("creating the output directory", e))? + } + Err(e) => Err(Error::Io("checking the output direectory", e))?, + Ok(m) if m.is_dir() => (), + Ok(_) => Err(Error::InvalidOutputDirectory)?, + } + Ok(()) + } + + fn prepare_zoom(&mut self, zoom: u32) -> Result<()> { + let target = [&self.base_dir, &zoom.to_string().into()] + .iter() + .collect::<PathBuf>(); + fs::create_dir(target) + .map_err(|e| Error::Io("creating the zoom directory", e))?; + 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) + .map_err(|e| Error::Io("creating the output directory", e))?, + Ok(m) if !m.is_dir() => Err(Error::InvalidOutputDirectory)?, + _ => {} + } + let file = folder.join(format!("{y}.png")); + fs::write(file, data) + .map_err(|e| Error::Io("writing the output file", e))?; + 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<P: AsRef<Path>>(file: P) -> Result<Self> { + let path = file.as_ref(); + if fs::metadata(path).is_ok() { + return Err(Error::OutputAlreadyExists(path.to_path_buf())); + } + 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(()) + } +} |