diff --git a/src/asynchronous.rs b/src/asynchronous.rs index f52913e..a57ce94 100644 --- a/src/asynchronous.rs +++ b/src/asynchronous.rs @@ -1,5 +1,5 @@ use std::convert::TryInto; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::process::Stdio; use std::sync::{Arc, OnceLock}; @@ -115,6 +115,7 @@ pub(crate) async fn exec_create_db( port: u32, owner: &str, dbname: &str, + template_db: Option<&str>, ) -> TmpPostgrustResult<()> { let mut command = Command::new(createdb_bin); command @@ -126,8 +127,11 @@ pub(crate) async fn exec_create_db( .arg("postgres") .arg("-O") .arg(owner) - .arg("--echo") - .arg(dbname); + .arg("--echo"); + if let Some(template_db) = template_db { + command.arg("-T").arg(template_db); + } + command.arg(dbname); cmd_as_non_root(&mut command); exec_process(&mut command, TmpPostgrustError::CreateDBFailed).await } @@ -199,6 +203,7 @@ pub struct ProcessGuard { pub(crate) _cache_directory: Arc, /// Socket directory for connection to the running process. pub(crate) socket_dir: Arc, + pub(crate) createdb_bin: PathBuf, pub(crate) postgres_process: Child, // Limit the total concurrent processes. pub(crate) _process_permit: SemaphorePermit<'static>, @@ -224,6 +229,16 @@ impl ProcessGuard { /// Panics if a string file path cannot be obtained from the socket directory. #[must_use] pub fn connection_string(&self) -> String { + self.connection_string_for_db(&self.db_name) + } + + /// Get a Postgresql format connection string for a database on this running instance. + /// + /// # Panics + /// + /// Panics if a string file path cannot be obtained from the socket directory. + #[must_use] + pub fn connection_string_for_db(&self, db_name: &str) -> String { format!( "postgresql:///?host={}&port={}&dbname={}&user={}", self.socket_dir @@ -231,10 +246,30 @@ impl ProcessGuard { .to_str() .expect("Failed to convert socket directory to a path"), self.port, - self.db_name, + db_name, self.user_name, ) } + + /// Clone the current database in this running instance. + /// + /// The returned connection string points at the newly-created database. + /// + /// # Errors + /// + /// Returns an error if `createdb` fails to clone the current database. + pub async fn clone_database(&self, db_name: &str) -> TmpPostgrustResult { + exec_create_db( + &self.createdb_bin, + self.socket_dir.path(), + self.port, + &self.user_name, + db_name, + Some(&self.db_name), + ) + .await?; + Ok(self.connection_string_for_db(db_name)) + } } fn cmd_as_non_root(command: &mut Command) { diff --git a/src/lib.rs b/src/lib.rs index 5d0cb8b..d616ed6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -303,6 +303,7 @@ impl TmpPostgrustFactory { process.port, &process.user_name, &process.db_name, + None, )?; Ok(factory) @@ -352,6 +353,7 @@ impl TmpPostgrustFactory { process.port, &process.user_name, &process.db_name, + None, ) .await?; @@ -467,6 +469,7 @@ impl TmpPostgrustFactory { _data_directory: Arc::clone(dir), _cache_directory: Arc::clone(&self.cache_dir), socket_dir: Arc::clone(&self.socket_dir), + createdb_bin: self.binaries.createdb.clone(), }) } @@ -548,6 +551,7 @@ impl TmpPostgrustFactory { _data_directory: Arc::clone(dir), _cache_directory: Arc::clone(&self.cache_dir), socket_dir: Arc::clone(&self.socket_dir), + createdb_bin: self.binaries.createdb.clone(), postgres_process: postgres_process_handle, _process_permit: permit, }) diff --git a/src/synchronous.rs b/src/synchronous.rs index 81d5499..f76571b 100644 --- a/src/synchronous.rs +++ b/src/synchronous.rs @@ -3,7 +3,7 @@ use std::fs::create_dir_all; use std::io::BufReader; use std::io::Lines; use std::os::unix::process::CommandExt; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::process::Child; use std::process::ChildStderr; use std::process::ChildStdout; @@ -125,6 +125,7 @@ pub(crate) fn exec_create_db( port: u32, owner: &str, dbname: &str, + template_db: Option<&str>, ) -> TmpPostgrustResult<()> { let mut command = Command::new(createdb_bin); command @@ -136,8 +137,11 @@ pub(crate) fn exec_create_db( .arg("postgres") .arg("-O") .arg(owner) - .arg("--echo") - .arg(dbname); + .arg("--echo"); + if let Some(template_db) = template_db { + command.arg("-T").arg(template_db); + } + command.arg(dbname); cmd_as_non_root(&mut command); exec_process(&mut command, TmpPostgrustError::CreateDBFailed) } @@ -192,6 +196,7 @@ pub struct ProcessGuard { pub(crate) _cache_directory: Arc, /// Socket directory for connection to the running process. pub(crate) socket_dir: Arc, + pub(crate) createdb_bin: PathBuf, } impl ProcessGuard { @@ -202,6 +207,16 @@ impl ProcessGuard { /// Panics if a string file path cannot be obtained from the socket directory. #[must_use] pub fn connection_string(&self) -> String { + self.connection_string_for_db(&self.db_name) + } + + /// Get a Postgresql format connection string for a database on this running instance. + /// + /// # Panics + /// + /// Panics if a string file path cannot be obtained from the socket directory. + #[must_use] + pub fn connection_string_for_db(&self, db_name: &str) -> String { format!( "postgresql:///?host={}&port={}&dbname={}&user={}", self.socket_dir @@ -209,10 +224,29 @@ impl ProcessGuard { .to_str() .expect("Failed to convert socket directory to a path"), self.port, - self.db_name, + db_name, self.user_name, ) } + + /// Clone the current database in this running instance. + /// + /// The returned connection string points at the newly-created database. + /// + /// # Errors + /// + /// Returns an error if `createdb` fails to clone the current database. + pub fn clone_database(&self, db_name: &str) -> TmpPostgrustResult { + exec_create_db( + &self.createdb_bin, + self.socket_dir.path(), + self.port, + &self.user_name, + db_name, + Some(&self.db_name), + )?; + Ok(self.connection_string_for_db(db_name)) + } } /// Signal that the process needs to end.