Skip to main content

egui_file_dialog/data/
disks.rs

1#[cfg(target_os = "macos")]
2use std::fs;
3use std::path::{Path, PathBuf};
4
5/// Wrapper above the `sysinfo::Disk` struct.
6/// Used for helper functions and so that more flexibility is guaranteed in the future if
7/// the names of the disks are generated dynamically.
8#[derive(Default, Debug, Clone, PartialEq, Eq)]
9pub struct Disk {
10    mount_point: PathBuf,
11    display_name: String,
12    is_removable: bool,
13}
14
15impl Disk {
16    /// Creates a new disk with the given name and mount point
17    pub fn new(
18        name: Option<&str>,
19        mount_point: &Path,
20        is_removable: bool,
21        canonicalize_paths: bool,
22    ) -> Self {
23        Self {
24            mount_point: canonicalize(mount_point, canonicalize_paths),
25            display_name: gen_display_name(
26                name.unwrap_or_default(),
27                mount_point.to_str().unwrap_or_default(),
28            ),
29            is_removable,
30        }
31    }
32
33    /// Create a new Disk object based on the data of a `sysinfo::Disk`.
34    pub fn from_sysinfo_disk(disk: &sysinfo::Disk, canonicalize_paths: bool) -> Self {
35        Self::new(
36            disk.name().to_str(),
37            disk.mount_point(),
38            disk.is_removable(),
39            canonicalize_paths,
40        )
41    }
42
43    /// Create a new Disk object based on its path (macos only)
44    #[cfg(target_os = "macos")]
45    pub fn from_path(path: &Path, canonicalize_paths: bool) -> Self {
46        let mount_point = canonicalize(path, canonicalize_paths);
47
48        // Use the directory name as the display name.
49        let display_name = path.file_name().map_or_else(
50            || "Unknown".to_string(),
51            |name| name.to_string_lossy().to_string(),
52        );
53
54        // Check if the path corresponds to a removable disk.
55        // This is a best guess as this information might not be available.
56        let is_removable = false; // Network drives or `/Volumes` entries don't have a clear removable flag.
57
58        Self {
59            mount_point,
60            display_name,
61            is_removable,
62        }
63    }
64
65    /// Returns the mount point of the disk
66    pub fn mount_point(&self) -> &Path {
67        &self.mount_point
68    }
69
70    /// Returns the display name of the disk
71    pub fn display_name(&self) -> &str {
72        &self.display_name
73    }
74
75    /// Returns true if the disk is removable
76    pub const fn is_removable(&self) -> bool {
77        self.is_removable
78    }
79}
80
81/// Wrapper above the `sysinfo::Disks` struct
82#[derive(Default, Debug)]
83pub struct Disks {
84    disks: Vec<Disk>,
85}
86
87impl Disks {
88    /// Create a new set of disks
89    pub const fn new(disks: Vec<Disk>) -> Self {
90        Self { disks }
91    }
92
93    /// Queries the operating system for disks
94    pub fn new_native_disks(canonicalize_paths: bool) -> Self {
95        Self {
96            disks: load_disks(canonicalize_paths),
97        }
98    }
99
100    /// Creates an empty list of Disks
101    pub const fn new_empty() -> Self {
102        Self { disks: Vec::new() }
103    }
104
105    /// Very simple wrapper method of the disks `.iter()` method.
106    /// No trait is implemented since this is currently only used internal.
107    pub(crate) fn iter(&self) -> std::slice::Iter<'_, Disk> {
108        self.disks.iter()
109    }
110}
111
112impl<'a> IntoIterator for &'a Disks {
113    type IntoIter = std::slice::Iter<'a, Disk>;
114    type Item = &'a Disk;
115    fn into_iter(self) -> Self::IntoIter {
116        self.iter()
117    }
118}
119
120/// Canonicalizes the given path.
121/// Returns the input path in case of an error.
122fn canonicalize(path: &Path, canonicalize: bool) -> PathBuf {
123    if canonicalize {
124        dunce::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
125    } else {
126        path.to_path_buf()
127    }
128}
129
130#[cfg(windows)]
131fn gen_display_name(name: &str, mount_point: &str) -> String {
132    let mount_point = mount_point.replace('\\', "");
133
134    // Try using the mount point as the display name if the specified name
135    // from sysinfo::Disk is empty or contains invalid characters
136    if name.is_empty() {
137        return mount_point;
138    }
139
140    format!("{name} ({mount_point})")
141}
142
143#[cfg(not(windows))]
144fn gen_display_name(name: &str, mount_point: &str) -> String {
145    // Try using the mount point as the display name if the specified name
146    // from sysinfo::Disk is empty or contains invalid characters
147    if name.is_empty() {
148        return mount_point.to_string();
149    }
150
151    name.to_string()
152}
153
154#[cfg(windows)]
155fn load_disks(canonicalize_paths: bool) -> Vec<Disk> {
156    #![allow(unused_mut)]
157    let mut disks: Vec<Disk> = sysinfo::Disks::new_with_refreshed_list()
158        .iter()
159        .map(|d| Disk::from_sysinfo_disk(d, canonicalize_paths))
160        .collect();
161
162    #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
163    {
164        // `sysinfo::Disks` currently do not include mapped network drives on Windows.
165        // We will load all other available drives using the Windows API.
166        // However, the sysinfo disks have priority, we are just adding to the list.
167        #[allow(unsafe_code)]
168        let mut drives = unsafe { GetLogicalDrives() };
169        let mut letter = b'A';
170
171        while drives > 0 {
172            if drives & 1 != 0 {
173                let path = PathBuf::from(format!("{}:\\", letter as char));
174                let mount_point = canonicalize(&path, canonicalize_paths);
175
176                if !disks.iter().any(|d| d.mount_point == mount_point) {
177                    disks.push(Disk::new(None, &path, false, canonicalize_paths));
178                }
179            }
180
181            drives >>= 1;
182            letter += 1;
183        }
184    }
185
186    disks
187}
188
189#[cfg(all(windows, any(target_arch = "x86_64", target_arch = "aarch64")))]
190extern "C" {
191    pub fn GetLogicalDrives() -> u32;
192}
193
194#[cfg(all(not(windows), not(target_os = "macos")))]
195fn load_disks(canonicalize_paths: bool) -> Vec<Disk> {
196    sysinfo::Disks::new_with_refreshed_list()
197        .iter()
198        .map(|d| Disk::from_sysinfo_disk(d, canonicalize_paths))
199        .collect()
200}
201
202// On macOS, add volumes from `/Volumes`
203#[cfg(target_os = "macos")]
204fn load_disks(canonicalize_paths: bool) -> Vec<Disk> {
205    let mut result = Vec::new();
206    let mut seen_mount_points = std::collections::HashSet::new();
207
208    // Collect disks from sysinfo
209    for disk in &sysinfo::Disks::new_with_refreshed_list() {
210        let mount_point = disk.mount_point();
211        if mount_point != Path::new("/")
212            && seen_mount_points.insert(mount_point.to_path_buf())
213            && disk.mount_point() != Path::new("/System/Volumes/Data")
214        {
215            result.push(Disk::from_sysinfo_disk(disk, canonicalize_paths));
216        }
217    }
218
219    // Collect volumes from /Volumes
220    if let Ok(entries) = fs::read_dir("/Volumes") {
221        for entry in entries.filter_map(Result::ok) {
222            let path = entry.path();
223            if seen_mount_points.insert(path.clone()) {
224                if let Some(name_osstr) = path.file_name() {
225                    if let Some(name) = name_osstr.to_str() {
226                        if path.is_dir() && !name.starts_with('.') {
227                            result.push(Disk::from_path(&path, canonicalize_paths));
228                        }
229                    }
230                }
231            }
232        }
233    }
234
235    result
236}