@@ -219,6 +219,7 @@ def _show_help_popup(self):
219219            "<li><b>Database snapshots:</b> Data reflects the current state at export time</li>" 
220220            "<li><b>No live sync:</b> Archived data won't update from the original database</li>" 
221221            "<li><b>Security:</b> All database credentials are automatically removed</li>" 
222+             "<li><b>Path review:</b> Some absolute file paths may require manual review</li>" 
222223            "</ul>" 
223224        )
224225
@@ -371,6 +372,189 @@ def _resize_images_in_folder(self, folder_path, max_long_side):
371372            self .emit_log (f"Error during image resizing: { str (e )}  " )
372373            return  0 
373374
375+     def  _detect_remaining_absolute_paths (self , qgs_path ):
376+         """Detect and report remaining absolute paths in the project file""" 
377+         try :
378+             with  open (str (qgs_path ), 'r' , encoding = 'utf-8' ) as  f :
379+                 content  =  f .read ()
380+             
381+             # Use simpler, safer patterns to avoid regex hangs 
382+             found_paths  =  {}
383+             
384+             # Look for Windows absolute paths (much simpler pattern) 
385+             windows_pattern  =  r'[A-Z]:[/\\][^"\s<>]*' 
386+             for  match  in  re .findall (windows_pattern , content ):
387+                 if  self ._is_likely_absolute_path (match ):
388+                     path_type  =  self ._categorize_path_simple (match )
389+                     if  path_type  not  in   found_paths :
390+                         found_paths [path_type ] =  set ()
391+                     found_paths [path_type ].add (match )
392+             
393+             # Look specifically for file:/// URIs (safer pattern) 
394+             file_uri_pattern  =  r'file:///[A-Z]:[/\\][^"\s<>]*' 
395+             for  match  in  re .findall (file_uri_pattern , content ):
396+                 if  self ._is_likely_absolute_path (match ):
397+                     path_type  =  self ._categorize_path_simple (match )
398+                     if  path_type  not  in   found_paths :
399+                         found_paths [path_type ] =  set ()
400+                     found_paths [path_type ].add (match )
401+             
402+             return  found_paths 
403+             
404+         except  Exception  as  e :
405+             self .emit_log (f"Error detecting absolute paths: { str (e )}  " )
406+             return  {}
407+ 
408+     def  _is_likely_absolute_path (self , path ):
409+         """Check if a string is likely to be an absolute file path""" 
410+         # Skip URLs, XML namespaces, and other non-path strings 
411+         skip_patterns  =  [
412+             r'^https?://' ,
413+             r'^ftp://' ,
414+             r'xmlns' ,
415+             r'\.xsd$' ,
416+             r'\.dtd$' ,
417+             r'^qgis\.org' ,
418+             r'postgresql://' ,
419+             r'postgis:' ,
420+         ]
421+         
422+         for  pattern  in  skip_patterns :
423+             if  re .search (pattern , path , re .IGNORECASE ):
424+                 return  False 
425+         
426+         # Check if it looks like a file path 
427+         return  (
428+             len (path ) >  3  and 
429+             ('/'  in  path  or  '\\ '  in  path ) and 
430+             not  path .startswith ('www.' ) and 
431+             not  path .endswith ('.org' ) and 
432+             not  path .endswith ('.com' )
433+         )
434+ 
435+     def  _categorize_path_simple (self , path ):
436+         """Categorize the type of absolute path found using simple string checks""" 
437+         path_lower  =  path .lower ()
438+         
439+         if  '.csv'  in  path_lower :
440+             return  'CSV File References' 
441+         elif  any (ext  in  path_lower  for  ext  in  ['.svg' , '.png' , '.jpg' , '.jpeg' , '.tiff' , '.pdf' ]):
442+             return  'Image/Document References' 
443+         elif  any (keyword  in  path_lower  for  keyword  in  ['browse' , 'export' , 'layout' ]):
444+             return  'UI Preferences/Directories' 
445+         else :
446+             return  'Other File Paths' 
447+ 
448+     def  _show_absolute_paths_summary (self , found_paths , output_folder ):
449+         """Show a summary dialog of remaining absolute paths""" 
450+         if  not  found_paths :
451+             return 
452+         
453+         # Create summary message 
454+         summary_parts  =  [
455+             "<h3>⚠️ Manual Review Required</h3>" ,
456+             "<p>The portable project was created successfully, but some absolute file paths remain that may need manual attention:</p>" ,
457+             "" 
458+         ]
459+         
460+         total_paths  =  sum (len (paths ) for  paths  in  found_paths .values ())
461+         
462+         for  category , paths  in  found_paths .items ():
463+             summary_parts .append (f"<h4>{ category }   ({ len (paths )}   path(s)):</h4>" )
464+             summary_parts .append ("<ul>" )
465+             
466+             # Show first few paths as examples 
467+             path_list  =  list (paths )[:3 ]  # Show max 3 examples 
468+             for  path  in  path_list :
469+                 summary_parts .append (f"<li><code>{ path }  </code></li>" )
470+             
471+             if  len (paths ) >  3 :
472+                 summary_parts .append (f"<li><i>... and { len (paths ) -  3 }   more</i></li>" )
473+             
474+             summary_parts .append ("</ul>" )
475+             summary_parts .append ("" )
476+         
477+         summary_parts .extend ([
478+             "<h4>What you should do:</h4>" ,
479+             "<ul>" ,
480+             "<li><b>CSV files:</b> Copy the referenced CSV files to your project folder and update paths to be relative</li>" ,
481+             "<li><b>UI Preferences:</b> These are usually safe to ignore as they're user interface settings</li>" ,
482+             "<li><b>Images/Documents:</b> Copy referenced files to your project folder if needed</li>" ,
483+             "<li><b>Other paths:</b> Review and update as needed for your portable project</li>" ,
484+             "</ul>" ,
485+             "" ,
486+             f"<p><b>Tip:</b> You can search for absolute paths in your project file:<br>" ,
487+             f"<code>{ os .path .basename (output_folder )}  _portable.qgs</code></p>" ,
488+             "" ,
489+             f"<p><small>Total paths found: { total_paths }  </small></p>" 
490+         ])
491+         
492+         # Show dialog 
493+         msg  =  QMessageBox (self )
494+         msg .setWindowTitle ("Manual Review Required - Absolute Paths Found" )
495+         msg .setTextFormat (1 )  # Rich text format 
496+         msg .setText ("\n " .join (summary_parts ))
497+         msg .setIcon (QMessageBox .Warning )
498+         msg .setStandardButtons (QMessageBox .Ok )
499+         msg .setDefaultButton (QMessageBox .Ok )
500+         
501+         # Make dialog larger 
502+         msg .setMinimumWidth (600 )
503+         msg .exec_ ()
504+ 
505+     def  _try_convert_csv_paths_to_relative (self , qgs_path , output_folder ):
506+         """Attempt to convert CSV file paths to relative paths if the files exist in the project""" 
507+         try :
508+             with  open (str (qgs_path ), 'r' , encoding = 'utf-8' ) as  f :
509+                 content  =  f .read ()
510+             
511+             # Use a simpler, more targeted pattern for CSV LayerSource entries 
512+             csv_pattern  =  r'<Option name="LayerSource"[^>]*value="file:///([A-Z]:[/\\][^"]*\.csv[^"]*)"' 
513+             csv_matches  =  re .findall (csv_pattern , content )
514+             
515+             if  not  csv_matches :
516+                 return  0 
517+             
518+             updated_content  =  content 
519+             conversions_made  =  0 
520+             
521+             for  csv_path  in  csv_matches :
522+                 try :
523+                     # Extract just the filename 
524+                     csv_filename  =  os .path .basename (csv_path .split ('?' )[0 ])  # Remove query parameters 
525+                     
526+                     # Check if this CSV file exists in the output folder 
527+                     potential_csv_path  =  Path (output_folder ) /  csv_filename 
528+                     if  potential_csv_path .exists ():
529+                         # Create relative path 
530+                         relative_path  =  f"./{ csv_filename }  " 
531+                         query_part  =  "" 
532+                         if  '?'  in  csv_path :
533+                             query_part  =  "?"  +  csv_path .split ('?' , 1 )[1 ]
534+                         
535+                         new_source  =  f"file:///{ relative_path } { query_part }  " 
536+                         old_source  =  f"file:///{ csv_path }  " 
537+                         
538+                         # Replace in content 
539+                         updated_content  =  updated_content .replace (old_source , new_source )
540+                         conversions_made  +=  1 
541+                         self .emit_log (f"Converted CSV path to relative: { csv_filename }  " )
542+                 except  Exception  as  e :
543+                     self .emit_log (f"Error processing CSV path { csv_path }  : { str (e )}  " )
544+                     continue 
545+             
546+             if  conversions_made  >  0 :
547+                 # Write updated content back 
548+                 with  open (str (qgs_path ), 'w' , encoding = 'utf-8' ) as  f :
549+                     f .write (updated_content )
550+                 self .emit_log (f"Successfully converted { conversions_made }   CSV path(s) to relative" )
551+             
552+             return  conversions_made 
553+             
554+         except  Exception  as  e :
555+             self .emit_log (f"Error converting CSV paths: { str (e )}  " )
556+             return  0 
557+ 
374558    def  _on_archive_project (self ):
375559        output_folder  =  self .output_folder_edit .text ().strip ()
376560        if  not  self .validate_non_empty_field (output_folder , "output folder" ):
@@ -434,7 +618,7 @@ def _on_archive_project(self):
434618            project_dir  =  project_file .parent 
435619            total_files  =  len ([item  for  item  in  project_dir .iterdir () if  item .name  !=  project_file .name ])
436620
437-             total_steps  =  total_files  +  total_layers  +  3   # +3  for project copy, final update, and report creation 
621+             total_steps  =  total_files  +  total_layers  +  5   # +5  for project copy, final update, CSV conversion, path detection , and report creation 
438622            current_step  =  0 
439623
440624            # Switch to determinate progress 
@@ -564,7 +748,23 @@ def _on_archive_project(self):
564748            current_step  +=  1 
565749            self .progress_bar .setValue (current_step )
566750
567-             # 6. Create archive report 
751+             # 6. Try to convert CSV paths to relative if possible 
752+             self .progress_label .setText ("Converting CSV paths to relative..." )
753+             csv_conversions  =  self ._try_convert_csv_paths_to_relative (export_path , output_folder )
754+             if  csv_conversions  >  0 :
755+                 self .emit_log (f"Converted { csv_conversions }   CSV paths to relative" )
756+             
757+             current_step  +=  1 
758+             self .progress_bar .setValue (current_step )
759+ 
760+             # 7. Detect remaining absolute paths and inform user 
761+             self .progress_label .setText ("Checking for remaining absolute paths..." )
762+             remaining_paths  =  self ._detect_remaining_absolute_paths (export_path )
763+             
764+             current_step  +=  1 
765+             self .progress_bar .setValue (current_step )
766+ 
767+             # 8. Create archive report 
568768            self .progress_label .setText ("Creating archive report..." )
569769            self ._create_archive_report (
570770                str (output_folder ), 
@@ -576,6 +776,13 @@ def _on_archive_project(self):
576776
577777            self .progress_label .setText ("Complete!" )
578778            self .emit_log (f"✓ Successfully archived project '{ project_name }  ' to { export_path }  " )
779+             
780+             # Show summary of remaining paths if any found 
781+             if  remaining_paths :
782+                 self ._show_absolute_paths_summary (remaining_paths , output_folder )
783+             else :
784+                 self .emit_log ("✓ No remaining absolute paths detected" )
785+             
579786            self .project_archived .emit (str (export_path ))
580787
581788        except  Exception  as  e :
0 commit comments