1use std::path::{Path, PathBuf};
2
3use sysinfo::Disks;
4
5use crate::config::DriveId;
6
7#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
10pub struct ConnectedDrive {
11 pub name: String,
12 pub mount_point: PathBuf,
13 pub is_removable: bool,
14 pub total_space: u64,
15 pub available_space: u64,
16}
17
18pub fn list_connected_drives() -> Vec<ConnectedDrive> {
22 let disks = Disks::new_with_refreshed_list();
23 disks
24 .iter()
25 .map(|d| ConnectedDrive {
26 name: d.name().to_string_lossy().to_string(),
27 mount_point: d.mount_point().to_path_buf(),
28 is_removable: d.is_removable(),
29 total_space: d.total_space(),
30 available_space: d.available_space(),
31 })
32 .collect()
33}
34
35pub fn same_drive(a: &Path, b: &Path) -> bool {
38 let mount_a = find_mount_point(a);
39 let mount_b = find_mount_point(b);
40 match (mount_a, mount_b) {
41 (Some(ma), Some(mb)) => ma == mb,
42 _ => root_of(a) == root_of(b),
44 }
45}
46
47pub fn get_drive_id(path: &Path) -> Option<DriveId> {
50 let disks = Disks::new_with_refreshed_list();
51 let canonical = strip_unc_prefix(&path.canonicalize().ok()?);
52
53 let disk = disks
54 .iter()
55 .filter(|d| canonical.starts_with(d.mount_point()))
56 .max_by_key(|d| d.mount_point().as_os_str().len())?;
57
58 let raw_name = disk.name().to_string_lossy().to_string();
59 let label = if raw_name.is_empty() { None } else { Some(raw_name) };
60 let uuid = get_volume_uuid(disk.mount_point());
61
62 if label.is_none() && uuid.is_none() {
64 return None;
65 }
66
67 Some(DriveId { label, uuid })
68}
69
70pub fn find_mounted_drive(id: &DriveId) -> Option<PathBuf> {
74 let disks = Disks::new_with_refreshed_list();
75
76 for disk in disks.iter() {
77 let name = disk.name().to_string_lossy().to_string();
78 let disk_uuid = get_volume_uuid(disk.mount_point());
79
80 let label_match = id
81 .label
82 .as_ref()
83 .map(|l| !l.is_empty() && l == &name)
84 .unwrap_or(false);
85
86 let uuid_match = id
87 .uuid
88 .as_ref()
89 .zip(disk_uuid.as_ref())
90 .map(|(a, b)| a == b)
91 .unwrap_or(false);
92
93 if label_match || uuid_match {
94 return Some(disk.mount_point().to_path_buf());
95 }
96 }
97
98 None
99}
100
101fn find_mount_point(path: &Path) -> Option<PathBuf> {
104 let canonical = path.canonicalize().ok()?;
105 let canonical = strip_unc_prefix(&canonical);
108 let disks = Disks::new_with_refreshed_list();
109 disks
110 .iter()
111 .filter(|d| canonical.starts_with(d.mount_point()))
112 .max_by_key(|d| d.mount_point().as_os_str().len())
113 .map(|d| d.mount_point().to_path_buf())
114}
115
116fn strip_unc_prefix(path: &Path) -> PathBuf {
119 #[cfg(target_os = "windows")]
120 {
121 let s = path.to_string_lossy();
122 if let Some(rest) = s.strip_prefix(r"\\?\") {
123 return PathBuf::from(rest);
124 }
125 }
126 path.to_path_buf()
127}
128
129fn root_of(path: &Path) -> Option<std::path::Component<'_>> {
130 path.components().next()
131}
132
133#[cfg(target_os = "windows")]
144fn get_volume_uuid(mount_point: &Path) -> Option<String> {
145 use std::ffi::OsString;
146 use std::os::windows::ffi::{OsStrExt, OsStringExt};
147
148 let mut mount_wide: Vec<u16> = mount_point.as_os_str().encode_wide().collect();
149 if mount_wide.last() != Some(&(b'\\' as u16)) {
151 mount_wide.push(b'\\' as u16);
152 }
153 mount_wide.push(0); let mut guid_buf = vec![0u16; 64];
156
157 let ok = unsafe {
158 windows_sys::Win32::Storage::FileSystem::GetVolumeNameForVolumeMountPointW(
159 mount_wide.as_ptr(),
160 guid_buf.as_mut_ptr(),
161 guid_buf.len() as u32,
162 )
163 };
164
165 if ok == 0 {
166 return None;
167 }
168
169 let end = guid_buf.iter().position(|&c| c == 0).unwrap_or(guid_buf.len());
170 let raw = OsString::from_wide(&guid_buf[..end])
171 .to_string_lossy()
172 .to_string();
173
174 let guid = raw
177 .trim_start_matches(r"\\?\Volume{")
178 .trim_end_matches(r"}\")
179 .to_string();
180
181 if guid.is_empty() || guid == raw {
182 None
183 } else {
184 Some(guid)
185 }
186}
187
188#[cfg(target_os = "linux")]
189fn get_volume_uuid(mount_point: &Path) -> Option<String> {
190 let mounts = std::fs::read_to_string("/proc/mounts").ok()?;
191 let mount_str = mount_point.to_string_lossy();
192
193 let device = mounts.lines().find_map(|line| {
195 let mut parts = line.split_whitespace();
196 let dev = parts.next()?;
197 let mp = parts.next()?;
198 if mp == mount_str.as_ref() {
199 Some(dev.to_string())
200 } else {
201 None
202 }
203 })?;
204
205 let by_uuid = Path::new("/dev/disk/by-uuid");
207 if !by_uuid.exists() {
208 return None;
209 }
210
211 for entry in std::fs::read_dir(by_uuid).ok()?.flatten() {
212 if let Ok(link) = std::fs::read_link(entry.path()) {
213 let resolved = by_uuid.join(&link);
214 if let Ok(canonical) = resolved.canonicalize() {
215 if canonical == Path::new(&device) {
216 return Some(entry.file_name().to_string_lossy().to_string());
217 }
218 }
219 }
220 }
221
222 None
223}
224
225#[cfg(not(any(target_os = "windows", target_os = "linux")))]
226fn get_volume_uuid(_mount_point: &Path) -> Option<String> {
227 None
228}
229
230#[cfg(test)]
233mod tests {
234 use super::strip_unc_prefix;
235 use std::path::{Path, PathBuf};
236
237 #[test]
238 #[cfg(target_os = "windows")]
239 fn strip_unc_removes_windows_prefix() {
240 assert_eq!(
241 strip_unc_prefix(Path::new(r"\\?\C:\foo\bar")),
242 PathBuf::from(r"C:\foo\bar")
243 );
244 assert_eq!(
245 strip_unc_prefix(Path::new(r"\\?\E:\hard-sync-target")),
246 PathBuf::from(r"E:\hard-sync-target")
247 );
248 }
249
250 #[test]
251 #[cfg(target_os = "windows")]
252 fn strip_unc_is_noop_for_plain_paths() {
253 assert_eq!(
254 strip_unc_prefix(Path::new(r"C:\foo\bar")),
255 PathBuf::from(r"C:\foo\bar")
256 );
257 }
258
259 #[test]
260 #[cfg(not(target_os = "windows"))]
261 fn strip_unc_is_noop_on_non_windows() {
262 assert_eq!(
263 strip_unc_prefix(Path::new("/home/user/foo")),
264 PathBuf::from("/home/user/foo")
265 );
266 }
267}