Skip to content

Commit 6b42180

Browse files
committed
fix archive function
1 parent 760326a commit 6b42180

File tree

1 file changed

+159
-14
lines changed

1 file changed

+159
-14
lines changed

tabs/archive_project_tab.py

Lines changed: 159 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"""
55

66
import os
7+
import re
78
import shutil
89
import xml.etree.ElementTree as ET
910
from 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

Comments
 (0)