11//! Tauri commands for volume operations.
22
3+ use serde:: Serialize ;
34use tokio:: time:: Duration ;
45
56use super :: util:: { TimedOut , blocking_with_timeout_flag} ;
67use crate :: volumes:: { self , DEFAULT_VOLUME_ID , LocationCategory , VolumeInfo , VolumeSpaceInfo } ;
78
9+ /// Result of resolving a path to its containing volume.
10+ /// Unlike `TimedOut<Option<VolumeInfo>>`, `timed_out: true` means "the filesystem
11+ /// didn't respond, we genuinely don't know" — not "here's a fallback."
12+ #[ derive( Debug , Clone , Serialize ) ]
13+ #[ serde( rename_all = "camelCase" ) ]
14+ pub struct PathVolumeResolution {
15+ pub volume : Option < VolumeInfo > ,
16+ pub timed_out : bool ,
17+ }
18+
819const VOLUME_TIMEOUT : Duration = Duration :: from_secs ( 2 ) ;
920
1021/// Lists all mounted volumes, including connected MTP devices.
@@ -21,38 +32,6 @@ pub fn get_default_volume_id() -> String {
2132 DEFAULT_VOLUME_ID . to_string ( )
2233}
2334
24- /// Finds the actual volume (not a favorite) that contains a given path.
25- /// Returns the volume info for the best matching volume, excluding favorites.
26- /// This is used to determine which volume to set as active when a favorite is chosen.
27- #[ tauri:: command]
28- pub async fn find_containing_volume ( path : String ) -> TimedOut < Option < VolumeInfo > > {
29- let mut result = blocking_with_timeout_flag ( VOLUME_TIMEOUT , vec ! [ ] , volumes:: list_locations) . await ;
30- append_mtp_volumes ( & mut result. data ) . await ;
31-
32- // Only consider actual volumes, not favorites
33- let volumes: Vec < _ > = result
34- . data
35- . into_iter ( )
36- . filter ( |loc| loc. category != LocationCategory :: Favorite )
37- . collect ( ) ;
38-
39- // Find the volume with the longest matching path prefix
40- let mut best_match: Option < VolumeInfo > = None ;
41- let mut best_len = 0 ;
42-
43- for vol in volumes {
44- if path. starts_with ( & vol. path ) && vol. path . len ( ) > best_len {
45- best_len = vol. path . len ( ) ;
46- best_match = Some ( vol) ;
47- }
48- }
49-
50- TimedOut {
51- data : best_match,
52- timed_out : result. timed_out ,
53- }
54- }
55-
5635/// Gets space information for a volume at the given path.
5736/// Returns total and available bytes for the volume.
5837/// For MTP paths (`mtp://`), fetches from the MTP connection manager instead of macOS NSURL.
@@ -67,6 +46,64 @@ pub async fn get_volume_space(path: String) -> TimedOut<Option<VolumeSpaceInfo>>
6746 blocking_with_timeout_flag ( VOLUME_TIMEOUT , None , move || volumes:: get_volume_space ( & path) ) . await
6847}
6948
49+ /// Resolves a path to its containing volume without enumerating all volumes.
50+ /// Uses `statfs()` for filesystem paths (<1ms for local disks), protocol
51+ /// dispatch for MTP/SMB paths. Returns `timed_out: true` if the filesystem
52+ /// didn't respond within 2s.
53+ #[ tauri:: command]
54+ pub async fn resolve_path_volume ( path : String ) -> PathVolumeResolution {
55+ // MTP protocol dispatch
56+ if path. starts_with ( "mtp://" ) {
57+ let mtp_volume = find_mtp_volume_for_path ( & path) . await ;
58+ return PathVolumeResolution {
59+ volume : mtp_volume,
60+ timed_out : false ,
61+ } ;
62+ }
63+
64+ // SMB/network protocol paths → return the virtual network volume
65+ if path. starts_with ( "smb://" ) {
66+ return PathVolumeResolution {
67+ volume : Some ( VolumeInfo {
68+ id : "network" . to_string ( ) ,
69+ name : "Network" . to_string ( ) ,
70+ path : "smb://" . to_string ( ) ,
71+ category : LocationCategory :: Network ,
72+ icon : None ,
73+ is_ejectable : false ,
74+ fs_type : Some ( "smbfs" . to_string ( ) ) ,
75+ supports_trash : false ,
76+ is_read_only : false ,
77+ } ) ,
78+ timed_out : false ,
79+ } ;
80+ }
81+
82+ // Filesystem paths: resolve via statfs with timeout
83+ let result =
84+ blocking_with_timeout_flag ( VOLUME_TIMEOUT , None , move || volumes:: resolve_path_volume_fast ( & path) ) . await ;
85+
86+ PathVolumeResolution {
87+ volume : result. data ,
88+ timed_out : result. timed_out ,
89+ }
90+ }
91+
92+ /// Finds the MTP volume matching a `mtp://device_id/storage_id/...` path.
93+ async fn find_mtp_volume_for_path ( path : & str ) -> Option < VolumeInfo > {
94+ let rest = path. strip_prefix ( "mtp://" ) ?;
95+ let mut parts = rest. splitn ( 3 , '/' ) ;
96+ let device_id = parts. next ( ) ?;
97+ let storage_id_str = parts. next ( ) ?;
98+ let _storage_id: u32 = storage_id_str. parse ( ) . ok ( ) ?;
99+
100+ let mut volumes = Vec :: new ( ) ;
101+ append_mtp_volumes ( & mut volumes) . await ;
102+ // Match on the path prefix (mtp://device_id/storage_id)
103+ let prefix = format ! ( "mtp://{}/{}" , device_id, storage_id_str) ;
104+ volumes. into_iter ( ) . find ( |v| v. path == prefix)
105+ }
106+
70107/// Appends connected MTP device storages to the volume list.
71108/// Each storage becomes a separate volume entry with category `MobileDevice`.
72109async fn append_mtp_volumes ( volumes : & mut Vec < VolumeInfo > ) {
0 commit comments