Skip to main content

postgresql_commands/
pg_basebackup.rs

1use crate::Settings;
2use crate::traits::CommandBuilder;
3use std::convert::AsRef;
4use std::ffi::{OsStr, OsString};
5use std::path::PathBuf;
6
7/// `pg_basebackup` takes a base backup of a running `PostgreSQL` server.
8#[derive(Clone, Debug, Default)]
9pub struct PgBaseBackupBuilder {
10    program_dir: Option<PathBuf>,
11    envs: Vec<(OsString, OsString)>,
12    pgdata: Option<PathBuf>,
13    format: Option<OsString>,
14    max_rate: Option<OsString>,
15    write_recovery_conf: bool,
16    target: Option<OsString>,
17    tablespace_mapping: Option<OsString>,
18    waldir: Option<OsString>,
19    wal_method: Option<OsString>,
20    gzip: bool,
21    compress: Option<OsString>,
22    checkpoint: Option<OsString>,
23    create_slot: bool,
24    label: Option<OsString>,
25    no_clean: bool,
26    no_sync: bool,
27    progress: bool,
28    slot: Option<OsString>,
29    verbose: bool,
30    version: bool,
31    manifest_checksums: Option<OsString>,
32    manifest_force_encode: bool,
33    no_estimate_size: bool,
34    no_manifest: bool,
35    no_slot: bool,
36    no_verify_checksums: bool,
37    help: bool,
38    dbname: Option<OsString>,
39    host: Option<OsString>,
40    port: Option<u16>,
41    status_interval: Option<OsString>,
42    username: Option<OsString>,
43    no_password: bool,
44    password: bool,
45    pg_password: Option<OsString>,
46}
47
48impl PgBaseBackupBuilder {
49    /// Create a new [`PgBaseBackupBuilder`]
50    #[must_use]
51    pub fn new() -> Self {
52        Self::default()
53    }
54
55    /// Create a new [`PgBaseBackupBuilder`] from [Settings]
56    pub fn from(settings: &dyn Settings) -> Self {
57        let mut builder = Self::new()
58            .program_dir(settings.get_binary_dir())
59            .host(settings.get_host())
60            .port(settings.get_port())
61            .username(settings.get_username())
62            .pg_password(settings.get_password());
63        if let Some(socket_dir) = settings.get_socket_dir() {
64            builder = builder.host(socket_dir.to_string_lossy().to_string());
65        }
66        builder
67    }
68
69    /// Location of the program binary
70    #[must_use]
71    pub fn program_dir<P: Into<PathBuf>>(mut self, path: P) -> Self {
72        self.program_dir = Some(path.into());
73        self
74    }
75
76    /// receive base backup into directory
77    #[must_use]
78    pub fn pgdata<P: Into<PathBuf>>(mut self, pgdata: P) -> Self {
79        self.pgdata = Some(pgdata.into());
80        self
81    }
82
83    /// output format (plain (default), tar)
84    #[must_use]
85    pub fn format<S: AsRef<OsStr>>(mut self, format: S) -> Self {
86        self.format = Some(format.as_ref().to_os_string());
87        self
88    }
89
90    /// maximum transfer rate to transfer data directory (in kB/s, or use suffix "k" or "M")
91    #[must_use]
92    pub fn max_rate<S: AsRef<OsStr>>(mut self, max_rate: S) -> Self {
93        self.max_rate = Some(max_rate.as_ref().to_os_string());
94        self
95    }
96
97    /// write configuration for replication
98    #[must_use]
99    pub fn write_recovery_conf(mut self) -> Self {
100        self.write_recovery_conf = true;
101        self
102    }
103
104    /// backup target (if other than client)
105    #[must_use]
106    pub fn target<S: AsRef<OsStr>>(mut self, target: S) -> Self {
107        self.target = Some(target.as_ref().to_os_string());
108        self
109    }
110
111    /// relocate tablespace in OLDDIR to NEWDIR
112    #[must_use]
113    pub fn tablespace_mapping<S: AsRef<OsStr>>(mut self, tablespace_mapping: S) -> Self {
114        self.tablespace_mapping = Some(tablespace_mapping.as_ref().to_os_string());
115        self
116    }
117
118    /// location for the write-ahead log directory
119    #[must_use]
120    pub fn waldir<S: AsRef<OsStr>>(mut self, waldir: S) -> Self {
121        self.waldir = Some(waldir.as_ref().to_os_string());
122        self
123    }
124
125    /// include required WAL files with specified method
126    #[must_use]
127    pub fn wal_method<S: AsRef<OsStr>>(mut self, wal_method: S) -> Self {
128        self.wal_method = Some(wal_method.as_ref().to_os_string());
129        self
130    }
131
132    /// compress tar output
133    #[must_use]
134    pub fn gzip(mut self) -> Self {
135        self.gzip = true;
136        self
137    }
138
139    /// compress on client or server as specified
140    #[must_use]
141    pub fn compress<S: AsRef<OsStr>>(mut self, compress: S) -> Self {
142        self.compress = Some(compress.as_ref().to_os_string());
143        self
144    }
145
146    /// set fast or spread checkpointing
147    #[must_use]
148    pub fn checkpoint<S: AsRef<OsStr>>(mut self, checkpoint: S) -> Self {
149        self.checkpoint = Some(checkpoint.as_ref().to_os_string());
150        self
151    }
152
153    /// create replication slot
154    #[must_use]
155    pub fn create_slot(mut self) -> Self {
156        self.create_slot = true;
157        self
158    }
159
160    /// set backup label
161    #[must_use]
162    pub fn label<S: AsRef<OsStr>>(mut self, label: S) -> Self {
163        self.label = Some(label.as_ref().to_os_string());
164        self
165    }
166
167    /// do not clean up after errors
168    #[must_use]
169    pub fn no_clean(mut self) -> Self {
170        self.no_clean = true;
171        self
172    }
173
174    /// do not wait for changes to be written safely to disk
175    #[must_use]
176    pub fn no_sync(mut self) -> Self {
177        self.no_sync = true;
178        self
179    }
180
181    /// show progress information
182    #[must_use]
183    pub fn progress(mut self) -> Self {
184        self.progress = true;
185        self
186    }
187
188    /// replication slot to use
189    #[must_use]
190    pub fn slot<S: AsRef<OsStr>>(mut self, slot: S) -> Self {
191        self.slot = Some(slot.as_ref().to_os_string());
192        self
193    }
194
195    /// output verbose messages
196    #[must_use]
197    pub fn verbose(mut self) -> Self {
198        self.verbose = true;
199        self
200    }
201
202    /// output version information, then exit
203    #[must_use]
204    pub fn version(mut self) -> Self {
205        self.version = true;
206        self
207    }
208
209    /// use algorithm for manifest checksums
210    #[must_use]
211    pub fn manifest_checksums<S: AsRef<OsStr>>(mut self, manifest_checksums: S) -> Self {
212        self.manifest_checksums = Some(manifest_checksums.as_ref().to_os_string());
213        self
214    }
215
216    /// hex encode all file names in manifest
217    #[must_use]
218    pub fn manifest_force_encode(mut self) -> Self {
219        self.manifest_force_encode = true;
220        self
221    }
222
223    /// do not estimate backup size in server side
224    #[must_use]
225    pub fn no_estimate_size(mut self) -> Self {
226        self.no_estimate_size = true;
227        self
228    }
229
230    /// suppress generation of backup manifest
231    #[must_use]
232    pub fn no_manifest(mut self) -> Self {
233        self.no_manifest = true;
234        self
235    }
236
237    /// prevent creation of temporary replication slot
238    #[must_use]
239    pub fn no_slot(mut self) -> Self {
240        self.no_slot = true;
241        self
242    }
243
244    /// do not verify checksums
245    #[must_use]
246    pub fn no_verify_checksums(mut self) -> Self {
247        self.no_verify_checksums = true;
248        self
249    }
250
251    /// show this help, then exit
252    #[must_use]
253    pub fn help(mut self) -> Self {
254        self.help = true;
255        self
256    }
257
258    /// connection string
259    #[must_use]
260    pub fn dbname<S: AsRef<OsStr>>(mut self, dbname: S) -> Self {
261        self.dbname = Some(dbname.as_ref().to_os_string());
262        self
263    }
264
265    /// database server host or socket directory
266    #[must_use]
267    pub fn host<S: AsRef<OsStr>>(mut self, host: S) -> Self {
268        self.host = Some(host.as_ref().to_os_string());
269        self
270    }
271
272    /// database server port number
273    #[must_use]
274    pub fn port(mut self, port: u16) -> Self {
275        self.port = Some(port);
276        self
277    }
278
279    /// time between status packets sent to server (in seconds)
280    #[must_use]
281    pub fn status_interval<S: AsRef<OsStr>>(mut self, status_interval: S) -> Self {
282        self.status_interval = Some(status_interval.as_ref().to_os_string());
283        self
284    }
285
286    /// connect as specified database user
287    #[must_use]
288    pub fn username<S: AsRef<OsStr>>(mut self, username: S) -> Self {
289        self.username = Some(username.as_ref().to_os_string());
290        self
291    }
292
293    /// never prompt for password
294    #[must_use]
295    pub fn no_password(mut self) -> Self {
296        self.no_password = true;
297        self
298    }
299
300    /// force password prompt (should happen automatically)
301    #[must_use]
302    pub fn password(mut self) -> Self {
303        self.password = true;
304        self
305    }
306
307    /// user password
308    #[must_use]
309    pub fn pg_password<S: AsRef<OsStr>>(mut self, pg_password: S) -> Self {
310        self.pg_password = Some(pg_password.as_ref().to_os_string());
311        self
312    }
313}
314
315impl CommandBuilder for PgBaseBackupBuilder {
316    /// Get the program name
317    fn get_program(&self) -> &'static OsStr {
318        "pg_basebackup".as_ref()
319    }
320
321    /// Location of the program binary
322    fn get_program_dir(&self) -> &Option<PathBuf> {
323        &self.program_dir
324    }
325
326    /// Get the arguments for the command
327    #[expect(clippy::too_many_lines)]
328    fn get_args(&self) -> Vec<OsString> {
329        let mut args: Vec<OsString> = Vec::new();
330
331        if let Some(pgdata) = &self.pgdata {
332            args.push("--pgdata".into());
333            args.push(pgdata.into());
334        }
335
336        if let Some(format) = &self.format {
337            args.push("--format".into());
338            args.push(format.into());
339        }
340
341        if let Some(max_rate) = &self.max_rate {
342            args.push("--max-rate".into());
343            args.push(max_rate.into());
344        }
345
346        if self.write_recovery_conf {
347            args.push("--write-recovery-conf".into());
348        }
349
350        if let Some(target) = &self.target {
351            args.push("--target".into());
352            args.push(target.into());
353        }
354
355        if let Some(tablespace_mapping) = &self.tablespace_mapping {
356            args.push("--tablespace-mapping".into());
357            args.push(tablespace_mapping.into());
358        }
359
360        if let Some(waldir) = &self.waldir {
361            args.push("--waldir".into());
362            args.push(waldir.into());
363        }
364
365        if let Some(wal_method) = &self.wal_method {
366            args.push("--wal-method".into());
367            args.push(wal_method.into());
368        }
369
370        if self.gzip {
371            args.push("--gzip".into());
372        }
373
374        if let Some(compress) = &self.compress {
375            args.push("--compress".into());
376            args.push(compress.into());
377        }
378
379        if let Some(checkpoint) = &self.checkpoint {
380            args.push("--checkpoint".into());
381            args.push(checkpoint.into());
382        }
383
384        if self.create_slot {
385            args.push("--create-slot".into());
386        }
387
388        if let Some(label) = &self.label {
389            args.push("--label".into());
390            args.push(label.into());
391        }
392
393        if self.no_clean {
394            args.push("--no-clean".into());
395        }
396
397        if self.no_sync {
398            args.push("--no-sync".into());
399        }
400
401        if self.progress {
402            args.push("--progress".into());
403        }
404
405        if let Some(slot) = &self.slot {
406            args.push("--slot".into());
407            args.push(slot.into());
408        }
409
410        if self.verbose {
411            args.push("--verbose".into());
412        }
413
414        if self.version {
415            args.push("--version".into());
416        }
417
418        if let Some(manifest_checksums) = &self.manifest_checksums {
419            args.push("--manifest-checksums".into());
420            args.push(manifest_checksums.into());
421        }
422
423        if self.manifest_force_encode {
424            args.push("--manifest-force-encode".into());
425        }
426
427        if self.no_estimate_size {
428            args.push("--no-estimate-size".into());
429        }
430
431        if self.no_manifest {
432            args.push("--no-manifest".into());
433        }
434
435        if self.no_slot {
436            args.push("--no-slot".into());
437        }
438
439        if self.no_verify_checksums {
440            args.push("--no-verify-checksums".into());
441        }
442
443        if self.help {
444            args.push("--help".into());
445        }
446
447        if let Some(dbname) = &self.dbname {
448            args.push("--dbname".into());
449            args.push(dbname.into());
450        }
451
452        if let Some(host) = &self.host {
453            args.push("--host".into());
454            args.push(host.into());
455        }
456
457        if let Some(port) = &self.port {
458            args.push("--port".into());
459            args.push(port.to_string().into());
460        }
461
462        if let Some(status_interval) = &self.status_interval {
463            args.push("--status-interval".into());
464            args.push(status_interval.into());
465        }
466
467        if let Some(username) = &self.username {
468            args.push("--username".into());
469            args.push(username.into());
470        }
471
472        if self.no_password {
473            args.push("--no-password".into());
474        }
475
476        if self.password {
477            args.push("--password".into());
478        }
479
480        args
481    }
482
483    /// Get the environment variables for the command
484    fn get_envs(&self) -> Vec<(OsString, OsString)> {
485        let mut envs: Vec<(OsString, OsString)> = self.envs.clone();
486
487        if let Some(password) = &self.pg_password {
488            envs.push(("PGPASSWORD".into(), password.into()));
489        }
490
491        envs
492    }
493
494    /// Set an environment variable for the command
495    fn env<S: AsRef<OsStr>>(mut self, key: S, value: S) -> Self {
496        self.envs
497            .push((key.as_ref().to_os_string(), value.as_ref().to_os_string()));
498        self
499    }
500}
501
502#[cfg(test)]
503mod tests {
504    use super::*;
505    use crate::TestSettings;
506    use crate::TestSocketSettings;
507    use crate::traits::CommandToString;
508    use test_log::test;
509
510    #[test]
511    fn test_builder_new() {
512        let command = PgBaseBackupBuilder::new().program_dir(".").build();
513        assert_eq!(
514            PathBuf::from(".").join("pg_basebackup"),
515            PathBuf::from(command.to_command_string().replace('"', ""))
516        );
517    }
518
519    #[test]
520    fn test_builder_from() {
521        let command = PgBaseBackupBuilder::from(&TestSettings).build();
522        #[cfg(not(target_os = "windows"))]
523        let command_prefix = r#"PGPASSWORD="password" "./pg_basebackup" "#;
524        #[cfg(target_os = "windows")]
525        let command_prefix = r#"".\\pg_basebackup" "#;
526
527        assert_eq!(
528            format!(
529                r#"{command_prefix}"--host" "localhost" "--port" "5432" "--username" "postgres""#
530            ),
531            command.to_command_string()
532        );
533    }
534
535    #[test]
536    fn test_builder_from_socket() {
537        let command = PgBaseBackupBuilder::from(&TestSocketSettings).build();
538        #[cfg(not(target_os = "windows"))]
539        let command_prefix = r#"PGPASSWORD="password" "./pg_basebackup" "#;
540        #[cfg(target_os = "windows")]
541        let command_prefix = r#"".\\pg_basebackup" "#;
542
543        assert_eq!(
544            format!(
545                r#"{command_prefix}"--host" "/tmp/pg_socket" "--port" "5432" "--username" "postgres""#
546            ),
547            command.to_command_string()
548        );
549    }
550
551    #[test]
552    fn test_builder() {
553        let command = PgBaseBackupBuilder::new()
554            .env("PGDATABASE", "database")
555            .pgdata("pgdata")
556            .format("plain")
557            .max_rate("100M")
558            .write_recovery_conf()
559            .target("localhost")
560            .tablespace_mapping("tablespace_mapping")
561            .waldir("waldir")
562            .wal_method("stream")
563            .gzip()
564            .compress("client")
565            .checkpoint("fast")
566            .create_slot()
567            .label("my_backup")
568            .no_clean()
569            .no_sync()
570            .progress()
571            .slot("my_slot")
572            .verbose()
573            .version()
574            .manifest_checksums("sha256")
575            .manifest_force_encode()
576            .no_estimate_size()
577            .no_manifest()
578            .no_slot()
579            .no_verify_checksums()
580            .help()
581            .dbname("postgres")
582            .host("localhost")
583            .port(5432)
584            .status_interval("10")
585            .username("postgres")
586            .no_password()
587            .password()
588            .pg_password("password")
589            .build();
590        #[cfg(not(target_os = "windows"))]
591        let command_prefix = r#"PGDATABASE="database" PGPASSWORD="password" "#;
592        #[cfg(target_os = "windows")]
593        let command_prefix = String::new();
594
595        assert_eq!(
596            format!(
597                r#"{command_prefix}"pg_basebackup" "--pgdata" "pgdata" "--format" "plain" "--max-rate" "100M" "--write-recovery-conf" "--target" "localhost" "--tablespace-mapping" "tablespace_mapping" "--waldir" "waldir" "--wal-method" "stream" "--gzip" "--compress" "client" "--checkpoint" "fast" "--create-slot" "--label" "my_backup" "--no-clean" "--no-sync" "--progress" "--slot" "my_slot" "--verbose" "--version" "--manifest-checksums" "sha256" "--manifest-force-encode" "--no-estimate-size" "--no-manifest" "--no-slot" "--no-verify-checksums" "--help" "--dbname" "postgres" "--host" "localhost" "--port" "5432" "--status-interval" "10" "--username" "postgres" "--no-password" "--password""#
598            ),
599            command.to_command_string()
600        );
601    }
602}