@@ -442,6 +442,100 @@ fn copy_local_flow_dir(src: &Path, dest: &Path) -> Result<(), String> {
442442 Ok ( ( ) )
443443}
444444
445+ fn remove_existing_path ( path : & Path , label : & str ) -> Result < ( ) , String > {
446+ let metadata = match fs:: symlink_metadata ( path) {
447+ Ok ( metadata) => metadata,
448+ Err ( e) if e. kind ( ) == std:: io:: ErrorKind :: NotFound => return Ok ( ( ) ) ,
449+ Err ( e) => {
450+ return Err ( format ! (
451+ "Failed to inspect {} {}: {}" ,
452+ label,
453+ path. display( ) ,
454+ e
455+ ) )
456+ }
457+ } ;
458+
459+ let file_type = metadata. file_type ( ) ;
460+ if file_type. is_dir ( ) && !file_type. is_symlink ( ) {
461+ fs:: remove_dir_all ( path)
462+ } else {
463+ fs:: remove_file ( path)
464+ }
465+ . map_err ( |e| {
466+ format ! (
467+ "Failed to remove existing {} {}: {}" ,
468+ label,
469+ path. display( ) ,
470+ e
471+ )
472+ } )
473+ }
474+
475+ fn path_exists_or_symlink ( path : & Path ) -> bool {
476+ fs:: symlink_metadata ( path) . is_ok ( )
477+ }
478+
479+ fn path_has_wildcard ( path : & str ) -> bool {
480+ path. contains ( '*' ) || path. contains ( '?' )
481+ }
482+
483+ fn wildcard_match ( pattern : & str , value : & str ) -> bool {
484+ let pattern_chars: Vec < char > = pattern. chars ( ) . collect ( ) ;
485+ let value_chars: Vec < char > = value. chars ( ) . collect ( ) ;
486+ let mut pattern_idx = 0 ;
487+ let mut value_idx = 0 ;
488+ let mut star_idx: Option < usize > = None ;
489+ let mut star_value_idx = 0 ;
490+
491+ while value_idx < value_chars. len ( ) {
492+ if pattern_idx < pattern_chars. len ( )
493+ && ( pattern_chars[ pattern_idx] == '?'
494+ || pattern_chars[ pattern_idx] == value_chars[ value_idx] )
495+ {
496+ pattern_idx += 1 ;
497+ value_idx += 1 ;
498+ } else if pattern_idx < pattern_chars. len ( ) && pattern_chars[ pattern_idx] == '*' {
499+ star_idx = Some ( pattern_idx) ;
500+ pattern_idx += 1 ;
501+ star_value_idx = value_idx;
502+ } else if let Some ( star) = star_idx {
503+ pattern_idx = star + 1 ;
504+ star_value_idx += 1 ;
505+ value_idx = star_value_idx;
506+ } else {
507+ return false ;
508+ }
509+ }
510+
511+ while pattern_idx < pattern_chars. len ( ) && pattern_chars[ pattern_idx] == '*' {
512+ pattern_idx += 1 ;
513+ }
514+
515+ pattern_idx == pattern_chars. len ( )
516+ }
517+
518+ fn normalized_path_string ( path : & Path ) -> String {
519+ path. to_string_lossy ( ) . replace ( '\\' , "/" )
520+ }
521+
522+ fn wildcard_search_root ( path : & Path ) -> PathBuf {
523+ let mut root = PathBuf :: new ( ) ;
524+ for component in path. components ( ) {
525+ let component_text = component. as_os_str ( ) . to_string_lossy ( ) ;
526+ if path_has_wildcard ( & component_text) {
527+ break ;
528+ }
529+ root. push ( component. as_os_str ( ) ) ;
530+ }
531+
532+ if root. as_os_str ( ) . is_empty ( ) {
533+ PathBuf :: from ( "." )
534+ } else {
535+ root
536+ }
537+ }
538+
445539fn list_nextflow_locks ( flow_path : & Path ) -> Vec < PathBuf > {
446540 let nextflow_dir = flow_path. join ( ".nextflow" ) ;
447541 if !nextflow_dir. exists ( ) {
@@ -1609,10 +1703,9 @@ pub async fn create_flow(
16091703 // Create flow directory in managed location
16101704 let managed_flow_dir = flows_dir. join ( & name) ;
16111705
1612- if managed_flow_dir . exists ( ) {
1706+ if path_exists_or_symlink ( & managed_flow_dir ) {
16131707 if overwrite {
1614- fs:: remove_dir_all ( & managed_flow_dir)
1615- . map_err ( |e| format ! ( "Failed to remove existing flow directory: {}" , e) ) ?;
1708+ remove_existing_path ( & managed_flow_dir, "flow directory" ) ?;
16161709 } else {
16171710 return Err ( format ! (
16181711 "Flow '{}' already exists at {}. Use overwrite to replace." ,
@@ -1649,15 +1742,9 @@ pub async fn create_flow(
16491742 let module_dir_name = entry. file_name ( ) . to_string_lossy ( ) . to_string ( ) ;
16501743 let dest_module_dir = modules_dir. join ( & module_dir_name) ;
16511744
1652- if dest_module_dir . exists ( ) {
1745+ if path_exists_or_symlink ( & dest_module_dir ) {
16531746 if overwrite {
1654- fs:: remove_dir_all ( & dest_module_dir) . map_err ( |e| {
1655- format ! (
1656- "Failed to remove existing module directory {}: {}" ,
1657- dest_module_dir. display( ) ,
1658- e
1659- )
1660- } ) ?;
1747+ remove_existing_path ( & dest_module_dir, "module directory" ) ?;
16611748 } else {
16621749 continue ;
16631750 }
@@ -1897,9 +1984,8 @@ pub async fn import_flow_from_json(
18971984 . map_err ( |e| e. to_string ( ) ) ?;
18981985 }
18991986
1900- if flow_dir. exists ( ) && overwrite {
1901- fs:: remove_dir_all ( & flow_dir)
1902- . map_err ( |e| format ! ( "Failed to remove existing flow directory: {}" , e) ) ?;
1987+ if overwrite {
1988+ remove_existing_path ( & flow_dir, "flow directory" ) ?;
19031989 }
19041990
19051991 fs:: create_dir_all ( & flow_dir) . map_err ( |e| format ! ( "Failed to create flow directory: {}" , e) ) ?;
@@ -2068,9 +2154,8 @@ pub async fn delete_flow(state: tauri::State<'_, AppState>, flow_id: i64) -> Res
20682154 let path_buf = PathBuf :: from ( p. flow_path ) ;
20692155
20702156 // Only delete if the path is within the flows directory
2071- if path_buf. starts_with ( & flows_dir) && path_buf. exists ( ) {
2072- fs:: remove_dir_all ( & path_buf)
2073- . map_err ( |e| format ! ( "Failed to delete flow directory: {}" , e) ) ?;
2157+ if path_buf. starts_with ( & flows_dir) && path_exists_or_symlink ( & path_buf) {
2158+ remove_existing_path ( & path_buf, "flow directory" ) ?;
20742159 }
20752160 }
20762161
@@ -3873,9 +3958,49 @@ pub fn get_flow_run_logs_full(
38733958
38743959#[ tauri:: command]
38753960pub fn path_exists ( path : String ) -> Result < bool , String > {
3961+ if path_has_wildcard ( & path) {
3962+ return Ok ( !resolve_path_matches ( path) ?. is_empty ( ) ) ;
3963+ }
38763964 Ok ( Path :: new ( & path) . exists ( ) )
38773965}
38783966
3967+ #[ tauri:: command]
3968+ pub fn resolve_path_matches ( path : String ) -> Result < Vec < String > , String > {
3969+ if !path_has_wildcard ( & path) {
3970+ let path_buf = PathBuf :: from ( & path) ;
3971+ return Ok ( if path_buf. exists ( ) {
3972+ vec ! [ path]
3973+ } else {
3974+ Vec :: new ( )
3975+ } ) ;
3976+ }
3977+
3978+ let pattern_path = PathBuf :: from ( & path) ;
3979+ let search_root = wildcard_search_root ( & pattern_path) ;
3980+ if !search_root. exists ( ) {
3981+ return Ok ( Vec :: new ( ) ) ;
3982+ }
3983+
3984+ let normalized_pattern = path. replace ( '\\' , "/" ) ;
3985+ let mut matches = Vec :: new ( ) ;
3986+ for entry in WalkDir :: new ( & search_root)
3987+ . follow_links ( false )
3988+ . into_iter ( )
3989+ . filter_map ( Result :: ok)
3990+ {
3991+ if !entry. file_type ( ) . is_file ( ) {
3992+ continue ;
3993+ }
3994+ let candidate = normalized_path_string ( entry. path ( ) ) ;
3995+ if wildcard_match ( & normalized_pattern, & candidate) {
3996+ matches. push ( entry. path ( ) . to_string_lossy ( ) . to_string ( ) ) ;
3997+ }
3998+ }
3999+ matches. sort ( ) ;
4000+ matches. dedup ( ) ;
4001+ Ok ( matches)
4002+ }
4003+
38794004#[ tauri:: command]
38804005pub fn get_flow_run_work_dir ( state : tauri:: State < AppState > , run_id : i64 ) -> Result < String , String > {
38814006 let biovault_db = state. biovault_db . lock ( ) . map_err ( |e| e. to_string ( ) ) ?;
@@ -3999,11 +4124,10 @@ pub async fn import_flow_from_message(
39994124 let flow_dir = flows_dir. join ( & name) ;
40004125
40014126 // Check if flow already exists
4002- if flow_dir . exists ( ) {
4127+ if path_exists_or_symlink ( & flow_dir ) {
40034128 // For now, we'll overwrite - in the future could prompt user
40044129 // or rename with version suffix
4005- fs:: remove_dir_all ( & flow_dir)
4006- . map_err ( |e| format ! ( "Failed to remove existing flow: {}" , e) ) ?;
4130+ remove_existing_path ( & flow_dir, "flow directory" ) ?;
40074131 }
40084132
40094133 // Create flow directory
@@ -4170,10 +4294,9 @@ pub async fn import_flow_from_request(
41704294 . map_err ( |e| e. to_string ( ) ) ?;
41714295 }
41724296
4173- if dest_dir . exists ( ) {
4297+ if path_exists_or_symlink ( & dest_dir ) {
41744298 if overwrite {
4175- fs:: remove_dir_all ( & dest_dir)
4176- . map_err ( |e| format ! ( "Failed to remove existing flow: {}" , e) ) ?;
4299+ remove_existing_path ( & dest_dir, "flow directory" ) ?;
41774300 } else {
41784301 return Err ( format ! (
41794302 "Flow '{}' already exists at {}. Use overwrite to replace." ,
@@ -4203,15 +4326,9 @@ pub async fn import_flow_from_request(
42034326 let module_dir_name = entry. file_name ( ) . to_string_lossy ( ) . to_string ( ) ;
42044327 let dest_module_dir = modules_dir. join ( & module_dir_name) ;
42054328
4206- if dest_module_dir . exists ( ) {
4329+ if path_exists_or_symlink ( & dest_module_dir ) {
42074330 if overwrite {
4208- fs:: remove_dir_all ( & dest_module_dir) . map_err ( |e| {
4209- format ! (
4210- "Failed to remove existing module directory {}: {}" ,
4211- dest_module_dir. display( ) ,
4212- e
4213- )
4214- } ) ?;
4331+ remove_existing_path ( & dest_module_dir, "module directory" ) ?;
42154332 } else {
42164333 continue ;
42174334 }
0 commit comments