Skip to main content

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/// What kind of fs networks do fm support ?
182/// Currently only NFS & CIFS.
183#[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/// A mounted device from a remote location.
211/// Only NTFS & CIFS are supported ATM.
212#[derive(Debug)]
213pub struct NetworkMount {
214    pub kind: NetworkKind,
215    pub path: String,
216    pub mountpoint: String,
217}
218
219/// Holds a network mount point.
220impl NetworkMount {
221    /// Returns a `NetWorkMount` parsed from a line of /proc/self/mountinfo
222    /// 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
223    /// 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
224    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/// Holds a MTP device name, a path and a flag set to true
276/// if the device is mounted.
277#[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    /// Creates a `Removable` instance from a filtered `gio` command output.
287    ///
288    /// `gio mount -l`  will return a lot of information about mount points,
289    /// including MTP (aka Android) devices.
290    /// We don't check if the device actually exists, we just create the instance.
291    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    /// Format itself as a valid `gio mount $device` argument.
313    fn format_for_gio(&self) -> String {
314        format!("mtp://{name}", name = self.name)
315    }
316
317    /// True if the device is mounted
318    fn is_mounted(&self) -> bool {
319        self.is_mounted
320    }
321
322    /// Mount a non mounted removable device.
323    /// `Err` if the device is already mounted.
324    /// Runs a `gio mount $name` command and check
325    /// the result.
326    /// The `is_mounted` flag is updated accordingly to the result.
327    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    /// Unount a mounted removable device.
344    /// `Err` if the device isnt mounted.
345    /// Runs a `gio mount $device_name` command and check
346    /// the result.
347    /// The `is_mounted` flag is updated accordingly to the result.
348    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/// Encrypted devices which can be mounted.
388/// Mounting an encrypted device requires a password.
389#[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    /// True if there's a mount point for this drive.
576    /// It's only valid if we mounted the device since it requires
577    /// the uuid to be in the mount point.
578    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/// A device which can be mounted.
591/// Default "mountable" struct for any kind of device **except** encrypted devices.
592/// They require special methods to be mounted since it requires a password which
593/// can't be provided from here.
594#[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    /// True if there's a mount point for this drive.
703    /// It's only valid if we mounted the device since it requires
704    /// the uuid to be in the mount point.
705    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        // sudo
711        let success = set_sudo_session(password)?;
712        password.reset();
713        if !success {
714            return Ok(false);
715        }
716        // mount
717        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/// A mounted partition using sshfs.
762#[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/// A mountable device which can be of many forms.
790#[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/// Holds the mountable devices.
1010#[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    /// Open and mount the selected device.
1070    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(&current_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    /// We receive the uuid of the _parent_ and must compare it to the parent of the device.
1116    /// Returns the mountpoint of the found device, if any.
1117    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
1187/// True iff `lsblk` and `udisksctl` are in path.
1188/// Nothing here can be done without those programs.
1189pub 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}