44""" 
55
66import  os 
7+ import  re 
78import  shutil 
89import  xml .etree .ElementTree  as  ET 
910from  pathlib  import  Path 
@@ -198,6 +199,7 @@ def _show_help_popup(self):
198199            "<li><b>PostgreSQL layers →</b> Converted to a single 'data.gpkg' GeoPackage file</li>" 
199200            "<li><b>All project files →</b> Copied to output folder (including DCIM, photos, etc.)</li>" 
200201            "<li><b>Project file →</b> Updated to reference local GeoPackage instead of database</li>" 
202+             "<li><b>Credentials →</b> Database credentials automatically removed for security</li>" 
201203            "<li><b>Folder structure →</b> Preserved exactly as in original project</li>" 
202204            "</ul>" 
203205            "<h4>Image Resizing (Optional):</h4>" 
@@ -216,6 +218,7 @@ def _show_help_popup(self):
216218            "<li><b>Image resizing:</b> Processing many high-resolution images can take significant time</li>" 
217219            "<li><b>Database snapshots:</b> Data reflects the current state at export time</li>" 
218220            "<li><b>No live sync:</b> Archived data won't update from the original database</li>" 
221+             "<li><b>Security:</b> All database credentials are automatically removed</li>" 
219222            "</ul>" 
220223        )
221224
@@ -270,6 +273,8 @@ def _create_archive_report(self, output_folder, project_file, resized_images_cou
270273                "- PostgreSQL layers converted to GeoPackage format" ,
271274                "- All project files and folders copied to output directory" ,
272275                "- Project file updated to reference local data sources" ,
276+                 "- Database credentials removed for security" ,
277+                 "- All PostgreSQL references converted to GeoPackage" ,
273278            ]
274279
275280            # Add image resizing info if applicable 
@@ -387,7 +392,8 @@ def _on_archive_project(self):
387392            "This includes:" ,
388393            "• DCIM folder (which might be quite large)" ,
389394            "• All other project files and folders" ,
390-             "• PostgreSQL layers will be converted to Geopackage" 
395+             "• PostgreSQL layers will be converted to Geopackage" ,
396+             "• Database credentials will be automatically removed" 
391397        ]
392398
393399        # Add image resizing warning if enabled 
@@ -539,7 +545,8 @@ def _on_archive_project(self):
539545                    )
540546
541547                    if  error  ==  QgsVectorFileWriter .NoError :
542-                         new_source  =  f"{ gpkg_path }  |layername={ layer_name }  " 
548+                         # Use relative path since GeoPackage is always next to QGS file 
549+                         new_source  =  f"data.gpkg|layername={ layer_name }  " 
543550                        new_layer_sources [layer_id ] =  new_source 
544551                        self .emit_log (f"Exported { layer .name ()}   to geopackage" )
545552                    else :
@@ -548,11 +555,11 @@ def _on_archive_project(self):
548555                    current_step  +=  1 
549556                    self .progress_bar .setValue (current_step )
550557
551-             # 5. Update the copied project file to point to geopackage 
558+             # 5. Update the copied project file to point to geopackage and clean credentials  
552559            self .progress_label .setText ("Updating project file..." )
553560            if  new_layer_sources :
554-                 self ._update_project_sources (export_path , new_layer_sources , postgresql_layers )
555-                 self .emit_log ("Updated project file to use geopackage sources" )
561+                 self ._update_project_sources_comprehensive (export_path , new_layer_sources , postgresql_layers ,  str ( gpkg_path ) )
562+                 self .emit_log ("Updated project file to use geopackage sources and removed credentials " )
556563
557564            current_step  +=  1 
558565            self .progress_bar .setValue (current_step )
@@ -579,16 +586,61 @@ def _on_archive_project(self):
579586            self .progress_bar .setVisible (False )
580587            self .archive_btn .setEnabled (True )
581588
582-     def  _update_project_sources (self , qgs_path , new_sources , postgresql_layers ):
583-         """Update the project file to use geopackage sources instead of PostgreSQL""" 
589+     def  _clean_credentials_from_content (self , content ):
590+         """Clean database credentials from QGS content using similar logic as clean_qgs_tab.py""" 
591+         changes_count  =  0 
592+         cleaned_content  =  content 
593+         
594+         # Remove user credentials 
595+         user_matches  =  re .findall (r'user=[\'"][^\'\"]*[\'"]|user=[^\s]+' , cleaned_content )
596+         changes_count  +=  len (user_matches )
597+         
598+         # Remove user credentials (being very careful about spaces) 
599+         cleaned_content  =  re .sub (r'\s+user=[\'"][^\'\"]*[\'"]' , '' , cleaned_content )
600+         cleaned_content  =  re .sub (r'\s+user=[^\s]+' , '' , cleaned_content )
601+         cleaned_content  =  re .sub (r'user=[\'"][^\'\"]*[\'"]\s+' , '' , cleaned_content )
602+         cleaned_content  =  re .sub (r'user=[^\s]+\s+' , '' , cleaned_content )
603+         cleaned_content  =  re .sub (r'user=[\'"][^\'\"]*[\'"]' , '' , cleaned_content )
604+         cleaned_content  =  re .sub (r'user=[^\s]+' , '' , cleaned_content )
605+         
606+         # Remove password credentials 
607+         password_matches  =  re .findall (r'password=[\'"][^\'\"]*[\'"]|password=[^\s]+' , cleaned_content )
608+         changes_count  +=  len (password_matches )
609+         
610+         # Remove password credentials (being very careful about spaces) 
611+         cleaned_content  =  re .sub (r'\s+password=[\'"][^\'\"]*[\'"]' , '' , cleaned_content )
612+         cleaned_content  =  re .sub (r'\s+password=[^\s]+' , '' , cleaned_content )
613+         cleaned_content  =  re .sub (r'password=[\'"][^\'\"]*[\'"]\s+' , '' , cleaned_content )
614+         cleaned_content  =  re .sub (r'password=[^\s]+\s+' , '' , cleaned_content )
615+         cleaned_content  =  re .sub (r'password=[\'"][^\'\"]*[\'"]' , '' , cleaned_content )
616+         cleaned_content  =  re .sub (r'password=[^\s]+' , '' , cleaned_content )
617+         
618+         return  cleaned_content , changes_count 
619+ 
620+     def  _update_project_sources_comprehensive (self , qgs_path , new_sources , postgresql_layers , gpkg_path ):
621+         """Comprehensive update of the project file to use geopackage sources and remove all PostgreSQL references""" 
584622        try :
585-             # Parse  the QGS  file (XML)  
586-             tree   =   ET . parse (str (qgs_path )) 
587-             root   =   tree . getroot ()
623+             # Read  the file content  
624+             with   open (str (qgs_path ),  'r' ,  encoding = 'utf-8' )  as   f : 
625+                  content   =   f . read ()
588626
589-             # Find all maplayer elements 
590-             maplayers  =  root .findall (".//maplayer" )
627+             # Clean database credentials first 
628+             content , creds_removed  =  self ._clean_credentials_from_content (content )
629+             if  creds_removed  >  0 :
630+                 self .emit_log (f"Removed { creds_removed }   database credential(s)" )
631+             
632+             # Parse the XML 
633+             root  =  ET .fromstring (content )
591634
635+             # Create layer ID to layer name mapping for easier lookup 
636+             layer_id_to_name  =  {}
637+             for  layer_id , layer  in  QgsProject .instance ().mapLayers ().items ():
638+                 if  layer_id  in  new_sources :
639+                     layer_name  =  layer .name ().replace (' ' , '_' ).replace ('/' , '_' )
640+                     layer_id_to_name [layer_id ] =  layer_name 
641+             
642+             # 1. Update basic maplayer elements 
643+             maplayers  =  root .findall (".//maplayer" )
592644            for  maplayer  in  maplayers :
593645                layer_id  =  maplayer .get ("id" ) or  maplayer .findtext ("id" )
594646
@@ -603,10 +655,103 @@ def _update_project_sources(self, qgs_path, new_sources, postgresql_layers):
603655                    if  provider_elem  is  not   None :
604656                        provider_elem .text  =  "ogr" 
605657
606-                     self .emit_log (f"Updated layer { layer_id }   source in project file" )
658+                     self .emit_log (f"Updated maplayer { layer_id }   source in project file" )
659+             
660+             # 2. Update layer-tree-layer elements (providerKey and source attributes) 
661+             layer_tree_layers  =  root .findall (".//layer-tree-layer" )
662+             for  layer_tree_layer  in  layer_tree_layers :
663+                 layer_id  =  layer_tree_layer .get ("id" )
664+                 
665+                 if  layer_id  in  new_sources :
666+                     # Update providerKey attribute 
667+                     layer_tree_layer .set ("providerKey" , "ogr" )
668+                     
669+                     # Update source attribute 
670+                     layer_tree_layer .set ("source" , new_sources [layer_id ])
671+                     
672+                     self .emit_log (f"Updated layer-tree-layer { layer_id }   in project file" )
673+             
674+             # 3. Update relation elements 
675+             relations  =  root .findall (".//relation" )
676+             for  relation  in  relations :
677+                 # Update referencingLayer dataSource 
678+                 referencing_layer  =  relation .get ("referencingLayer" )
679+                 if  referencing_layer  in  new_sources :
680+                     relation .set ("dataSource" , new_sources [referencing_layer ])
681+                 
682+                 # Update referencedLayer dataSource   
683+                 referenced_layer  =  relation .get ("referencedLayer" )
684+                 if  referenced_layer  in  new_sources :
685+                     relation .set ("dataSource" , new_sources [referenced_layer ])
686+                 
687+                 # Update providerKey 
688+                 if  referencing_layer  in  new_sources  or  referenced_layer  in  new_sources :
689+                     relation .set ("providerKey" , "ogr" )
690+             
691+             # 4. Update Layer elements in project styles 
692+             layer_elements  =  root .findall (".//Layer" )
693+             for  layer_elem  in  layer_elements :
694+                 source  =  layer_elem .get ("source" )
695+                 if  source  and  ("postgres"  in  source .lower () or  "dbname="  in  source ):
696+                     # Try to find matching layer by source pattern 
697+                     for  layer_id , new_source  in  new_sources .items ():
698+                         # This is a simplified matching - you might want to improve this 
699+                         if  layer_id  in  source  or  any (part  in  source  for  part  in  source .split ()):
700+                             layer_elem .set ("source" , new_source )
701+                             layer_elem .set ("provider" , "ogr" )
702+                             break 
703+             
704+             # 5. Update LayerStyle elements 
705+             layer_styles  =  root .findall (".//LayerStyle" )
706+             for  layer_style  in  layer_styles :
707+                 layer_id  =  layer_style .get ("layerid" )
708+                 if  layer_id  in  new_sources :
709+                     layer_style .set ("source" , new_sources [layer_id ])
710+                     layer_style .set ("provider" , "ogr" )
711+             
712+             # 6. Update Atlas configuration 
713+             atlas  =  root .find (".//Atlas" )
714+             if  atlas  is  not   None :
715+                 coverage_layer  =  atlas .get ("coverageLayer" )
716+                 if  coverage_layer  in  new_sources :
717+                     atlas .set ("coverageLayer" , coverage_layer )
718+                     atlas .set ("coverageLayerSource" , new_sources [coverage_layer ])
719+                     atlas .set ("coverageLayerProvider" , "ogr" )
720+             
721+             # 7. Update GPS settings 
722+             gps_settings  =  root .find (".//ProjectGpsSettings" )
723+             if  gps_settings  is  not   None :
724+                 dest_layer  =  gps_settings .get ("destinationLayer" )
725+                 if  dest_layer  in  new_sources :
726+                     gps_settings .set ("destinationLayerSource" , new_sources [dest_layer ])
727+                     gps_settings .set ("destinationLayerProvider" , "ogr" )
728+             
729+             # 8. Update Option elements with LayerProviderName 
730+             option_elements  =  root .findall (".//Option[@name='LayerProviderName']" )
731+             for  option_elem  in  option_elements :
732+                 if  option_elem .get ("value" ) ==  "postgres" :
733+                     option_elem .set ("value" , "ogr" )
734+             
735+             # 9. Clean up any remaining PostgreSQL references in the serialized content 
736+             content  =  ET .tostring (root , encoding = 'unicode' )
737+             
738+             # Additional text-based cleanup for any missed references 
739+             # Replace remaining postgres provider references 
740+             content  =  re .sub (r'providerKey="postgres"' , 'providerKey="ogr"' , content )
741+             content  =  re .sub (r"providerKey='postgres'" , "providerKey='ogr'" , content )
742+             content  =  re .sub (r'provider="postgres"' , 'provider="ogr"' , content )
743+             content  =  re .sub (r"provider='postgres'" , "provider='ogr'" , content )
744+             
745+             # Replace LayerProviderName values 
746+             content  =  re .sub (r'<Option name="LayerProviderName" type="QString" value="postgres" />' ,
747+                            '<Option name="LayerProviderName" type="QString" value="ogr" />' , content )
607748
608749            # Write the updated XML back to file 
609-             tree .write (str (qgs_path ), encoding = 'utf-8' , xml_declaration = True )
750+             with  open (str (qgs_path ), 'w' , encoding = 'utf-8' ) as  f :
751+                 f .write ('<?xml version="1.0" encoding="UTF-8"?>\n ' )
752+                 f .write (content )
753+             
754+             self .emit_log ("Comprehensive project file update completed" )
610755
611756        except  Exception  as  e :
612757            self .emit_log (f"Error updating project file: { str (e )}  " )
0 commit comments