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