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    let mut disks: Vec<Disk> = sysinfo::Disks::new_with_refreshed_list()
157        .iter()
158        .map(|d| Disk::from_sysinfo_disk(d, canonicalize_paths))
159        .collect();
160
161    // `sysinfo::Disks` currently do not include mapped network drives on Windows.
162    // We will load all other available drives using the Windows API.
163    // However, the sysinfo disks have priority, we are just adding to the list.
164    #[allow(unsafe_code)]
165    let mut drives = unsafe { GetLogicalDrives() };
166    let mut letter = b'A';
167
168    while drives > 0 {
169        if drives & 1 != 0 {
170            let path = PathBuf::from(format!("{}:\\", letter as char));
171            let mount_point = canonicalize(&path, canonicalize_paths);
172
173            if !disks.iter().any(|d| d.mount_point == mount_point) {
174                disks.push(Disk::new(None, &path, false, canonicalize_paths));
175            }
176        }
177
178        drives >>= 1;
179        letter += 1;
180    }
181
182    disks
183}
184
185#[cfg(windows)]
186extern "C" {
187    pub fn GetLogicalDrives() -> u32;
188}
189
190#[cfg(all(not(windows), not(target_os = "macos")))]
191fn load_disks(canonicalize_paths: bool) -> Vec<Disk> {
192    sysinfo::Disks::new_with_refreshed_list()
193        .iter()
194        .map(|d| Disk::from_sysinfo_disk(d, canonicalize_paths))
195        .collect()
196}
197
198// On macOS, add volumes from `/Volumes`
199#[cfg(target_os = "macos")]
200fn load_disks(canonicalize_paths: bool) -> Vec<Disk> {
201    let mut result = Vec::new();
202    let mut seen_mount_points = std::collections::HashSet::new();
203
204    // Collect disks from sysinfo
205    for disk in &sysinfo::Disks::new_with_refreshed_list() {
206        let mount_point = disk.mount_point();
207        if mount_point != Path::new("/")
208            && seen_mount_points.insert(mount_point.to_path_buf())
209            && disk.mount_point() != Path::new("/System/Volumes/Data")
210        {
211            result.push(Disk::from_sysinfo_disk(disk, canonicalize_paths));
212        }
213    }
214
215    // Collect volumes from /Volumes
216    if let Ok(entries) = fs::read_dir("/Volumes") {
217        for entry in entries.filter_map(Result::ok) {
218            let path = entry.path();
219            if seen_mount_points.insert(path.clone()) {
220                if let Some(name_osstr) = path.file_name() {
221                    if let Some(name) = name_osstr.to_str() {
222                        if path.is_dir() && !name.starts_with('.') {
223                            result.push(Disk::from_path(&path, canonicalize_paths));
224                        }
225                    }
226                }
227            }
228        }
229    }
230
231    result
232}