1use std::collections::HashMap;
2use std::fs;
3use std::path::PathBuf;
4use std::process::Command;
5use std::time::Duration;
6
7use crate::model::DEFAULT_MOUNT_PATH_PREFIX;
8
9use super::errors::{ManagerInitError, PreflightCheckError, SftpManError};
10use super::model::{FilesystemMountDefinition, MountState};
11
12use super::utils::command::{run_command, run_command_background};
13use super::utils::fs::{
14 ensure_directory_recursively_created, get_mounts_under_path_prefix, remove_empty_directory,
15};
16use super::utils::fusermount::{create_fusermount_check_command, create_fusermount3_check_command};
17use super::utils::process::{ensure_process_killed, sshfs_pid_by_definition};
18
19const VFS_TYPE_SSHFS: &str = "fuse.sshfs";
20
21#[derive(Default, Clone)]
22pub struct Manager {
23 config_path: PathBuf,
24}
25
26impl Manager {
27 pub fn new() -> Result<Self, ManagerInitError> {
28 let d = directories::ProjectDirs::from("sftpman", "Devture Ltd", "sftpman")
29 .ok_or(ManagerInitError::NoConfigDirectory)?;
30
31 Ok(Self {
32 config_path: d.config_dir().to_path_buf().to_owned(),
33 })
34 }
35
36 pub fn definitions(&self) -> Result<Vec<FilesystemMountDefinition>, SftpManError> {
38 let dir_path = self.config_path_mounts();
39
40 if !dir_path.is_dir() {
41 log::debug!(
42 "Mount config directory {0} doesn't exist. Returning an empty definitions list ...",
43 dir_path.display()
44 );
45 return Ok(vec![]);
46 }
47
48 let mut list: Vec<FilesystemMountDefinition> = Vec::new();
49
50 let directory_entries =
51 fs::read_dir(dir_path).map_err(|err| SftpManError::Generic(err.to_string()))?;
52
53 for entry in directory_entries {
54 let entry = entry.map_err(|err| SftpManError::Generic(err.to_string()))?;
55
56 let path = entry.path();
57 if !path.is_file() {
58 continue;
59 }
60
61 let name = path.file_name();
62 if name.is_none() {
63 continue;
64 }
65 if !name.unwrap().to_string_lossy().ends_with(".json") {
66 continue;
67 }
68
69 match Self::definition_from_config_path(&path) {
70 Ok(cfg) => list.push(cfg),
71 Err(err) => return Err(err),
72 }
73 }
74
75 list.sort_by_key(|item| item.id.clone());
76
77 Ok(list)
78 }
79
80 pub fn definition(&self, id: &str) -> Result<FilesystemMountDefinition, SftpManError> {
82 Self::definition_from_config_path(&self.config_path_for_definition_id(id))
83 }
84
85 pub fn full_state(&self) -> Result<Vec<MountState>, SftpManError> {
87 let mut mounted_sshfs_paths_map: HashMap<String, bool> = HashMap::new();
88
89 for mount in get_mounts_under_path_prefix("/")? {
90 if mount.vfstype != VFS_TYPE_SSHFS {
91 continue;
92 }
93
94 mounted_sshfs_paths_map
95 .insert(mount.file.as_os_str().to_str().unwrap().to_owned(), true);
96 }
97
98 let mut list: Vec<MountState> = Vec::new();
99
100 for definition in self.definitions()? {
101 let mounted = mounted_sshfs_paths_map.contains_key(&definition.local_mount_path());
102 list.push(MountState::new(definition, mounted));
103 }
104
105 Ok(list)
106 }
107
108 pub fn is_definition_mounted(
110 &self,
111 definition: &FilesystemMountDefinition,
112 ) -> Result<bool, SftpManError> {
113 let local_mount_path = definition.local_mount_path();
114
115 for mount in get_mounts_under_path_prefix(local_mount_path.as_str())? {
116 if *mount.file.as_os_str().to_str().unwrap() != local_mount_path {
117 continue;
118 }
119
120 if mount.vfstype != VFS_TYPE_SSHFS {
121 return Err(SftpManError::MountVfsTypeMismatch {
122 path: std::path::Path::new(&local_mount_path).to_path_buf(),
123 found_vfs_type: mount.vfstype.to_string(),
124 expected_vfs_type: VFS_TYPE_SSHFS.to_string(),
125 });
126 }
127
128 return Ok(true);
129 }
130
131 Ok(false)
132 }
133
134 pub fn mount(&self, definition: &FilesystemMountDefinition) -> Result<(), SftpManError> {
136 if self.is_definition_mounted(definition)? {
137 log::info!("{0}: already mounted, nothing to do..", definition.id);
138 return Ok(());
139 }
140
141 log::info!("{0}: mounting..", definition.id);
142
143 ensure_directory_recursively_created(&definition.local_mount_path())?;
144
145 let cmds = definition.mount_commands().unwrap();
146
147 for cmd in cmds {
148 log::debug!("{0}: executing mount command: {1:?}", definition.id, cmd);
149
150 if let Err(err) = run_command(cmd) {
151 log::error!(
152 "{0}: failed to run mount command: {1:?}",
153 definition.id,
154 err
155 );
156
157 log::debug!("{0}: performing umount to clean up", definition.id);
158
159 if let Err(err) = self.umount(definition) {
161 log::debug!(
162 "{0}: failed to perform cleanup-umount: {1:?}",
163 definition.id,
164 err
165 );
166 }
167
168 self.clean_up_after_unmount(definition);
169
170 return Err(err);
171 }
172 }
173
174 Ok(())
175 }
176
177 pub fn umount(&self, definition: &FilesystemMountDefinition) -> Result<(), SftpManError> {
183 if !self.is_definition_mounted(definition)? {
184 log::info!("{0}: not mounted, nothing to do..", definition.id);
185 return Ok(());
186 }
187
188 log::info!("{0}: unmounting..", definition.id);
189
190 match self.do_umount(definition) {
191 Ok(_) => Ok(()),
192
193 Err(err) => {
194 log::warn!("{0} failed to get unmounted: {1:?}", definition.id, err);
197
198 self.kill_sshfs_for_definition(definition)?;
199
200 self.clean_up_after_unmount(definition);
206
207 Ok(())
208 }
209 }
210 }
211
212 fn do_umount(&self, definition: &FilesystemMountDefinition) -> Result<(), SftpManError> {
213 let cmds = definition.umount_commands().unwrap();
214
215 for cmd in cmds {
216 log::debug!("{0}: executing unmount command: {1:?}", definition.id, cmd);
217
218 if let Err(err) = run_command(cmd) {
219 log::error!(
220 "{0}: failed to run unmount command: {1:?}",
221 definition.id,
222 err
223 );
224
225 self.clean_up_after_unmount(definition);
228
229 return Err(err);
230 }
231 }
232
233 self.clean_up_after_unmount(definition);
234
235 Ok(())
236 }
237
238 pub fn remove(&self, definition: &FilesystemMountDefinition) -> Result<(), SftpManError> {
240 log::info!("{0}: removing..", definition.id);
241
242 self.umount(definition)?;
243
244 let definition_config_path = self.config_path_for_definition_id(&definition.id);
245
246 log::debug!(
247 "{0}: deleting file {1}",
248 definition.id,
249 definition_config_path.display()
250 );
251
252 fs::remove_file(&definition_config_path).map_err(|err| {
253 SftpManError::FilesystemMountDefinitionRemove(definition_config_path, err)
254 })?;
255
256 Ok(())
257 }
258
259 pub fn preflight_check(&self) -> Result<(), Vec<PreflightCheckError>> {
261 let mut cmd_alternative_groups: Vec<Vec<Command>> = Vec::new();
262
263 let mut cmd_sshfs = Command::new("sshfs");
264 cmd_sshfs.arg("-h");
265 cmd_alternative_groups.push(vec![cmd_sshfs]);
266
267 let mut cmd_ssh = Command::new("ssh");
268 cmd_ssh.arg("-V");
269 cmd_alternative_groups.push(vec![cmd_ssh]);
270
271 cmd_alternative_groups.push(vec![
274 create_fusermount3_check_command(),
275 create_fusermount_check_command(),
276 ]);
277
278 let mut errors: Vec<PreflightCheckError> = Vec::new();
279
280 for cmd_group in cmd_alternative_groups {
281 let mut cmd_group_successful = false;
282 let mut cmd_group_errors: Vec<PreflightCheckError> = Vec::new();
283
284 for cmd in cmd_group {
285 log::debug!("Executing preflight-check command: {0:?}", cmd);
286
287 if let Err(err) = run_command(cmd) {
288 log::warn!("Failed to run preflight-check command: {0:?}", err);
289
290 let preflight_check_error = match err {
291 SftpManError::CommandExecution(cmd, err) => {
292 Some(PreflightCheckError::CommandExecution(cmd, err))
293 }
294 SftpManError::CommandUnsuccessful(cmd, output) => {
295 Some(PreflightCheckError::CommandUnsuccessful(cmd, output))
296 }
297 _ => {
298 log::error!("Unexpected error type: {0:?}", err);
300 None
301 }
302 };
303
304 if let Some(preflight_check_error) = preflight_check_error {
305 cmd_group_errors.push(preflight_check_error);
306 }
307 } else {
308 log::debug!("Preflight-check command succeeded");
309 cmd_group_successful = true;
310 break;
311 }
312 }
313
314 if !cmd_group_successful {
315 errors.extend(cmd_group_errors);
316 }
317 }
318
319 let default_mount_path = PathBuf::from(DEFAULT_MOUNT_PATH_PREFIX);
320 let mut default_mount_path_ok = false;
321 let random_test_path = default_mount_path.join(format!(
322 "_{}_test_{}",
323 env!("CARGO_PKG_NAME"),
324 rand::random::<u32>()
325 ));
326
327 if default_mount_path.exists() {
328 log::debug!(
329 "Default mount path {} already exists",
330 DEFAULT_MOUNT_PATH_PREFIX
331 );
332 default_mount_path_ok = true;
333 } else {
334 log::warn!(
335 "Default mount path {} does not exist, attempting to create it",
336 DEFAULT_MOUNT_PATH_PREFIX
337 );
338
339 if let Err(err) = fs::create_dir_all(&default_mount_path) {
340 log::error!(
341 "Failed to create mount path {}: {}",
342 DEFAULT_MOUNT_PATH_PREFIX,
343 err
344 );
345
346 errors.push(PreflightCheckError::DefaultBasePathIO(
347 default_mount_path,
348 err,
349 ));
350 } else {
351 default_mount_path_ok = true;
352 }
353 }
354
355 if default_mount_path_ok {
356 log::debug!(
357 "Testing if we can create and remove directory: {}",
358 random_test_path.display()
359 );
360
361 if let Err(err) = fs::create_dir_all(&random_test_path) {
362 log::error!(
363 "Failed to create test directory {}: {}",
364 random_test_path.display(),
365 err
366 );
367 errors.push(PreflightCheckError::TestUnderBasePathIO(
368 random_test_path,
369 err,
370 ));
371 } else if let Err(err) = fs::remove_dir(&random_test_path) {
372 log::error!(
373 "Failed to remove test directory {}: {}",
374 random_test_path.display(),
375 err
376 );
377 errors.push(PreflightCheckError::TestUnderBasePathIO(
378 random_test_path,
379 err,
380 ));
381 }
382 }
383
384 if errors.is_empty() {
385 Ok(())
386 } else {
387 Err(errors)
388 }
389 }
390
391 pub fn persist(&self, definition: &FilesystemMountDefinition) -> Result<(), SftpManError> {
395 let mut is_existing_and_mounted = false;
396 if let Ok(old) = self.definition(&definition.id) {
397 is_existing_and_mounted = self.is_definition_mounted(&old)?;
398
399 if is_existing_and_mounted {
400 log::debug!(
401 "{0} was found to be an existing and currently mounted definition. Unmounting..",
402 definition.id
403 );
404
405 if let Err(err) = self.umount(&old) {
406 log::error!("{0} failed to be unmounted: {1:?}", definition.id, err);
407 }
408 }
409 }
410
411 let path = self.config_path_for_definition_id(&definition.id);
412
413 let config_dir_path = path
414 .parent()
415 .expect("Config directory path should have a parent");
416
417 if !config_dir_path.exists() {
418 log::info!(
419 "Config directory {} does not exist, attempting to create it",
420 config_dir_path.display()
421 );
422
423 if let Err(err) = fs::create_dir_all(config_dir_path) {
424 log::error!(
425 "Failed to create config directory {}: {}",
426 config_dir_path.display(),
427 err
428 );
429 return Err(SftpManError::IO(path.clone(), err));
430 }
431 }
432
433 let serialized = definition
434 .to_json_string()
435 .map_err(|err| SftpManError::JSON(path.clone(), err))?;
436
437 fs::write(&path, serialized).map_err(|err| SftpManError::IO(path.clone(), err))?;
438
439 if is_existing_and_mounted {
440 log::debug!(
441 "{0} is being mounted, because it was before updating..",
442 definition.id
443 );
444
445 if let Err(err) = self.mount(definition) {
446 log::error!(
447 "{0} failed get re-mounted afte rupdating: {1:?}",
448 definition.id,
449 err
450 );
451 }
452 }
453
454 Ok(())
455 }
456
457 pub fn open(&self, definition: &FilesystemMountDefinition) -> Result<(), SftpManError> {
459 if let Err(err) = run_command_background(definition.open_command()) {
460 log::error!("{0}: failed to run open command: {1:?}", definition.id, err);
461 }
462 Ok(())
463 }
464
465 fn kill_sshfs_for_definition(
466 &self,
467 definition: &FilesystemMountDefinition,
468 ) -> Result<(), SftpManError> {
469 log::debug!(
470 "Trying to determine the sshfs process for {0}",
471 definition.id
472 );
473
474 let pid = sshfs_pid_by_definition(definition)?;
475
476 match pid {
477 Some(pid) => {
478 log::debug!(
479 "Process id for {0} determined to be: {1}. Killing..",
480 definition.id,
481 pid
482 );
483
484 ensure_process_killed(pid, Duration::from_millis(500), Duration::from_millis(2000))
485 }
486
487 None => Err(SftpManError::Generic(format!(
488 "Failed to determine pid for: {0}",
489 definition.id
490 ))),
491 }
492 }
493
494 fn clean_up_after_unmount(&self, definition: &FilesystemMountDefinition) {
495 log::debug!("{0}: cleaning up after unmounting", definition.id);
496
497 if let Err(err) = remove_empty_directory(&definition.local_mount_path()) {
498 log::debug!(
499 "{0}: failed to remove local mount point: {1:?}",
500 definition.id,
501 err
502 );
503 }
504 }
505
506 fn config_path_mounts(&self) -> PathBuf {
507 self.config_path.join("mounts")
508 }
509
510 fn config_path_for_definition_id(&self, id: &str) -> PathBuf {
511 self.config_path_mounts().join(format!("{0}.json", id))
512 }
513
514 fn definition_from_config_path(
515 path: &PathBuf,
516 ) -> Result<FilesystemMountDefinition, SftpManError> {
517 let contents = fs::read_to_string(path)
518 .map_err(|err| SftpManError::FilesystemMountDefinitionRead(path.clone(), err))?;
519
520 let mount_config_result = FilesystemMountDefinition::from_json_string(&contents);
521
522 match mount_config_result {
523 Ok(cfg) => Ok(cfg),
524 Err(err) => Err(SftpManError::JSON(path.clone(), err)),
525 }
526 }
527}