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