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