libsftpman/model/
filesystem_mount_definition.rs1use std::process::Command;
2
3use serde::{Deserialize, Serialize};
4
5use validator::{Validate, ValidationError};
6
7use crate::utils::command::command_to_string;
8
9use crate::auth_type::{
10 deserialize_auth_type_from_string, serialize_auth_type_to_string, AuthType,
11};
12
13use crate::errors::SftpManError;
14
15pub const DEFAULT_MOUNT_PATH_PREFIX: &str = "/mnt/sshfs";
16
17#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
18#[validate(schema(
19 function = "validate_ssh_key_for_publickey_auth",
20 skip_on_field_errors = false
21))]
22pub struct FilesystemMountDefinition {
23 #[validate(
26 length(min = 1, message = "An ID must be provided."),
27 custom(
28 function = "validate_id",
29 message = "The ID must be a valid identifier (alphanumeric characters, underscores, dashes, or dots)."
30 )
31 )]
32 pub id: String,
33
34 #[validate(length(min = 1, message = "A host must be provided."))]
36 pub host: String,
37
38 pub port: u16,
40
41 #[validate(length(min = 1, message = "A user must be provided."))]
43 pub user: String,
44
45 #[serde(rename = "mountOptions")]
48 pub mount_options: Vec<String>,
49
50 #[serde(rename = "mountPoint")]
52 #[validate(
53 length(min = 1, message = "A remote path must be provided."),
54 custom(
55 function = "validate_absolute_path",
56 message = "The remote path must be absolute."
57 )
58 )]
59 pub remote_path: String,
60
61 #[serde(rename = "mountDestPath")]
64 #[validate(
65 length(min = 1, message = "A local mount destination path must be provided."),
66 custom(
67 function = "validate_absolute_path",
68 message = "The local mount destination path must be absolute."
69 )
70 )]
71 pub mount_dest_path: Option<String>,
72
73 #[serde(rename = "beforeMount")]
75 #[serde(default)]
76 pub cmd_before_mount: String,
77
78 #[serde(rename = "authType")]
81 #[serde(
82 serialize_with = "serialize_auth_type_to_string",
83 deserialize_with = "deserialize_auth_type_from_string"
84 )]
85 pub auth_type: AuthType,
86
87 #[serde(rename = "sshKey")]
89 pub ssh_key: String,
90}
91
92const SSH_DEFAULT_TIMEOUT: u32 = 10;
93
94impl Default for FilesystemMountDefinition {
95 fn default() -> Self {
96 FilesystemMountDefinition {
97 id: String::new(),
98 host: String::new(),
99 port: 22,
100 user: String::new(),
101 mount_options: Vec::new(),
102 remote_path: String::new(),
103 mount_dest_path: None,
104 cmd_before_mount: String::new(),
105 auth_type: AuthType::PublicKey,
106 ssh_key: String::new(),
107 }
108 }
109}
110
111impl FilesystemMountDefinition {
112 pub fn from_json_string(contents: &str) -> Result<Self, serde_json::Error> {
113 let deserialized: Self = serde_json::from_str(contents)?;
114 Ok(deserialized)
115 }
116
117 pub fn to_json_string(&self) -> serde_json::Result<String> {
118 serde_json::to_string_pretty(self)
119 }
120
121 pub fn local_mount_path(&self) -> String {
125 match &self.mount_dest_path {
126 Some(path) => path.clone(),
127 None => format!("{0}/{1}", DEFAULT_MOUNT_PATH_PREFIX, self.id),
128 }
129 }
130
131 pub fn mount_commands(&self) -> Result<Vec<Command>, SftpManError> {
134 log::debug!("{0}: building list of mount commands", self.id);
135
136 let mut list: Vec<Command> = Vec::new();
137
138 if !self.cmd_before_mount.is_empty() {
139 if self.cmd_before_mount == "/bin/true" || self.cmd_before_mount == "true" {
140 log::debug!(
143 "{0}: ignoring no-op before-mount command {1}",
144 self.id,
145 self.cmd_before_mount
146 );
147 } else {
148 let mut program_name = "";
149 let mut args: Vec<&str> = Vec::new();
150
151 for (idx, arg) in self.cmd_before_mount.split(' ').enumerate() {
152 match idx {
153 0 => {
154 program_name = arg;
155 }
156 _ => {
157 args.push(arg);
158 }
159 }
160 }
161
162 if program_name.is_empty() {
163 return Err(SftpManError::MountCommandBuilding(format!(
164 "could not extract program name from {0}",
165 self.cmd_before_mount
166 )));
167 }
168
169 let mut cmd_before = Command::new(program_name);
170 for arg in args {
171 cmd_before.arg(arg);
172 }
173
174 list.push(cmd_before);
175 }
176 }
177
178 let mut cmd_ssh = Command::new("ssh");
179 cmd_ssh
180 .arg("-p")
181 .arg(self.port.to_string())
182 .arg("-o")
183 .arg(format!("ConnectTimeout={0}", SSH_DEFAULT_TIMEOUT));
184
185 match &self.auth_type {
186 AuthType::PublicKey => {
187 cmd_ssh.arg(format!(
188 "-o PreferredAuthentications={0}",
189 AuthType::PublicKey.to_static_str()
190 ));
191 cmd_ssh.arg(format!("-i {0}", self.ssh_key));
192 }
193 AuthType::AuthenticationAgent => {
194 }
197 any_other => {
198 cmd_ssh.arg(format!(
199 "-o PreferredAuthentications={0}",
200 any_other.to_static_str()
201 ));
202 }
203 };
204
205 let mut cmd_sshfs = Command::new("sshfs");
206 cmd_sshfs
207 .args(self.mount_options.iter().flat_map(|opt| ["-o", opt]))
209 .arg("-o")
211 .arg(format!("ssh_command={0}", command_to_string(&cmd_ssh)))
212 .arg(format!(
215 "{0}@[{1}]:{2}",
216 self.user, self.host, self.remote_path
217 ))
218 .arg(self.local_mount_path());
220
221 list.push(cmd_sshfs);
222
223 Ok(list)
224 }
225
226 pub fn umount_commands(&self) -> Result<Vec<Command>, SftpManError> {
231 log::debug!("{0}: building list of unmount commands", self.id);
232
233 let mut list: Vec<Command> = Vec::new();
234
235 let mut cmd = Command::new("fusermount");
240 cmd.arg("-u").arg(self.local_mount_path());
241
242 list.push(cmd);
243
244 Ok(list)
245 }
246
247 pub fn open_command(&self) -> Command {
251 let mut cmd = Command::new("xdg-open");
252 cmd.arg(self.local_mount_path());
253
254 cmd
255 }
256}
257
258fn validate_id(id: &str) -> Result<(), ValidationError> {
259 if !id
260 .chars()
261 .all(|c| c.is_alphanumeric() || c == '_' || c == '-' || c == '.')
262 {
263 return Err(ValidationError::new("invalid_id").with_message(
264 "The ID must contain only alphanumeric characters, underscores, dashes, or dots."
265 .into(),
266 ));
267 }
268 Ok(())
269}
270
271fn validate_absolute_path(path: &str) -> Result<(), ValidationError> {
272 if !path.starts_with('/') {
273 return Err(ValidationError::new("not_absolute_path")
274 .with_message(format!("The path {0} is not absolute.", path).into()));
275 }
276
277 Ok(())
278}
279
280fn validate_ssh_key_for_publickey_auth(
281 entity: &&FilesystemMountDefinition,
282) -> Result<(), ValidationError> {
283 match entity.auth_type {
284 AuthType::PublicKey => {
285 if entity.ssh_key.is_empty() {
286 Err(
287 ValidationError::new("no_ssh_key_for_publickey_auth").with_message(
288 format!(
289 "The {0} authentication type requires an SSH key to be provided.",
290 AuthType::PublicKey,
291 )
292 .into(),
293 ),
294 )
295 } else {
296 Ok(())
297 }
298 }
299 _ => Ok(()),
300 }
301}