Skip to content

Commit ffcf30e

Browse files
committed
find absolue path and warn
1 parent 6b42180 commit ffcf30e

File tree

1 file changed

+209
-2
lines changed

1 file changed

+209
-2
lines changed

tabs/archive_project_tab.py

Lines changed: 209 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)