use color_eyre::{ eyre::{bail, WrapErr}, Result, }; use rusqlite::{params, Connection}; use std::{ fs, io::ErrorKind, path::{Path, PathBuf}, }; pub trait Storage { fn prepare(&mut self) -> Result<()>; fn prepare_zoom(&mut self, zoom: u32) -> Result<()>; fn store(&mut self, zoom: u32, x: u64, y: u64, data: &[u8]) -> Result<()>; fn finish(&mut self) -> Result<()>; } #[derive(Debug)] pub struct Folder { base_dir: PathBuf, } impl Folder { 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(()) } } #[derive(Debug)] pub struct Sqlite { connection: Connection, } impl Sqlite { 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(()) } }