Refactor drag handling and update tests
- Renamed `initiate_drag` to `handle_drag` in MainWindow and updated related tests. - Improved drag handling logic to utilize a bridge for starting file drags. - Updated `_on_drag_started` and `_on_drag_failed` methods to match new signatures. - Modified test cases to reflect changes in drag handling and assertions. Enhance path validation and logging - Updated `PathValidator` to log warnings for nonexistent roots instead of raising errors. - Adjusted tests to verify the new behavior of skipping nonexistent roots. Update web application UI and functionality - Changed displayed text for drag items to reflect local paths and Azure Blob Storage URLs. - Added debug logging for drag operations in the web application. - Improved instructions for testing drag and drop functionality. Add configuration documentation and example files - Created `CONFIG_README.md` to provide detailed configuration instructions for WebDrop Bridge. - Added `config.example.json` and `config_test.json` for reference and testing purposes. Implement URL conversion logic - Introduced `URLConverter` class to handle conversion of Azure Blob Storage URLs to local paths. - Added unit tests for URL conversion to ensure correct functionality. Develop download interceptor script - Created `download_interceptor.js` to intercept download-related actions in the web application. - Implemented logging for fetch calls, XMLHttpRequests, and Blob URL creations. Add download test page and related tests - Created `test_download.html` for testing various download scenarios. - Implemented `test_download.py` to verify download path resolution and file construction. - Added `test_url_mappings.py` to ensure URL mappings are loaded correctly. Add unit tests for URL converter - Created `test_url_converter.py` to validate URL conversion logic and mapping behavior.
This commit is contained in:
parent
c9704efc8d
commit
88dc358894
21 changed files with 1870 additions and 432 deletions
|
|
@ -1,8 +1,9 @@
|
|||
"""Configuration management for WebDrop Bridge application."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
|
|
@ -17,9 +18,29 @@ class ConfigurationError(Exception):
|
|||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class URLMapping:
|
||||
"""Maps an Azure Blob Storage URL prefix to a local drive path."""
|
||||
|
||||
url_prefix: str
|
||||
local_path: str
|
||||
|
||||
def __post_init__(self):
|
||||
"""Validate mapping configuration."""
|
||||
if not self.url_prefix.startswith(("http://", "https://")):
|
||||
raise ConfigurationError(
|
||||
f"URL prefix must start with http:// or https://: {self.url_prefix}"
|
||||
)
|
||||
# Ensure URL prefix ends with /
|
||||
if not self.url_prefix.endswith("/"):
|
||||
self.url_prefix += "/"
|
||||
# Normalize local path
|
||||
self.local_path = str(Path(self.local_path))
|
||||
|
||||
|
||||
@dataclass
|
||||
class Config:
|
||||
"""Application configuration loaded from environment variables.
|
||||
"""Application configuration loaded from environment variables or JSON file.
|
||||
|
||||
Attributes:
|
||||
app_name: Application display name
|
||||
|
|
@ -28,7 +49,11 @@ class Config:
|
|||
log_file: Optional log file path
|
||||
allowed_roots: List of whitelisted root directories for file access
|
||||
allowed_urls: List of whitelisted URL domains/patterns (empty = no restriction)
|
||||
webapp_url: URL to load in embedded web application
|
||||
webapp_url: URL to load in embedded web application (default: https://wps.agravity.io/)
|
||||
url_mappings: List of Azure URL to local path mappings
|
||||
check_file_exists: Whether to validate that files exist before drag
|
||||
auto_check_updates: Whether to automatically check for updates
|
||||
update_check_interval_hours: Hours between update checks
|
||||
window_width: Initial window width in pixels
|
||||
window_height: Initial window height in pixels
|
||||
window_title: Main window title (default: "{app_name} v{app_version}")
|
||||
|
|
@ -45,10 +70,85 @@ class Config:
|
|||
allowed_roots: List[Path]
|
||||
allowed_urls: List[str]
|
||||
webapp_url: str
|
||||
window_width: int
|
||||
window_height: int
|
||||
window_title: str
|
||||
enable_logging: bool
|
||||
url_mappings: List[URLMapping] = field(default_factory=list)
|
||||
check_file_exists: bool = True
|
||||
auto_check_updates: bool = True
|
||||
update_check_interval_hours: int = 24
|
||||
window_width: int = 1024
|
||||
window_height: int = 768
|
||||
window_title: str = ""
|
||||
enable_logging: bool = True
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, config_path: Path) -> "Config":
|
||||
"""Load configuration from JSON file.
|
||||
|
||||
Args:
|
||||
config_path: Path to configuration file
|
||||
|
||||
Returns:
|
||||
Config: Configured instance from JSON file
|
||||
|
||||
Raises:
|
||||
ConfigurationError: If configuration file is invalid
|
||||
"""
|
||||
if not config_path.exists():
|
||||
raise ConfigurationError(f"Configuration file not found: {config_path}")
|
||||
|
||||
try:
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
except (json.JSONDecodeError, IOError) as e:
|
||||
raise ConfigurationError(f"Failed to load configuration: {e}") from e
|
||||
|
||||
# Get version from package
|
||||
from webdrop_bridge import __version__
|
||||
|
||||
# Parse URL mappings
|
||||
mappings = [
|
||||
URLMapping(
|
||||
url_prefix=m["url_prefix"],
|
||||
local_path=m["local_path"]
|
||||
)
|
||||
for m in data.get("url_mappings", [])
|
||||
]
|
||||
|
||||
# Parse allowed roots
|
||||
allowed_roots = [Path(p).resolve() for p in data.get("allowed_roots", [])]
|
||||
|
||||
# Validate allowed roots exist
|
||||
for root in allowed_roots:
|
||||
if not root.exists():
|
||||
logger.warning(f"Allowed root does not exist: {root}")
|
||||
elif not root.is_dir():
|
||||
raise ConfigurationError(f"Allowed root is not a directory: {root}")
|
||||
|
||||
# Get log file path
|
||||
log_file = None
|
||||
if data.get("enable_logging", True):
|
||||
log_file_str = data.get("log_file", "logs/webdrop_bridge.log")
|
||||
log_file = Path(log_file_str).resolve()
|
||||
|
||||
app_name = data.get("app_name", "WebDrop Bridge")
|
||||
window_title = data.get("window_title", f"{app_name} v{__version__}")
|
||||
|
||||
return cls(
|
||||
app_name=app_name,
|
||||
app_version=__version__,
|
||||
log_level=data.get("log_level", "INFO").upper(),
|
||||
log_file=log_file,
|
||||
allowed_roots=allowed_roots,
|
||||
allowed_urls=data.get("allowed_urls", []),
|
||||
webapp_url=data.get("webapp_url", "https://wps.agravity.io/"),
|
||||
url_mappings=mappings,
|
||||
check_file_exists=data.get("check_file_exists", True),
|
||||
auto_check_updates=data.get("auto_check_updates", True),
|
||||
update_check_interval_hours=data.get("update_check_interval_hours", 24),
|
||||
window_width=data.get("window_width", 1024),
|
||||
window_height=data.get("window_height", 768),
|
||||
window_title=window_title,
|
||||
enable_logging=data.get("enable_logging", True),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_env(cls, env_file: str | None = None) -> "Config":
|
||||
|
|
@ -81,7 +181,7 @@ class Config:
|
|||
log_file_str = os.getenv("LOG_FILE", "logs/webdrop_bridge.log")
|
||||
allowed_roots_str = os.getenv("ALLOWED_ROOTS", "Z:/,C:/Users/Public")
|
||||
allowed_urls_str = os.getenv("ALLOWED_URLS", "")
|
||||
webapp_url = os.getenv("WEBAPP_URL", "file:///./webapp/index.html")
|
||||
webapp_url = os.getenv("WEBAPP_URL", "https://wps.agravity.io/")
|
||||
window_width = int(os.getenv("WINDOW_WIDTH", "1024"))
|
||||
window_height = int(os.getenv("WINDOW_HEIGHT", "768"))
|
||||
# Window title defaults to app_name + version if not specified
|
||||
|
|
@ -103,14 +203,13 @@ class Config:
|
|||
for p in allowed_roots_str.split(","):
|
||||
root_path = Path(p.strip()).resolve()
|
||||
if not root_path.exists():
|
||||
raise ConfigurationError(
|
||||
f"Allowed root '{p.strip()}' does not exist"
|
||||
)
|
||||
if not root_path.is_dir():
|
||||
logger.warning(f"Allowed root does not exist: {p.strip()}")
|
||||
elif not root_path.is_dir():
|
||||
raise ConfigurationError(
|
||||
f"Allowed root '{p.strip()}' is not a directory"
|
||||
)
|
||||
allowed_roots.append(root_path)
|
||||
else:
|
||||
allowed_roots.append(root_path)
|
||||
except ConfigurationError:
|
||||
raise
|
||||
except (ValueError, OSError) as e:
|
||||
|
|
@ -140,6 +239,32 @@ class Config:
|
|||
if url.strip()
|
||||
] if allowed_urls_str else []
|
||||
|
||||
# Parse URL mappings (Azure Blob Storage → Local Paths)
|
||||
# Format: url_prefix1=local_path1;url_prefix2=local_path2
|
||||
url_mappings_str = os.getenv("URL_MAPPINGS", "")
|
||||
url_mappings = []
|
||||
if url_mappings_str:
|
||||
try:
|
||||
for mapping in url_mappings_str.split(";"):
|
||||
mapping = mapping.strip()
|
||||
if not mapping:
|
||||
continue
|
||||
if "=" not in mapping:
|
||||
raise ConfigurationError(
|
||||
f"Invalid URL mapping format: {mapping}. Expected 'url=path'"
|
||||
)
|
||||
url_prefix, local_path_str = mapping.split("=", 1)
|
||||
url_mappings.append(
|
||||
URLMapping(
|
||||
url_prefix=url_prefix.strip(),
|
||||
local_path=local_path_str.strip()
|
||||
)
|
||||
)
|
||||
except (ValueError, OSError) as e:
|
||||
raise ConfigurationError(
|
||||
f"Invalid URL_MAPPINGS: {url_mappings_str}. Error: {e}"
|
||||
) from e
|
||||
|
||||
return cls(
|
||||
app_name=app_name,
|
||||
app_version=app_version,
|
||||
|
|
@ -148,12 +273,60 @@ class Config:
|
|||
allowed_roots=allowed_roots,
|
||||
allowed_urls=allowed_urls,
|
||||
webapp_url=webapp_url,
|
||||
url_mappings=url_mappings,
|
||||
window_width=window_width,
|
||||
window_height=window_height,
|
||||
window_title=window_title,
|
||||
enable_logging=enable_logging,
|
||||
)
|
||||
|
||||
def to_file(self, config_path: Path) -> None:
|
||||
"""Save configuration to JSON file.
|
||||
|
||||
Args:
|
||||
config_path: Path to save configuration
|
||||
"""
|
||||
data = {
|
||||
"app_name": self.app_name,
|
||||
"webapp_url": self.webapp_url,
|
||||
"url_mappings": [
|
||||
{
|
||||
"url_prefix": m.url_prefix,
|
||||
"local_path": m.local_path
|
||||
}
|
||||
for m in self.url_mappings
|
||||
],
|
||||
"allowed_roots": [str(p) for p in self.allowed_roots],
|
||||
"allowed_urls": self.allowed_urls,
|
||||
"check_file_exists": self.check_file_exists,
|
||||
"auto_check_updates": self.auto_check_updates,
|
||||
"update_check_interval_hours": self.update_check_interval_hours,
|
||||
"log_level": self.log_level,
|
||||
"log_file": str(self.log_file) if self.log_file else None,
|
||||
"window_width": self.window_width,
|
||||
"window_height": self.window_height,
|
||||
"window_title": self.window_title,
|
||||
"enable_logging": self.enable_logging,
|
||||
}
|
||||
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
@staticmethod
|
||||
def get_default_config_path() -> Path:
|
||||
"""Get the default configuration file path.
|
||||
|
||||
Returns:
|
||||
Path to default config file
|
||||
"""
|
||||
import platform
|
||||
if platform.system() == "Windows":
|
||||
base = Path.home() / "AppData" / "Roaming"
|
||||
else:
|
||||
base = Path.home() / ".config"
|
||||
return base / "webdrop_bridge" / "config.json"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return developer-friendly representation."""
|
||||
return (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue