Implement configuration management, drag-and-drop functionality, and logging utilities for WebDrop Bridge
This commit is contained in:
parent
04ef84cf9a
commit
6bef2f6119
9 changed files with 1154 additions and 0 deletions
106
src/webdrop_bridge/core/drag_interceptor.py
Normal file
106
src/webdrop_bridge/core/drag_interceptor.py
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
"""Qt widget for intercepting drag events and initiating native drag operations."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtGui import QDrag, QMimeData
|
||||
from PySide6.QtWidgets import QWidget
|
||||
|
||||
from webdrop_bridge.core.validator import PathValidator, ValidationError
|
||||
|
||||
|
||||
class DragInterceptor(QWidget):
|
||||
"""Widget that handles drag initiation for file paths.
|
||||
|
||||
Intercepts drag events from web content and initiates native Qt drag
|
||||
operations, allowing files to be dragged from web content to native
|
||||
applications.
|
||||
|
||||
Signals:
|
||||
drag_started: Emitted when a drag operation begins successfully
|
||||
drag_failed: Emitted when drag initiation fails
|
||||
"""
|
||||
|
||||
# Signals with string parameters (file paths that were dragged)
|
||||
drag_started = Signal(list) # List[str] - list of file paths
|
||||
drag_failed = Signal(str) # str - error message
|
||||
|
||||
def __init__(self, parent: Optional[QWidget] = None):
|
||||
"""Initialize the drag interceptor.
|
||||
|
||||
Args:
|
||||
parent: Parent widget
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self._validator: Optional[PathValidator] = None
|
||||
|
||||
def set_validator(self, validator: PathValidator) -> None:
|
||||
"""Set the path validator for this interceptor.
|
||||
|
||||
Args:
|
||||
validator: PathValidator instance to use for validation
|
||||
"""
|
||||
self._validator = validator
|
||||
|
||||
def initiate_drag(self, file_paths: List[str]) -> bool:
|
||||
"""Initiate a native drag operation for the given files.
|
||||
|
||||
Args:
|
||||
file_paths: List of file paths to drag
|
||||
|
||||
Returns:
|
||||
True if drag was successfully initiated, False otherwise
|
||||
"""
|
||||
if not file_paths:
|
||||
self.drag_failed.emit("No files to drag")
|
||||
return False
|
||||
|
||||
if not self._validator:
|
||||
self.drag_failed.emit("Validator not configured")
|
||||
return False
|
||||
|
||||
# Validate all paths first
|
||||
validated_paths = []
|
||||
for path_str in file_paths:
|
||||
try:
|
||||
path = Path(path_str)
|
||||
if self._validator.validate(path):
|
||||
validated_paths.append(path)
|
||||
except ValidationError as e:
|
||||
self.drag_failed.emit(f"Validation failed for {path_str}: {e}")
|
||||
return False
|
||||
|
||||
if not validated_paths:
|
||||
self.drag_failed.emit("No valid files after validation")
|
||||
return False
|
||||
|
||||
# Create MIME data with file URLs
|
||||
mime_data = QMimeData()
|
||||
file_urls = [
|
||||
path.as_uri() for path in validated_paths
|
||||
]
|
||||
mime_data.setUrls([QUrl(url) for url in file_urls])
|
||||
|
||||
# Create and execute drag operation
|
||||
drag = QDrag(self)
|
||||
drag.setMimeData(mime_data)
|
||||
# Use default drag pixmap (small icon)
|
||||
drag.setPixmap(self.grab(self.rect()).scaled(
|
||||
64, 64, Qt.AspectRatioMode.KeepAspectRatio
|
||||
))
|
||||
|
||||
# Execute drag operation (blocking call)
|
||||
drop_action = drag.exec(Qt.DropAction.CopyAction)
|
||||
|
||||
# Check result
|
||||
if drop_action == Qt.DropAction.CopyAction:
|
||||
self.drag_started.emit(validated_paths)
|
||||
return True
|
||||
else:
|
||||
self.drag_failed.emit("Drag operation cancelled or failed")
|
||||
return False
|
||||
|
||||
|
||||
# Import QUrl here to avoid circular import at module level
|
||||
from PySide6.QtCore import QUrl # noqa: E402, F401
|
||||
97
src/webdrop_bridge/core/validator.py
Normal file
97
src/webdrop_bridge/core/validator.py
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
"""Path validation for secure file operations."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
|
||||
class ValidationError(Exception):
|
||||
"""Raised when path validation fails."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class PathValidator:
|
||||
"""Validates file paths against security whitelist.
|
||||
|
||||
Ensures that only files within allowed root directories can be accessed.
|
||||
All paths are resolved to absolute form before validation to prevent
|
||||
directory traversal attacks.
|
||||
"""
|
||||
|
||||
def __init__(self, allowed_roots: List[Path]):
|
||||
"""Initialize validator with allowed root directories.
|
||||
|
||||
Args:
|
||||
allowed_roots: List of Path objects representing allowed root dirs
|
||||
|
||||
Raises:
|
||||
ValidationError: If any root doesn't exist or isn't a directory
|
||||
"""
|
||||
self.allowed_roots = []
|
||||
|
||||
for root in allowed_roots:
|
||||
root_path = Path(root).resolve()
|
||||
if not root_path.exists():
|
||||
raise ValidationError(
|
||||
f"Allowed root '{root}' does not exist"
|
||||
)
|
||||
if not root_path.is_dir():
|
||||
raise ValidationError(
|
||||
f"Allowed root '{root}' is not a directory"
|
||||
)
|
||||
self.allowed_roots.append(root_path)
|
||||
|
||||
def validate(self, path: Path) -> bool:
|
||||
"""Validate that path is within an allowed root directory.
|
||||
|
||||
Args:
|
||||
path: File path to validate
|
||||
|
||||
Returns:
|
||||
True if path is valid and accessible
|
||||
|
||||
Raises:
|
||||
ValidationError: If path fails validation
|
||||
"""
|
||||
try:
|
||||
# Resolve to absolute path (handles symlinks, .., etc)
|
||||
file_path = Path(path).resolve()
|
||||
except (OSError, ValueError) as e:
|
||||
raise ValidationError(f"Cannot resolve path '{path}': {e}") from e
|
||||
|
||||
# Check file exists
|
||||
if not file_path.exists():
|
||||
raise ValidationError(f"File does not exist: {path}")
|
||||
|
||||
# Check it's a regular file (not directory, symlink to dir, etc)
|
||||
if not file_path.is_file():
|
||||
raise ValidationError(f"Path is not a regular file: {path}")
|
||||
|
||||
# Check path is within an allowed root
|
||||
for allowed_root in self.allowed_roots:
|
||||
try:
|
||||
# This raises ValueError if file_path is not relative to root
|
||||
file_path.relative_to(allowed_root)
|
||||
return True
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
# Not in any allowed root
|
||||
raise ValidationError(
|
||||
f"Path '{file_path}' is not within allowed roots: "
|
||||
f"{self.allowed_roots}"
|
||||
)
|
||||
|
||||
def is_valid(self, path: Path) -> bool:
|
||||
"""Check if path is valid without raising exception.
|
||||
|
||||
Args:
|
||||
path: File path to check
|
||||
|
||||
Returns:
|
||||
True if valid, False otherwise
|
||||
"""
|
||||
try:
|
||||
return self.validate(path)
|
||||
except ValidationError:
|
||||
return False
|
||||
Loading…
Add table
Add a link
Reference in a new issue