fm/modes/menu/
mount.rs

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/// Used to mount an iso file as a loop device.
36/// Holds info about its source (`path`) and optional mountpoint (`mountpoints`).
37/// Since it's used once and nothing can be done with it after mounting, it's dropped as soon as possible.
38#[derive(Debug, Clone, Default)]
39pub struct IsoDevice {
40    /// The source, aka the iso file itself.
41    pub path: String,
42    /// None when creating, updated once the device is mounted.
43    pub mountpoints: Option<String>,
44    is_mounted: bool,
45}
46
47impl IsoDevice {
48    const FILENAME: &'static str = "fm_iso";
49
50    /// Creates a new instance from an iso file path.
51    #[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        // sudo
114        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        // sudo
135        let success = set_sudo_session(password)?;
136        password.reset();
137        if !success {
138            return Ok(false);
139        }
140        // mkdir
141        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            // mount
150            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/// Possible actions on mountable devices
175#[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/// A mounted device from a remote location.
209/// Only NTFS & CIFS are supported ATM.
210#[derive(Debug)]
211pub struct NetworkMount {
212    pub kind: NetworkKind,
213    pub path: String,
214    pub mountpoint: String,
215}
216
217/// Holds a network mount point.
218impl NetworkMount {
219    /// Returns a `NetWorkMount` parsed from a line of /proc/self/mountinfo
220    /// 96 29 0:60 / /home/user/nfs rw,relatime shared:523 - nfs4 hostname:/remote/path rw,vers=4.2,rsize=524288,wsize=524288,namlen=255,hard,proto=tcp,timeo=900,retrans=5,sec=sys,clientaddr=192.168.1.17,local_lock=none,addr=remote_ip
221    /// 483 29 0:73 / /home/user/cifs rw,relatime shared:424 - cifs //ip_adder/qnas rw,vers=3.1.1,cache=strict,username=quentin,uid=0,noforceuid,gid=0,noforcegid,addr=yout_ip,file_mode=0755,dir_mode=0755,soft,nounix,serverino,mapposix,reparse=nfs,rsize=4194304,wsize=4194304,bsize=1048576,retrans=1,echo_interval=60,actimeo=1,closetimeo=1
222    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/// Holds a MTP device name, a path and a flag set to true
274/// if the device is mounted.
275#[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    /// Creates a `Removable` instance from a filtered `gio` command output.
285    ///
286    /// `gio mount -l`  will return a lot of information about mount points,
287    /// including MTP (aka Android) devices.
288    /// We don't check if the device actually exists, we just create the instance.
289    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    /// Format itself as a valid `gio mount $device` argument.
311    fn format_for_gio(&self) -> String {
312        format!("mtp://{name}", name = self.name)
313    }
314
315    /// True if the device is mounted
316    fn is_mounted(&self) -> bool {
317        self.is_mounted
318    }
319
320    /// Mount a non mounted removable device.
321    /// `Err` if the device is already mounted.
322    /// Runs a `gio mount $name` command and check
323    /// the result.
324    /// The `is_mounted` flag is updated accordingly to the result.
325    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    /// Unount a mounted removable device.
342    /// `Err` if the device isnt mounted.
343    /// Runs a `gio mount $device_name` command and check
344    /// the result.
345    /// The `is_mounted` flag is updated accordingly to the result.
346    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/// Encrypted devices which can be mounted.
386/// Mounting an encrypted device requires a password.
387#[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    /// True if there's a mount point for this drive.
574    /// It's only valid if we mounted the device since it requires
575    /// the uuid to be in the mount point.
576    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/// A device which can be mounted.
589/// Default "mountable" struct for any kind of device **except** encrypted devices.
590/// They require special methods to be mounted since it requires a password which
591/// can't be provided from here.
592#[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    /// True if there's a mount point for this drive.
701    /// It's only valid if we mounted the device since it requires
702    /// the uuid to be in the mount point.
703    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        // sudo
709        let success = set_sudo_session(password)?;
710        password.reset();
711        if !success {
712            return Ok(false);
713        }
714        // mount
715        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/// A mounted partition using sshfs.
760#[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/// A mountable device which can be of many forms.
788#[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/// Holds the mountable devices.
1008#[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    /// Open and mount the selected device.
1068    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(&current_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    /// We receive the uuid of the _parent_ and must compare it to the parent of the device.
1114    /// Returns the mountpoint of the found device, if any.
1115    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
1185/// True iff `lsblk` and `cryptsetup` are in path.
1186/// Nothing here can be done without those programs.
1187pub 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}