diff --git a/martin/src/bin/martin-cp.rs b/martin/src/bin/martin-cp.rs index 277d2896f..bfe93e680 100644 --- a/martin/src/bin/martin-cp.rs +++ b/martin/src/bin/martin-cp.rs @@ -19,7 +19,8 @@ use martin::{ use martin_tile_utils::{bbox_to_xyz, TileInfo}; use mbtiles::sqlx::SqliteConnection; use mbtiles::{ - init_mbtiles_schema, is_empty_database, CopyDuplicateMode, MbtType, MbtTypeCli, Mbtiles, + init_mbtiles_schema, is_empty_database, CopyDuplicateMode, MbtError, MbtType, MbtTypeCli, + Mbtiles, }; use tilejson::Bounds; use tokio::sync::mpsc::channel; @@ -73,9 +74,9 @@ pub struct CopyArgs { /// Use `identity` to disable compression. Ignored for non-encodable tiles like PNG and JPEG. #[arg(long, alias = "encodings", default_value = "gzip")] pub encoding: String, - /// Specify the behaviour when generated tile already exists in the destination file. - #[arg(long, value_enum, default_value_t = CopyDuplicateMode::default())] - pub on_duplicate: CopyDuplicateMode, + /// Allow copying to existing files, and indicate what to do if a tile with the same Z/X/Y already exists + #[arg(long, value_enum)] + pub on_duplicate: Option, /// Number of concurrent connections to use. #[arg(long, default_value = "1")] pub concurrency: Option, @@ -220,7 +221,7 @@ enum MartinCpError { #[error(transparent)] Actix(#[from] actix_web::Error), #[error(transparent)] - Mbt(#[from] mbtiles::MbtError), + Mbt(#[from] MbtError), } impl Display for Progress { @@ -273,6 +274,13 @@ async fn run_tile_copy(args: CopyArgs, state: ServerState) -> MartinCpResult<()> let tiles = compute_tile_ranges(&args); let mbt = Mbtiles::new(output_file)?; let mut conn = mbt.open_or_new().await?; + let on_duplicate = if let Some(on_duplicate) = args.on_duplicate { + on_duplicate + } else if !is_empty_database(&mut conn).await? { + return Err(MbtError::DestinationFileExists(output_file.clone()).into()); + } else { + CopyDuplicateMode::Override + }; let mbt_type = init_schema(&mbt, &mut conn, sources, tile_info, args.mbt_type).await?; let query = args.url_query.as_deref(); let req = TestRequest::default() @@ -317,7 +325,7 @@ async fn run_tile_copy(args: CopyArgs, state: ServerState) -> MartinCpResult<()> } else { batch.push((tile.xyz.z, tile.xyz.x, tile.xyz.y, tile.data)); if batch.len() >= BATCH_SIZE || last_saved.elapsed() > SAVE_EVERY { - mbt.insert_tiles(&mut conn, mbt_type, args.on_duplicate, &batch) + mbt.insert_tiles(&mut conn, mbt_type, on_duplicate, &batch) .await?; batch.clear(); last_saved = Instant::now(); @@ -332,7 +340,7 @@ async fn run_tile_copy(args: CopyArgs, state: ServerState) -> MartinCpResult<()> } } if !batch.is_empty() { - mbt.insert_tiles(&mut conn, mbt_type, args.on_duplicate, &batch) + mbt.insert_tiles(&mut conn, mbt_type, on_duplicate, &batch) .await?; } Ok(()) diff --git a/mbtiles/src/bin/mbtiles.rs b/mbtiles/src/bin/mbtiles.rs index f04e857ec..75e6f5eba 100644 --- a/mbtiles/src/bin/mbtiles.rs +++ b/mbtiles/src/bin/mbtiles.rs @@ -312,7 +312,7 @@ mod tests { #[test] fn test_copy_diff_with_override_copy_duplicate_mode() { let mut opt = MbtilesCopier::new(PathBuf::from("src_file"), PathBuf::from("dst_file")); - opt.on_duplicate = CopyDuplicateMode::Override; + opt.on_duplicate = Some(CopyDuplicateMode::Override); assert_eq!( Args::parse_from([ "mbtiles", @@ -332,7 +332,7 @@ mod tests { #[test] fn test_copy_diff_with_ignore_copy_duplicate_mode() { let mut opt = MbtilesCopier::new(PathBuf::from("src_file"), PathBuf::from("dst_file")); - opt.on_duplicate = CopyDuplicateMode::Ignore; + opt.on_duplicate = Some(CopyDuplicateMode::Ignore); assert_eq!( Args::parse_from([ "mbtiles", @@ -352,7 +352,7 @@ mod tests { #[test] fn test_copy_diff_with_abort_copy_duplicate_mode() { let mut opt = MbtilesCopier::new(PathBuf::from("src_file"), PathBuf::from("dst_file")); - opt.on_duplicate = CopyDuplicateMode::Abort; + opt.on_duplicate = Some(CopyDuplicateMode::Abort); assert_eq!( Args::parse_from([ "mbtiles", diff --git a/mbtiles/src/copier.rs b/mbtiles/src/copier.rs index 85bd3fd2a..7c4ba4cfa 100644 --- a/mbtiles/src/copier.rs +++ b/mbtiles/src/copier.rs @@ -19,11 +19,10 @@ use crate::{ AGG_TILES_HASH_IN_DIFF, }; -#[derive(PartialEq, Eq, Default, Debug, Clone, Copy, EnumDisplay, Serialize, Deserialize)] +#[derive(PartialEq, Eq, Debug, Clone, Copy, EnumDisplay, Serialize, Deserialize)] #[enum_display(case = "Kebab")] #[cfg_attr(feature = "cli", derive(ValueEnum))] pub enum CopyDuplicateMode { - #[default] Override, Ignore, Abort, @@ -62,9 +61,9 @@ pub struct MbtilesCopier { /// Destination type with options #[cfg_attr(feature = "cli", arg(skip))] pub dst_type: Option, - /// Specify copying behaviour when tiles with duplicate (zoom_level, tile_column, tile_row) values are found - #[cfg_attr(feature = "cli", arg(long, value_enum, default_value_t = CopyDuplicateMode::default()))] - pub on_duplicate: CopyDuplicateMode, + /// Allow copying to existing files, and indicate what to do if a tile with the same Z/X/Y already exists + #[cfg_attr(feature = "cli", arg(long, value_enum))] + pub on_duplicate: Option, /// Minimum zoom level to copy #[cfg_attr(feature = "cli", arg(long, conflicts_with("zoom_levels")))] pub min_zoom: Option, @@ -102,7 +101,7 @@ impl MbtilesCopier { dst_file: dst_filepath, dst_type_cli: None, dst_type: None, - on_duplicate: CopyDuplicateMode::Override, + on_duplicate: None, min_zoom: None, max_zoom: None, zoom_levels: Vec::default(), @@ -172,6 +171,14 @@ impl MbtileCopierInt { let src_type = src_mbt.open_and_detect_type().await?; let mut conn = dst_mbt.open_or_new().await?; let is_empty_db = is_empty_database(&mut conn).await?; + let on_duplicate = if let Some(on_duplicate) = self.options.on_duplicate { + on_duplicate + } else if !is_empty_database(&mut conn).await? { + return Err(MbtError::DestinationFileExists(self.options.dst_file)); + } else { + CopyDuplicateMode::Override + }; + src_mbt.attach_to(&mut conn, "sourceDb").await?; let dst_type: MbtType; @@ -211,8 +218,8 @@ impl MbtileCopierInt { let (where_clause, query_args) = self.get_where_clause(); let select_from = format!("{select_from} {where_clause}"); - let on_dupl = self.options.on_duplicate.to_sql(); - let sql_cond = self.get_on_duplicate_sql_cond(dst_type); + let on_dupl = on_duplicate.to_sql(); + let sql_cond = Self::get_on_duplicate_sql_cond(on_duplicate, dst_type); debug!("Copying tiles with 'INSERT {on_dupl}' {src_type} -> {dst_type} ({sql_cond})"); @@ -417,8 +424,8 @@ impl MbtileCopierInt { } /// Returns WHERE condition SQL depending on the override and destination type - fn get_on_duplicate_sql_cond(&self, dst_type: MbtType) -> String { - match &self.options.on_duplicate { + fn get_on_duplicate_sql_cond(on_duplicate: CopyDuplicateMode, dst_type: MbtType) -> String { + match on_duplicate { CopyDuplicateMode::Ignore | CopyDuplicateMode::Override => String::new(), CopyDuplicateMode::Abort => { let (main_table, tile_identifier) = match dst_type { @@ -820,7 +827,7 @@ mod tests { let dst = PathBuf::from("../tests/fixtures/mbtiles/world_cities.mbtiles"); let mut opt = MbtilesCopier::new(src.clone(), dst.clone()); - opt.on_duplicate = CopyDuplicateMode::Abort; + opt.on_duplicate = Some(CopyDuplicateMode::Abort); assert!(matches!( opt.run().await.unwrap_err(), @@ -841,9 +848,9 @@ mod tests { .run() .await?; - let mut dst_conn = MbtilesCopier::new(src_file.clone(), dst.clone()) - .run() - .await?; + let mut opt = MbtilesCopier::new(src_file.clone(), dst.clone()); + opt.on_duplicate = Some(CopyDuplicateMode::Override); + let mut dst_conn = opt.run().await?; // Verify the tiles in the destination file is a superset of the tiles in the source file Mbtiles::new(src_file)? @@ -871,7 +878,7 @@ mod tests { .await?; let mut opt = MbtilesCopier::new(src_file.clone(), dst.clone()); - opt.on_duplicate = CopyDuplicateMode::Ignore; + opt.on_duplicate = Some(CopyDuplicateMode::Ignore); let mut dst_conn = opt.run().await?; // Verify the tiles in the destination file are the same as those in the source file except for those with duplicate (zoom_level, tile_column, tile_row) diff --git a/mbtiles/src/errors.rs b/mbtiles/src/errors.rs index 543de62c8..fbcc09dd1 100644 --- a/mbtiles/src/errors.rs +++ b/mbtiles/src/errors.rs @@ -71,6 +71,9 @@ pub enum MbtError { #[error("The MBTiles file {0} has data of type {1}, but the desired type was set to {2}")] MismatchedTargetType(PathBuf, MbtType, MbtType), + + #[error("Unless --on-duplicate (override|ignore|abort) is set, writing tiles to an existing non-empty MBTiles file is disabled. Either set --on-duplicate flag, or delete {}", .0.display())] + DestinationFileExists(PathBuf), } pub type MbtResult = Result;