yggdrasil/api/database/backup/
commands.rs1use std::path::Path;
4
5fn pg_command_with_db_config(
7 program: &str,
8 db_url: &str,
9) -> std::io::Result<std::process::Command> {
10 use std::str::FromStr;
11 use tokio_postgres::config::{Host, SslMode};
12
13 let config = tokio_postgres::Config::from_str(db_url).map_err(|error| {
14 std::io::Error::new(
15 std::io::ErrorKind::InvalidInput,
16 format!("DATABASE_URL 解析失败: {error}"),
17 )
18 })?;
19 let mut command = std::process::Command::new(program);
20 command.env_remove("DATABASE_URL");
23
24 let hosts: Vec<String> = config
25 .get_hosts()
26 .iter()
27 .map(|host| match host {
28 Host::Tcp(host) => host.clone(),
29 Host::Unix(path) => path.to_string_lossy().into_owned(),
30 })
31 .collect();
32 if !hosts.is_empty() {
33 command.env("PGHOST", hosts.join(","));
34 }
35 if !config.get_hostaddrs().is_empty() {
36 command.env(
37 "PGHOSTADDR",
38 config
39 .get_hostaddrs()
40 .iter()
41 .map(ToString::to_string)
42 .collect::<Vec<_>>()
43 .join(","),
44 );
45 }
46 if !config.get_ports().is_empty() {
47 command.env(
48 "PGPORT",
49 config
50 .get_ports()
51 .iter()
52 .map(ToString::to_string)
53 .collect::<Vec<_>>()
54 .join(","),
55 );
56 }
57 if let Some(user) = config.get_user() {
58 command.env("PGUSER", user);
59 }
60 if let Some(password) = config.get_password() {
61 command.env("PGPASSWORD", String::from_utf8_lossy(password).into_owned());
62 }
63 if let Some(dbname) = config.get_dbname() {
64 command.env("PGDATABASE", dbname);
65 }
66 if let Some(options) = config.get_options() {
67 command.env("PGOPTIONS", options);
68 }
69 if let Some(application_name) = config.get_application_name() {
70 command.env("PGAPPNAME", application_name);
71 }
72 let ssl_mode = match config.get_ssl_mode() {
73 SslMode::Disable => "disable",
74 SslMode::Prefer => "prefer",
75 SslMode::Require => "require",
76 _ => "prefer",
77 };
78 command.env("PGSSLMODE", ssl_mode);
79 if let Some(timeout) = config.get_connect_timeout() {
80 command.env("PGCONNECT_TIMEOUT", timeout.as_secs().max(1).to_string());
81 }
82 Ok(command)
83}
84
85pub(super) fn pg_dump_command(db_url: &str) -> std::io::Result<std::process::Command> {
86 let mut command = pg_command_with_db_config("pg_dump", db_url)?;
87 command.args(["--clean", "--if-exists", "--no-owner", "--no-privileges"]);
88 Ok(command)
89}
90pub(super) fn psql_restore_command(
91 db_url: &str,
92 path: &Path,
93) -> std::io::Result<std::process::Command> {
94 let mut command = pg_command_with_db_config("psql", db_url)?;
95 command
96 .args(["--single-transaction", "-v", "ON_ERROR_STOP=1", "-f"])
97 .arg(path)
98 .stdout(std::process::Stdio::null())
99 .stderr(std::process::Stdio::piped());
100 Ok(command)
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 #[test]
108 fn pg_dump_is_owner_acl_neutral_without_db_url_in_args() {
109 let db_url = "postgres://user:secret@example.com:5432/dbname";
110 let command = pg_dump_command(db_url).expect("valid database URL");
111 let args: Vec<_> = command
112 .get_args()
113 .map(|arg| arg.to_string_lossy().into_owned())
114 .collect();
115 assert!(args.iter().any(|arg| arg == "--no-owner"), "{args:?}");
116 assert!(args.iter().any(|arg| arg == "--no-privileges"), "{args:?}");
117 assert!(!args.iter().any(|arg| arg.contains("secret")), "{args:?}");
118 assert!(command
119 .get_envs()
120 .any(|(key, value)| key == "PGPASSWORD" && value.is_some()));
121 }
122
123 #[test]
124 fn psql_restore_is_atomic_without_db_url_in_args() {
125 let db_url = "postgres://user:secret@example.com:5432/dbname";
126 let command =
127 psql_restore_command(db_url, Path::new("backup.sql")).expect("valid database URL");
128 let args: Vec<_> = command
129 .get_args()
130 .map(|arg| arg.to_string_lossy().into_owned())
131 .collect();
132 assert!(
133 args.iter().any(|arg| arg == "--single-transaction"),
134 "{args:?}"
135 );
136 assert!(!args.iter().any(|arg| arg.contains("secret")), "{args:?}");
137 }
138}