1use std::{
2 borrow::Cow,
3 cmp::min,
4 fmt::Display,
5 fs::File,
6 io::{self, BufRead, BufReader},
7 path::PathBuf,
8};
9
10use anyhow::{bail, Context, Result};
11use ratatui::{
12 layout::{Constraint, Rect},
13 style::{Color, Style},
14 text::Text,
15 widgets::{Cell, Row, Table},
16 Frame,
17};
18use serde::Deserialize;
19use serde_json::{from_str, from_value, Value};
20use sysinfo::Disks;
21
22use crate::common::{
23 current_uid, current_username, is_dir_empty, is_in_path, CRYPTSETUP, GIO, LSBLK, MKDIR, MOUNT,
24 UDISKSCTL, UMOUNT,
25};
26use crate::config::{ColorG, Gradient, MENU_STYLES};
27use crate::io::{
28 drop_sudo_privileges, execute_and_output, execute_sudo_command_passwordless,
29 execute_sudo_command_with_password, reset_sudo_faillock, set_sudo_session, CowStr, DrawMenu,
30 Offseted,
31};
32use crate::modes::{ContentWindow, MountCommands, MountParameters, PasswordHolder};
33use crate::{colored_skip_take, impl_content, impl_selectable, log_info, log_line};
34
35#[derive(Debug, Clone, Default)]
39pub struct IsoDevice {
40 pub path: String,
42 pub mountpoints: Option<String>,
44 is_mounted: bool,
45}
46
47impl IsoDevice {
48 const FILENAME: &'static str = "fm_iso";
49
50 #[must_use]
52 pub fn from_path(path: String) -> Self {
53 log_info!("IsoDevice from_path: {path}");
54 Self {
55 path,
56 ..Default::default()
57 }
58 }
59
60 fn mountpoints(username: &str) -> String {
61 format!(
62 "/run/media/{username}/{filename}",
63 filename = Self::FILENAME
64 )
65 }
66
67 fn set_mountpoint(&mut self, username: &str) {
68 self.mountpoints = Some(Self::mountpoints(username))
69 }
70}
71
72impl MountParameters for IsoDevice {
73 fn mkdir_parameters(&self, username: &str) -> [String; 3] {
74 [
75 "mkdir".to_owned(),
76 "-p".to_owned(),
77 format!(
78 "/run/media/{username}/{filename}",
79 filename = Self::FILENAME
80 ),
81 ]
82 }
83
84 fn mount_parameters(&self, _username: &str) -> Vec<String> {
85 vec![
86 "mount".to_owned(),
87 "-o".to_owned(),
88 "loop".to_owned(),
89 self.path.clone(),
90 self.mountpoints
91 .clone()
92 .expect("mountpoint should be set already"),
93 ]
94 }
95
96 fn umount_parameters(&self, username: &str) -> Vec<String> {
97 vec![
98 "umount".to_owned(),
99 format!(
100 "/run/media/{username}/{mountpoint}",
101 mountpoint = Self::mountpoints(username),
102 ),
103 ]
104 }
105}
106
107impl MountCommands for IsoDevice {
108 fn is_mounted(&self) -> bool {
109 self.is_mounted
110 }
111
112 fn umount(&mut self, username: &str, password: &mut PasswordHolder) -> Result<bool> {
113 let success = set_sudo_session(password)?;
115 password.reset();
116 if !success {
117 return Ok(false);
118 }
119 let (success, stdout, stderr) =
120 execute_sudo_command_passwordless(&self.umount_parameters(username))?;
121 log_info!("stdout: {stdout}\nstderr: {stderr}");
122 if success {
123 self.is_mounted = false;
124 }
125 drop_sudo_privileges()?;
126 Ok(success)
127 }
128
129 fn mount(&mut self, username: &str, password: &mut PasswordHolder) -> Result<bool> {
130 log_info!("iso mount: {username}, {password:?}");
131 if self.is_mounted {
132 bail!("iso device mount: device is already mounted")
133 };
134 let success = set_sudo_session(password)?;
136 password.reset();
137 if !success {
138 return Ok(false);
139 }
140 let (success, stdout, stderr) =
142 execute_sudo_command_passwordless(&self.mkdir_parameters(username))?;
143 if !stdout.is_empty() || !stderr.is_empty() {
144 log_info!("stdout: {stdout}\nstderr: {stderr}");
145 }
146 let mut last_success = false;
147 if success {
148 self.set_mountpoint(username);
149 let (success, stdout, stderr) =
151 execute_sudo_command_passwordless(&self.mount_parameters(username))?;
152 last_success = success;
153 if !success {
154 log_info!("stdout: {stdout}\nstderr: {stderr}");
155 }
156 self.is_mounted = success;
157 } else {
158 self.is_mounted = false;
159 }
160 drop_sudo_privileges()?;
161 Ok(last_success)
162 }
163}
164
165impl Display for IsoDevice {
166 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167 match &self.mountpoints {
168 Some(mountpoint) => write!(f, "mounted {path} to {mountpoint}", path = self.path,),
169 None => write!(f, "not mounted {path}", path = self.path),
170 }
171 }
172}
173
174#[derive(Debug, Clone, Copy, Eq, PartialEq)]
176pub enum MountAction {
177 MOUNT,
178 UMOUNT,
179}
180
181#[derive(Debug)]
182pub enum NetworkKind {
183 NFS,
184 CIFS,
185}
186
187impl NetworkKind {
188 fn from_fs_type(fs_type: &str) -> Option<Self> {
189 match fs_type {
190 "cifs" => Some(Self::CIFS),
191 "nfs4" => Some(Self::NFS),
192 _ => None,
193 }
194 }
195}
196
197impl Display for NetworkKind {
198 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
199 let kind = match self {
200 Self::NFS => "nfs",
201 Self::CIFS => "smb",
202 };
203
204 write!(f, "{kind}")
205 }
206}
207
208#[derive(Debug)]
211pub struct NetworkMount {
212 pub kind: NetworkKind,
213 pub path: String,
214 pub mountpoint: String,
215}
216
217impl NetworkMount {
219 fn from_network_line(line: io::Result<String>) -> Option<Self> {
223 let Ok(line) = line else {
224 return None;
225 };
226 let parts: Vec<&str> = line.split_whitespace().collect();
227 if parts.len() <= 6 {
228 return None;
229 }
230 let kind = NetworkKind::from_fs_type(parts.get(parts.len() - 3)?)?;
231 let mountpoint = parts.get(4)?.to_string();
232 let path = parts.get(parts.len() - 2)?.to_string();
233 Some(Self {
234 kind,
235 mountpoint,
236 path,
237 })
238 }
239
240 fn umount(&self, password: &mut PasswordHolder) -> Result<bool> {
241 let success = set_sudo_session(password);
242 password.reset();
243 if !matches!(success, Ok(true)) {
244 return Ok(false);
245 }
246 let (success, _, _) =
247 execute_sudo_command_passwordless(&[UMOUNT, self.mountpoint.as_str()])?;
248 log_info!(
249 "Unmounted {device}. Success ? {success}",
250 device = self.mountpoint,
251 );
252 drop_sudo_privileges()?;
253 Ok(success)
254 }
255
256 fn symbols(&self) -> String {
257 " MN".to_string()
258 }
259}
260
261impl Display for NetworkMount {
262 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
263 write!(
264 f,
265 "MN {kind} {path} -> {mountpoint}",
266 kind = self.kind,
267 path = self.path,
268 mountpoint = self.mountpoint
269 )
270 }
271}
272
273#[derive(Debug, Clone, Default)]
276pub struct Mtp {
277 pub name: String,
278 pub path: String,
279 pub is_mounted: bool,
280 pub is_ejected: bool,
281}
282
283impl Mtp {
284 fn from_gio(line: &str) -> Result<Self> {
290 let name = line
291 .replace("activation_root=mtp://", "")
292 .replace('/', "")
293 .trim()
294 .to_owned();
295 let uid = current_uid()?;
296 let path = format!("/run/user/{uid}/gvfs/mtp:host={name}");
297 let pb_path = std::path::Path::new(&path);
298 let is_mounted = pb_path.exists() && !is_dir_empty(pb_path)?;
299 let is_ejected = false;
300 #[cfg(debug_assertions)]
301 log_info!("gio {name} - is_mounted {is_mounted}");
302 Ok(Self {
303 name,
304 path,
305 is_mounted,
306 is_ejected,
307 })
308 }
309
310 fn format_for_gio(&self) -> String {
312 format!("mtp://{name}", name = self.name)
313 }
314
315 fn is_mounted(&self) -> bool {
317 self.is_mounted
318 }
319
320 fn mount(&mut self) -> Result<bool> {
326 if self.is_mounted {
327 bail!("Already mounted {name}", name = self.name);
328 }
329 self.is_mounted = execute_and_output(GIO, ["mount", &self.format_for_gio()])?
330 .status
331 .success();
332
333 log_line!(
334 "Mounted {device}. Success ? {success}",
335 device = self.name,
336 success = self.is_mounted
337 );
338 Ok(self.is_mounted)
339 }
340
341 fn umount(&mut self) -> Result<bool> {
347 if !self.is_mounted {
348 bail!("Not mounted {name}", name = self.name);
349 }
350 self.is_mounted = execute_and_output(GIO, ["mount", &self.format_for_gio(), "-u"])?
351 .status
352 .success();
353
354 log_info!(
355 "Unmounted {device}. Success ? {success}",
356 device = self.name,
357 success = self.is_mounted
358 );
359 Ok(!self.is_mounted)
360 }
361
362 fn symbols(&self) -> String {
363 let is_mounted = self.is_mounted();
364 let mount_repr = if is_mounted { 'M' } else { 'U' };
365 format!(" {mount_repr}P")
366 }
367}
368
369impl Display for Mtp {
370 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
371 let is_mounted = self.is_mounted();
372 write!(
373 f,
374 "{mount_repr}P {name}",
375 mount_repr = if is_mounted { 'M' } else { 'U' },
376 name = self.name.clone()
377 )?;
378 if is_mounted {
379 write!(f, " -> {path}", path = self.path)?;
380 }
381 Ok(())
382 }
383}
384
385#[derive(Debug)]
388pub struct EncryptedBlockDevice {
389 pub path: String,
390 pub uuid: Option<String>,
391 mountpoint: Option<String>,
392 label: Option<String>,
393 model: Option<String>,
394 parent: Option<String>,
395}
396
397impl MountParameters for EncryptedBlockDevice {
398 fn mkdir_parameters(&self, username: &str) -> [String; 3] {
399 [
400 MKDIR.to_owned(),
401 "-p".to_owned(),
402 format!("/run/media/{}/{}", username, self.uuid.clone().unwrap()),
403 ]
404 }
405
406 fn mount_parameters(&self, username: &str) -> Vec<String> {
407 vec![
408 MOUNT.to_owned(),
409 format!("/dev/mapper/{}", self.uuid.clone().unwrap()),
410 format!("/run/media/{}/{}", username, self.uuid.clone().unwrap()),
411 ]
412 }
413
414 fn umount_parameters(&self, _username: &str) -> Vec<String> {
415 vec![
416 UDISKSCTL.to_owned(),
417 "unmount".to_owned(),
418 "--block-device".to_owned(),
419 self.path.to_owned(),
420 ]
421 }
422}
423
424impl Display for EncryptedBlockDevice {
425 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
426 write!(
427 f,
428 "{is_mounted}C {path} {label}",
429 is_mounted = if self.is_mounted() { 'M' } else { 'U' },
430 label = self.label_repr(),
431 path = truncate_string(&self.path, 20)
432 )?;
433 if let Some(mountpoint) = &self.mountpoint {
434 write!(f, " -> {mp}", mp = truncate_string(mountpoint, 25))?;
435 }
436 Ok(())
437 }
438}
439
440impl From<BlockDevice> for EncryptedBlockDevice {
441 fn from(device: BlockDevice) -> Self {
442 EncryptedBlockDevice {
443 path: device.path,
444 uuid: device.uuid,
445 mountpoint: device.mountpoint,
446 label: device.label,
447 model: device.model,
448 parent: None,
449 }
450 }
451}
452impl EncryptedBlockDevice {
453 fn set_parent(&mut self, parent_uuid: &Option<String>) {
454 self.parent = parent_uuid.clone()
455 }
456
457 pub fn mount(&self, username: &str, password: &mut PasswordHolder) -> Result<bool> {
458 let success = is_in_path(CRYPTSETUP)
459 && self.set_sudo_session(password)?
460 && self.execute_luks_open(password)?
461 && self.execute_mkdir_crypto(username)?
462 && self.execute_mount_crypto(username)?;
463 drop_sudo_privileges()?;
464 Ok(success)
465 }
466
467 fn set_sudo_session(&self, password: &mut PasswordHolder) -> Result<bool> {
468 if !set_sudo_session(password)? {
469 password.reset();
470 return Ok(false);
471 }
472 Ok(true)
473 }
474
475 fn execute_luks_open(&self, password: &mut PasswordHolder) -> Result<bool> {
476 match execute_sudo_command_with_password(
477 &self.format_luksopen_parameters(),
478 password
479 .cryptsetup()
480 .as_ref()
481 .context("cryptsetup password_holder isn't set")?,
482 std::path::Path::new("/"),
483 ) {
484 Ok((success, stdout, stderr)) => {
485 log_info!("stdout: {stdout}\nstderr: {stderr}");
486 password.reset();
487 Ok(success)
488 }
489 Err(error) => {
490 password.reset();
491 Err(error)
492 }
493 }
494 }
495
496 fn execute_mkdir_crypto(&self, username: &str) -> Result<bool> {
497 let (success, stdout, stderr) =
498 execute_sudo_command_passwordless(&self.mkdir_parameters(username))?;
499 log_info!("stdout: {stdout}\nstderr: {stderr}");
500 Ok(success)
501 }
502
503 fn execute_mount_crypto(&self, username: &str) -> Result<bool> {
504 let (success, stdout, stderr) =
505 execute_sudo_command_passwordless(&self.mount_parameters(username))?;
506 log_info!("stdout: {stdout}\nstderr: {stderr}");
507 Ok(success)
508 }
509
510 pub fn umount_close_crypto(
511 &self,
512 username: &str,
513 password_holder: &mut PasswordHolder,
514 ) -> Result<bool> {
515 let success = is_in_path(CRYPTSETUP)
516 && self.set_sudo_session(password_holder)?
517 && self.execute_umount_crypto(username)?
518 && self.execute_luks_close()?;
519 drop_sudo_privileges()?;
520 password_holder.reset();
521 Ok(success)
522 }
523
524 fn execute_umount_crypto(&self, username: &str) -> Result<bool> {
525 let (success, stdout, stderr) =
526 execute_sudo_command_passwordless(&self.umount_parameters(username))?;
527 if !success {
528 log_info!("stdout: {stdout}\nstderr: {stderr}");
529 }
530 Ok(success)
531 }
532
533 fn execute_luks_close(&self) -> Result<bool> {
534 let (success, stdout, stderr) =
535 execute_sudo_command_passwordless(&self.format_luksclose_parameters())?;
536 if !success {
537 log_info!("stdout: {stdout}\nstderr: {stderr}");
538 }
539 Ok(success)
540 }
541
542 fn format_luksopen_parameters(&self) -> [String; 4] {
543 [
544 CRYPTSETUP.to_owned(),
545 "open".to_owned(),
546 self.path.clone(),
547 self.uuid.clone().unwrap(),
548 ]
549 }
550
551 fn format_luksclose_parameters(&self) -> [String; 3] {
552 [
553 CRYPTSETUP.to_owned(),
554 "close".to_owned(),
555 self.parent.clone().unwrap(),
556 ]
557 }
558
559 const fn is_crypto(&self) -> bool {
560 true
561 }
562
563 fn label_repr(&self) -> &str {
564 if let Some(label) = &self.label {
565 label
566 } else if let Some(model) = &self.model {
567 model
568 } else {
569 ""
570 }
571 }
572
573 fn is_mounted(&self) -> bool {
577 self.mountpoint.is_some()
578 }
579
580 fn symbols(&self) -> String {
581 format!(
582 " {is_mounted}C",
583 is_mounted = if self.is_mounted() { "M" } else { "U" }
584 )
585 }
586}
587
588#[derive(Default, Deserialize, Debug)]
593pub struct BlockDevice {
594 fstype: Option<String>,
595 pub path: String,
596 uuid: Option<String>,
597 mountpoint: Option<String>,
598 name: Option<String>,
599 label: Option<String>,
600 hotplug: bool,
601 model: Option<String>,
602 #[serde(default)]
603 children: Vec<BlockDevice>,
604}
605
606impl BlockDevice {
607 fn device_name(&self) -> String {
608 self.name
609 .clone()
610 .unwrap_or_else(|| self.uuid.as_ref().unwrap().clone())
611 }
612
613 fn mount_no_password(&self) -> Result<bool> {
614 let mut args = self.mount_parameters("");
615 let output = execute_and_output(&args.remove(0), &args)?;
616 Ok(output.status.success())
617 }
618
619 fn umount_no_password(&self) -> Result<bool> {
620 let mut args = self.umount_parameters("");
621 let output = execute_and_output(&args.remove(0), &args)?;
622 Ok(output.status.success())
623 }
624
625 fn is_crypto(&self) -> bool {
626 let Some(fstype) = &self.fstype else {
627 return false;
628 };
629 fstype.contains("crypto")
630 }
631
632 fn is_loop(&self) -> bool {
633 self.path.contains("loop")
634 }
635
636 fn prefix_repr(&self) -> &str {
637 match (self.is_loop(), self.hotplug) {
638 (true, _) => "L",
639 (false, true) => "R",
640 _ => " ",
641 }
642 }
643
644 fn label_repr(&self) -> &str {
645 if let Some(label) = &self.label {
646 label
647 } else if let Some(model) = &self.model {
648 model
649 } else {
650 ""
651 }
652 }
653
654 fn symbols(&self) -> String {
655 format!(
656 " {is_mounted}{prefix}",
657 is_mounted = if self.is_mounted() { "M" } else { "U" },
658 prefix = self.prefix_repr()
659 )
660 }
661
662 pub fn try_power_off(&self) -> Result<bool> {
663 if !self.hotplug && !self.is_mounted() {
664 return Ok(false);
665 }
666 let output = execute_and_output(UDISKSCTL, ["power-off", "-b", &self.path])?;
667 Ok(output.status.success())
668 }
669}
670
671impl MountParameters for BlockDevice {
672 fn mkdir_parameters(&self, username: &str) -> [String; 3] {
673 [
674 MKDIR.to_owned(),
675 "-p".to_owned(),
676 format!("/run/media/{}/{}", username, self.device_name()),
677 ]
678 }
679
680 fn mount_parameters(&self, _username: &str) -> Vec<String> {
681 vec![
682 UDISKSCTL.to_owned(),
683 "mount".to_owned(),
684 "--block-device".to_owned(),
685 self.path.to_owned(),
686 ]
687 }
688
689 fn umount_parameters(&self, _username: &str) -> Vec<String> {
690 vec![
691 UDISKSCTL.to_owned(),
692 "unmount".to_owned(),
693 "--block-device".to_owned(),
694 self.path.to_owned(),
695 ]
696 }
697}
698
699impl MountCommands for BlockDevice {
700 fn is_mounted(&self) -> bool {
704 self.mountpoint.is_some()
705 }
706
707 fn mount(&mut self, username: &str, password: &mut PasswordHolder) -> Result<bool> {
708 let success = set_sudo_session(password)?;
710 password.reset();
711 if !success {
712 return Ok(false);
713 }
714 let args_sudo = self.mount_parameters(username);
716 let (success, stdout, stderr) = execute_sudo_command_passwordless(&args_sudo)?;
717 if !success {
718 log_info!("stdout: {stdout}\nstderr: {stderr}");
719 return Ok(false);
720 }
721 if !success {
722 reset_sudo_faillock()?;
723 }
724 Ok(success)
725 }
726
727 fn umount(&mut self, username: &str, password: &mut PasswordHolder) -> Result<bool> {
728 let success = set_sudo_session(password)?;
729 password.reset();
730 if !success {
731 return Ok(false);
732 }
733 let (success, stdout, stderr) =
734 execute_sudo_command_passwordless(&self.umount_parameters(username))?;
735 if !success {
736 log_info!("stdout: {stdout}\nstderr: {stderr}");
737 }
738 Ok(success)
739 }
740}
741
742impl Display for BlockDevice {
743 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
744 write!(
745 f,
746 "{is_mounted}{prefix} {path} {label}",
747 is_mounted = if self.is_mounted() { "M" } else { "U" },
748 prefix = self.prefix_repr(),
749 label = self.label_repr(),
750 path = self.path
751 )?;
752 if let Some(mountpoint) = &self.mountpoint {
753 write!(f, " -> {mountpoint}")?;
754 }
755 Ok(())
756 }
757}
758
759#[derive(Debug)]
761pub struct RemoteDevice {
762 name: String,
763 mountpoint: String,
764}
765
766impl RemoteDevice {
767 fn new<S, T>(name: S, mountpoint: T) -> Self
768 where
769 S: Into<String>,
770 T: Into<String>,
771 {
772 Self {
773 name: name.into(),
774 mountpoint: mountpoint.into(),
775 }
776 }
777
778 const fn is_mounted(&self) -> bool {
779 true
780 }
781
782 fn symbols(&self) -> String {
783 " MR".to_string()
784 }
785}
786
787#[derive(Debug)]
789pub enum Mountable {
790 Device(BlockDevice),
791 Encrypted(EncryptedBlockDevice),
792 MTP(Mtp),
793 Remote(RemoteDevice),
794 Network(NetworkMount),
795}
796impl Display for Mountable {
797 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
798 match &self {
799 Self::Device(device) => write!(f, "{device}"),
800 Self::Encrypted(device) => write!(f, "{device}"),
801 Self::MTP(device) => write!(f, "{device}"),
802 Self::Network(device) => write!(f, "{device}"),
803 Self::Remote(RemoteDevice { name, mountpoint }) => {
804 write!(f, "MS {name} -> {mountpoint}",)
805 }
806 }
807 }
808}
809
810impl Mountable {
811 pub fn is_crypto(&self) -> bool {
812 match &self {
813 Self::Device(device) => device.is_crypto(),
814 Self::Encrypted(device) => device.is_crypto(),
815 Self::Network(_) => false,
816 Self::MTP(_) => false,
817 Self::Remote(_) => false,
818 }
819 }
820
821 pub fn is_mounted(&self) -> bool {
822 match &self {
823 Self::Device(device) => device.is_mounted(),
824 Self::Encrypted(device) => device.is_mounted(),
825 Self::MTP(device) => device.is_mounted(),
826 Self::Network(_) => true,
827 Self::Remote(device) => device.is_mounted(),
828 }
829 }
830
831 fn path(&self) -> &str {
832 match &self {
833 Self::Device(device) => device.path.as_str(),
834 Self::Encrypted(device) => device.path.as_str(),
835 Self::MTP(device) => device.path.as_str(),
836 Self::Network(device) => device.path.as_str(),
837 Self::Remote(RemoteDevice {
838 name: _,
839 mountpoint,
840 }) => mountpoint.as_str(),
841 }
842 }
843
844 pub fn path_repr(&self) -> String {
845 truncate_string(self.path(), 25)
846 }
847
848 fn mountpoint(&self) -> Option<&str> {
849 match self {
850 Mountable::Device(device) => device.mountpoint.as_deref(),
851 Mountable::Encrypted(device) => device.mountpoint.as_deref(),
852 Mountable::MTP(device) => Some(&device.path),
853 Mountable::Network(device) => Some(&device.mountpoint),
854 Mountable::Remote(RemoteDevice {
855 name: _,
856 mountpoint,
857 }) => Some(mountpoint),
858 }
859 }
860
861 pub fn mountpoint_repr(&self) -> &str {
862 self.mountpoint().unwrap_or_default()
863 }
864
865 pub fn symbols(&self) -> String {
866 match &self {
867 Self::Device(device) => device.symbols(),
868 Self::Encrypted(device) => device.symbols(),
869 Self::MTP(device) => device.symbols(),
870 Self::Network(device) => device.symbols(),
871 Self::Remote(device) => device.symbols(),
872 }
873 }
874
875 pub fn label(&self) -> String {
876 match self {
877 Self::Device(device) => device.label_repr().to_string(),
878 Self::Encrypted(device) => device.label_repr().to_string(),
879 Self::MTP(_) => "".to_string(),
880 Self::Network(device) => device.kind.to_string(),
881 Self::Remote(_) => "".to_string(),
882 }
883 }
884}
885
886impl CowStr for Mountable {
887 fn cow_str(&self) -> Cow<str> {
888 self.to_string().into()
889 }
890}
891struct MountBuilder;
892
893impl MountBuilder {
894 fn build_from_json() -> Result<Vec<Mountable>> {
895 let json_content = get_devices_json()?;
896 match Self::from_json(json_content) {
897 Ok(content) => Ok(content),
898 Err(e) => {
899 log_info!("update error {e:#?}");
900 Ok(vec![])
901 }
902 }
903 }
904
905 fn from_json(json_content: String) -> Result<Vec<Mountable>, Box<dyn std::error::Error>> {
906 let devices: Vec<BlockDevice> = Self::read_blocks_from_json(json_content)?;
907 let mut content = vec![];
908 for parent in devices.into_iter() {
909 let is_crypto = parent.is_crypto();
910 if !parent.children.is_empty() {
911 Self::push_children(is_crypto, &mut content, parent);
912 } else if parent.uuid.is_some() {
913 Self::push_parent(is_crypto, &mut content, parent)
914 }
915 }
916 Ok(content)
917 }
918
919 fn read_blocks_from_json(
920 json_content: String,
921 ) -> Result<Vec<BlockDevice>, Box<dyn std::error::Error>> {
922 let mut value: Value = from_str(&json_content)?;
923
924 let blockdevices_value: Value = value
925 .get_mut("blockdevices")
926 .ok_or("Missing 'blockdevices' field in JSON")?
927 .take();
928 Ok(from_value(blockdevices_value)?)
929 }
930
931 fn push_children(is_crypto: bool, content: &mut Vec<Mountable>, parent: BlockDevice) {
932 for mut children in parent.children.into_iter() {
933 if is_crypto {
934 let mut encrypted_children: EncryptedBlockDevice = children.into();
935 encrypted_children.set_parent(&parent.uuid);
936 content.push(Mountable::Encrypted(encrypted_children));
937 } else {
938 children.model = parent.model.clone();
939 content.push(Mountable::Device(children));
940 }
941 }
942 }
943
944 fn push_parent(is_crypto: bool, content: &mut Vec<Mountable>, parent: BlockDevice) {
945 if is_crypto {
946 content.push(Mountable::Encrypted(parent.into()))
947 } else {
948 content.push(Mountable::Device(parent))
949 }
950 }
951
952 fn extend_with_remote(content: &mut Vec<Mountable>, disks: &Disks) {
953 content.extend(
954 disks
955 .iter()
956 .filter(|d| d.file_system().to_string_lossy().contains("sshfs"))
957 .map(|d| {
958 Mountable::Remote(RemoteDevice::new(
959 d.name().to_string_lossy(),
960 d.mount_point().to_string_lossy(),
961 ))
962 })
963 .collect::<Vec<_>>(),
964 );
965 }
966
967 fn extend_with_network(content: &mut Vec<Mountable>) -> Result<()> {
968 content.extend(Self::get_network_devices()?);
969 Ok(())
970 }
971
972 fn get_network_devices() -> io::Result<Vec<Mountable>> {
973 let reader = BufReader::new(File::open("/proc/self/mountinfo")?);
974 let mut network_mountables = vec![];
975
976 for line in reader.lines() {
977 let Some(network_mount) = NetworkMount::from_network_line(line) else {
978 continue;
979 };
980 network_mountables.push(Mountable::Network(network_mount));
981 }
982 Ok(network_mountables)
983 }
984
985 fn extend_with_mtp_from_gio(content: &mut Vec<Mountable>) {
986 if !is_in_path(GIO) {
987 return;
988 }
989 let Ok(output) = execute_and_output(GIO, [MOUNT, "-li"]) else {
990 return;
991 };
992 let Ok(stdout) = String::from_utf8(output.stdout) else {
993 return;
994 };
995
996 content.extend(
997 stdout
998 .lines()
999 .filter(|line| line.contains("activation_root"))
1000 .map(Mtp::from_gio)
1001 .filter_map(std::result::Result::ok)
1002 .map(Mountable::MTP),
1003 )
1004 }
1005}
1006
1007#[derive(Debug, Default)]
1009pub struct Mount {
1010 pub content: Vec<Mountable>,
1011 index: usize,
1012}
1013
1014impl Mount {
1015 const WIDTHS: [Constraint; 5] = [
1016 Constraint::Length(2),
1017 Constraint::Length(3),
1018 Constraint::Max(28),
1019 Constraint::Length(10),
1020 Constraint::Min(1),
1021 ];
1022
1023 pub fn update(&mut self, disks: &Disks) -> Result<()> {
1024 self.index = 0;
1025
1026 self.content = MountBuilder::build_from_json()?;
1027 MountBuilder::extend_with_remote(&mut self.content, disks);
1028 MountBuilder::extend_with_mtp_from_gio(&mut self.content);
1029 MountBuilder::extend_with_network(&mut self.content)?;
1030
1031 #[cfg(debug_assertions)]
1032 log_info!("{self:#?}");
1033 Ok(())
1034 }
1035
1036 pub fn umount_selected_no_password(&mut self) -> Result<bool> {
1037 match &mut self.content[self.index] {
1038 Mountable::Device(device) => device.umount_no_password(),
1039 Mountable::Encrypted(_device) => {
1040 unreachable!("Encrypted devices can't be unmounted without password.")
1041 }
1042 Mountable::MTP(device) => device.umount(),
1043 Mountable::Network(_device) => Ok(false),
1044 Mountable::Remote(RemoteDevice {
1045 name: _,
1046 mountpoint,
1047 }) => umount_remote_no_password(mountpoint),
1048 }
1049 }
1050
1051 pub fn selected_mount_point(&self) -> Option<PathBuf> {
1052 Some(PathBuf::from(self.selected()?.mountpoint()?))
1053 }
1054
1055 pub fn mount_selected_no_password(&mut self) -> Result<bool> {
1056 match &mut self.content[self.index] {
1057 Mountable::Device(device) => device.mount_no_password(),
1058 Mountable::Encrypted(_device) => {
1059 unreachable!("Encrypted devices can't be mounted without password.")
1060 }
1061 Mountable::MTP(device) => device.mount(),
1062 Mountable::Network(_) => Ok(false),
1063 Mountable::Remote(_) => Ok(false),
1064 }
1065 }
1066
1067 pub fn mount_selected(&mut self, password_holder: &mut PasswordHolder) -> Result<bool> {
1069 let success = match &mut self.content[self.index] {
1070 Mountable::Device(device) => device.mount(¤t_username()?, password_holder)?,
1071 Mountable::Encrypted(_device) => {
1072 unreachable!("EncryptedBlockDevice should impl its own method")
1073 }
1074 Mountable::MTP(device) => device.mount()?,
1075 Mountable::Network(_) => false,
1076 Mountable::Remote(_) => false,
1077 };
1078
1079 password_holder.reset();
1080 drop_sudo_privileges()?;
1081 Ok(success)
1082 }
1083
1084 pub fn umount_selected(&mut self, password_holder: &mut PasswordHolder) -> Result<()> {
1085 let username = current_username()?;
1086 let success = match &mut self.content[self.index] {
1087 Mountable::Device(device) => device.umount(&username, password_holder)?,
1088 Mountable::MTP(device) => device.umount()?,
1089 Mountable::Network(device) => device.umount(password_holder)?,
1090 Mountable::Encrypted(_device) => {
1091 unreachable!("EncryptedBlockDevice should impl its own method")
1092 }
1093 Mountable::Remote(RemoteDevice {
1094 name: _,
1095 mountpoint,
1096 }) => umount_remote(mountpoint, password_holder)?,
1097 };
1098 if !success {
1099 reset_sudo_faillock()?
1100 }
1101 password_holder.reset();
1102 drop_sudo_privileges()?;
1103 Ok(())
1104 }
1105
1106 pub fn eject_removable_device(&self) -> Result<bool> {
1107 let Some(Mountable::Device(device)) = &self.selected() else {
1108 return Ok(false);
1109 };
1110 device.try_power_off()
1111 }
1112
1113 pub fn find_encrypted_by_uuid(&self, parent_uuid: Option<String>) -> Option<String> {
1116 for device in self.content() {
1117 let Mountable::Encrypted(device) = device else {
1118 continue;
1119 };
1120 if device.parent == parent_uuid && device.is_mounted() {
1121 return device.mountpoint.clone();
1122 }
1123 }
1124 None
1125 }
1126
1127 fn header() -> Row<'static> {
1128 let header_style = MENU_STYLES
1129 .get()
1130 .expect("Menu colors should be set")
1131 .palette_4
1132 .fg
1133 .unwrap_or(Color::Rgb(0, 0, 0));
1134 Row::new([
1135 Cell::from(""),
1136 Cell::from("sym"),
1137 Cell::from("path"),
1138 Cell::from("label"),
1139 Cell::from("mountpoint"),
1140 ])
1141 .style(header_style)
1142 }
1143
1144 fn row<'a>(&self, index: usize, item: &'a Mountable, style: Style) -> Row<'a> {
1145 let bind = Cell::from(format!("{bind:2<}", bind = index + 1));
1146 let symbols = Cell::from(Text::from(item.symbols()));
1147 let path = Cell::from(Text::from(item.path_repr()));
1148 let label = Cell::from(Text::from(item.label()));
1149 let mountpoint = Cell::from(Text::from(item.mountpoint_repr()));
1150 Row::new([bind, symbols, path, label, mountpoint]).style(self.style(index, &style))
1151 }
1152}
1153
1154fn umount_remote(mountpoint: &str, password_holder: &mut PasswordHolder) -> Result<bool> {
1155 let success = set_sudo_session(password_holder)?;
1156 password_holder.reset();
1157 if !success {
1158 return Ok(false);
1159 }
1160 let (success, stdout, stderr) = execute_sudo_command_passwordless(&[UMOUNT, mountpoint])?;
1161 if !success {
1162 log_info!(
1163 "umount remote failed:\nstdout: {}\nstderr: {}",
1164 stdout,
1165 stderr
1166 );
1167 }
1168
1169 Ok(success)
1170}
1171
1172fn umount_remote_no_password(mountpoint: &str) -> Result<bool> {
1173 let output = execute_and_output(UMOUNT, [mountpoint])?;
1174 let success = output.status.success();
1175 if !success {
1176 log_info!(
1177 "umount {mountpoint}:\nstdout: {stdout}\nstderr: {stderr}",
1178 stdout = String::from_utf8(output.stdout)?,
1179 stderr = String::from_utf8(output.stderr)?,
1180 );
1181 }
1182 Ok(success)
1183}
1184
1185pub fn lsblk_and_udisksctl_installed() -> bool {
1188 is_in_path(LSBLK) && is_in_path(UDISKSCTL)
1189}
1190
1191fn get_devices_json() -> Result<String> {
1192 Ok(String::from_utf8(
1193 execute_and_output(
1194 LSBLK,
1195 [
1196 "--json",
1197 "-o",
1198 "FSTYPE,PATH,UUID,MOUNTPOINT,NAME,LABEL,HOTPLUG,MODEL",
1199 ],
1200 )?
1201 .stdout,
1202 )?)
1203}
1204
1205fn truncate_string<S: AsRef<str>>(input: S, max_length: usize) -> String {
1206 if input.as_ref().chars().count() > max_length {
1207 let truncated: String = input.as_ref().chars().take(max_length).collect();
1208 format!("{}...", truncated)
1209 } else {
1210 input.as_ref().to_string()
1211 }
1212}
1213impl_selectable!(Mount);
1214impl_content!(Mount, Mountable);
1215
1216impl DrawMenu<Mountable> for Mount {
1217 fn draw_menu(&self, f: &mut Frame, rect: &Rect, window: &ContentWindow) {
1218 let mut p_rect = rect.offseted(2, 3);
1219 p_rect.height = p_rect.height.saturating_sub(2);
1220 p_rect.width = p_rect.width.saturating_sub(2);
1221
1222 let content = self.content();
1223 let table = Table::new(
1224 colored_skip_take!(content, window)
1225 .map(|(index, item, style)| self.row(index, item, style)),
1226 Self::WIDTHS,
1227 )
1228 .header(Self::header());
1229 f.render_widget(table, p_rect);
1230 }
1231}