Compare commits

..

No commits in common. "7873d0a060553bbe2bfeab0fe3e4aaecef979a8b" and "dffc925bb6bb11a8d771d1f481a19d509389c84e" have entirely different histories.

27 changed files with 132 additions and 12665 deletions

View file

@ -2,7 +2,7 @@
# Application
APP_NAME=WebDrop Bridge
APP_VERSION=0.5.0
APP_VERSION=0.1.0
# Web App
WEBAPP_URL=file:///./webapp/index.html
@ -10,8 +10,7 @@ WEBAPP_URL=file:///./webapp/index.html
# Logging
LOG_LEVEL=DEBUG
# LOG_FILE defaults to AppData/Roaming/webdrop_bridge/logs/webdrop_bridge.log if not set
# LOG_FILE=logs/webdrop_bridge.log
LOG_FILE=logs/webdrop_bridge.log
ENABLE_LOGGING=true
# Security - Path Whitelist

View file

@ -18,10 +18,10 @@ Create a `config.json` file with the following structure:
```json
{
"app_name": "WebDrop Bridge",
"webapp_url": "https://dev.agravity.io/",
"webapp_url": "https://wps.agravity.io/",
"url_mappings": [
{
"url_prefix": "https://devagravitystg.file.core.windows.net/devagravitysync/",
"url_prefix": "https://wpsagravitystg.file.core.windows.net/wpsagravitysync/",
"local_path": "Z:"
}
],
@ -45,7 +45,7 @@ Create a `config.json` file with the following structure:
### Core Settings
- **`webapp_url`** (string): URL of the web application to load
- Example: `"https://dev.agravity.io/"`
- Example: `"https://wps.agravity.io/"`
- Supports `http://`, `https://`, or `file:///` URLs
- **`url_mappings`** (array): Azure Blob Storage URL to local path mappings
@ -55,7 +55,7 @@ Create a `config.json` file with the following structure:
- Example:
```json
{
"url_prefix": "https://dev.file.core.windows.net/devagravitysync/",
"url_prefix": "https://wpsagravitystg.file.core.windows.net/wpsagravitysync/",
"local_path": "Z:"
}
```
@ -68,7 +68,7 @@ Create a `config.json` file with the following structure:
When the web application provides a drag URL like:
```
https://devagravitystg.file.core.windows.net/devagravitysync/aN5PysnXIuRECzcRbvHkjL7g0/Hintergrund_Agravity.png
https://wpsagravitystg.file.core.windows.net/wpsagravitysync/aN5PysnXIuRECzcRbvHkjL7g0/Hintergrund_Agravity.png
```
It will be converted to:
@ -84,7 +84,7 @@ Z:\aN5PysnXIuRECzcRbvHkjL7g0\Hintergrund_Agravity.png
- **`allowed_urls`** (array): Allowed URL patterns for web content
- Empty array = no restriction
- Example: `["dev.agravity.io", "*.example.com"]`
- Example: `["wps.agravity.io", "*.example.com"]`
### Update Settings
@ -103,13 +103,6 @@ Z:\aN5PysnXIuRECzcRbvHkjL7g0\Hintergrund_Agravity.png
- Options: `"DEBUG"`, `"INFO"`, `"WARNING"`, `"ERROR"`, `"CRITICAL"`
- Default: `"INFO"`
- **`log_file`** (string, optional): Path to log file
- If `null` or not specified: Logs to `%APPDATA%\webdrop_bridge\logs\webdrop_bridge.log` (Windows) or `~/.local/share/webdrop_bridge/logs/webdrop_bridge.log` (macOS/Linux)
- If relative path: Resolved relative to the app data directory (same as above location)
- If absolute path: Used as-is
- Default: `null` (uses AppData directory for permissions compatibility)
- **Important**: Logs are always stored in the user's AppData directory to ensure the app can write logs in both development and installed scenarios
- **`enable_logging`** (boolean): Whether to write logs to file
- Default: `true`
@ -155,7 +148,7 @@ If no JSON config exists, WebDrop Bridge will load from `.env`:
```env
APP_NAME=WebDrop Bridge
WEBAPP_URL=https://dev.agravity.io/
WEBAPP_URL=https://wps.agravity.io/
ALLOWED_ROOTS=Z:/
LOG_LEVEL=INFO
WINDOW_WIDTH=1024
@ -182,14 +175,14 @@ WINDOW_HEIGHT=768
## Example Configurations
### Production (Agravity DEV)
### Production (Agravity WPS)
```json
{
"webapp_url": "https://dev.agravity.io/",
"webapp_url": "https://wps.agravity.io/",
"url_mappings": [
{
"url_prefix": "https://devagravitystg.file.core.windows.net/devagravitysync/",
"url_prefix": "https://wpsagravitystg.file.core.windows.net/wpsagravitysync/",
"local_path": "Z:"
}
],

View file

@ -1220,14 +1220,6 @@ Phase 4 Complete - Professional Features & Auto-Update system fully implemented
- ✅ Phase 4.3: Advanced Configuration & Settings UI (43 tests)
- ✅ Total Phase 4: 139 tests passing, 90%+ coverage
**MSI Update Support (Feb 20, 2026):**
- ✅ Added `<MajorUpgrade />` element to WiX configuration (build/WebDropBridge.wxs)
- ✅ Configured `Schedule="afterInstallInitialize"` for safe upgrade flow
- ✅ Implemented EXE version information setting in build script (build/scripts/build_windows.py)
- ✅ Added pefile dependency for version injection
- Impact: MSI installer now properly detects and applies version updates
- Status: Ready for Phase 5 release candidate builds
**Application Status:**
- Version: 1.0.0 (released Jan 28, 2026)
- Phase 1-3: Complete (core features, testing, build system)

File diff suppressed because one or more lines are too long

View file

@ -1,52 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
xmlns:ui="http://schemas.microsoft.com/wix/2010/ui">
<Product Id="*" Name="WebDrop Bridge" Language="1033" Version="0.5.0"
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Product Id="*" Name="WebDrop Bridge" Language="1033" Version="0.1.0"
Manufacturer="HIM-Tools"
UpgradeCode="12345678-1234-1234-1234-123456789012">
<Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" Platform="x64" />
<Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" />
<Media Id="1" Cabinet="WebDropBridge.cab" EmbedCab="yes" />
<!-- Required property for WixUI_InstallDir dialog set -->
<Property Id="WIXUI_INSTALLDIR" Value="INSTALLFOLDER" />
<!-- Application Icon -->
<Icon Id="AppIcon.ico" SourceFile="$(var.ResourcesDir)\icons\app.ico" />
<!-- Custom branding for InstallDir dialog set -->
<WixVariable Id="WixUIDialogBmp" Value="$(var.ResourcesDir)\icons\background.bmp" />
<WixVariable Id="WixUIBannerBmp" Value="$(var.ResourcesDir)\icons\banner.bmp" />
<WixVariable Id="WixUILicenseRtf" Value="$(var.ResourcesDir)\license.rtf" />
<!-- Installation UI dialogs -->
<UIRef Id="WixUI_InstallDir" />
<UIRef Id="WixUI_ErrorProgressText" />
<Feature Id="ProductFeature" Title="WebDrop Bridge" Level="1">
<ComponentGroupRef Id="AppFiles" />
<ComponentRef Id="MainExecutable" />
<ComponentRef Id="ProgramMenuShortcut" />
<ComponentRef Id="DesktopShortcut" />
</Feature>
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="ProgramFiles64Folder">
<Directory Id="ProgramFilesFolder">
<Directory Id="INSTALLFOLDER" Name="WebDrop Bridge" />
</Directory>
<Directory Id="ProgramMenuFolder">
<Directory Id="ApplicationProgramsFolder" Name="WebDrop Bridge"/>
</Directory>
<Directory Id="DesktopFolder" />
</Directory>
<DirectoryRef Id="INSTALLFOLDER">
<Component Id="MainExecutable" Guid="*">
<File Id="WebDropBridgeExe" Source="$(var.DistDir)\WebDropBridge.exe" KeyPath="yes"/>
</Component>
</DirectoryRef>
<DirectoryRef Id="ApplicationProgramsFolder">
<Component Id="ProgramMenuShortcut" Guid="*">
<Shortcut Id="ApplicationStartMenuShortcut"
Name="WebDrop Bridge"
Description="Web Drag-and-Drop Bridge"
Target="[INSTALLFOLDER]WebDropBridge.exe"
Icon="AppIcon.ico"
IconIndex="0"
WorkingDirectory="INSTALLFOLDER" />
<RemoveFolder Id="ApplicationProgramsFolderRemove"
On="uninstall" />
@ -58,23 +44,5 @@
KeyPath="yes" />
</Component>
</DirectoryRef>
<DirectoryRef Id="DesktopFolder">
<Component Id="DesktopShortcut" Guid="*">
<Shortcut Id="DesktopApplicationShortcut"
Name="WebDrop Bridge"
Description="Web Drag-and-Drop Bridge"
Target="[INSTALLFOLDER]WebDropBridge.exe"
Icon="AppIcon.ico"
IconIndex="0"
WorkingDirectory="INSTALLFOLDER" />
<RegistryValue Root="HKCU"
Key="Software\WebDropBridge"
Name="DesktopShortcut"
Type="integer"
Value="1"
KeyPath="yes" />
</Component>
</DirectoryRef>
</Product>
</Wix>

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -20,29 +20,22 @@ Usage:
"""
import sys
import os
from typing import Optional
# Fix Unicode output on Windows BEFORE any other imports
if sys.platform == "win32":
os.environ["PYTHONIOENCODING"] = "utf-8"
import io
# Reconfigure stdout/stderr for UTF-8 output
sys.stdout = io.TextIOWrapper(
sys.stdout.buffer, encoding="utf-8", errors="replace"
)
sys.stderr = io.TextIOWrapper(
sys.stderr.buffer, encoding="utf-8", errors="replace"
)
import subprocess
import os
import shutil
import argparse
from pathlib import Path
from datetime import datetime
# Import shared version utilities
from sync_version import get_current_version, do_sync_version
from version_utils import get_current_version
# Fix Unicode output on Windows
if sys.platform == "win32":
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8")
class WindowsBuilder:
@ -123,7 +116,8 @@ class WindowsBuilder:
result = subprocess.run(
cmd,
cwd=str(self.project_root),
text=True,
encoding="utf-8",
errors="replace",
env=env
)
@ -131,22 +125,14 @@ class WindowsBuilder:
print("❌ PyInstaller build failed")
return False
# Check if executable exists (now in WebDropBridge/WebDropBridge.exe due to COLLECT)
exe_path = self.dist_dir / "WebDropBridge" / "WebDropBridge.exe"
exe_path = self.dist_dir / "WebDropBridge.exe"
if not exe_path.exists():
print(f"❌ Executable not found at {exe_path}")
return False
print("✅ Executable built successfully")
print(f"📦 Output: {exe_path}")
# Calculate total dist size
total_size = sum(f.stat().st_size for f in self.dist_dir.glob("WebDropBridge/**/*") if f.is_file())
if total_size > 0:
print(f" Total size: {total_size / 1024 / 1024:.1f} MB")
# Set executable version information (required for MSI updates)
self.set_exe_version(exe_path)
print(f" Size: {exe_path.stat().st_size / 1024 / 1024:.1f} MB")
# Generate SHA256 checksum
self.generate_checksum(exe_path)
@ -172,52 +158,6 @@ class WindowsBuilder:
print(f" File: {checksum_file}")
print(f" Hash: {checksum}")
def set_exe_version(self, exe_path: Path) -> None:
"""Set executable file version for Windows.
This is important for MSI updates: Windows Installer compares file versions
to determine if files should be updated during a major upgrade.
Args:
exe_path: Path to the executable file
"""
print("\n🏷️ Setting executable version information...")
try:
import pefile
except ImportError:
print("⚠️ pefile not installed - skipping EXE version update")
print(" Note: Install with: pip install pefile")
print(" EXE version info will be blank (MSI updates may not work correctly)")
return
try:
pe = pefile.PE(str(exe_path))
# Parse version into 4-part format (Major, Minor, Build, Revision)
version_parts = self.version.split(".")
while len(version_parts) < 4:
version_parts.append("0")
file_version = tuple(int(v) for v in version_parts[:4])
# Set version resource if it exists
if hasattr(pe, "VS_FIXEDFILEINFO"):
pe.VS_FIXEDFILEINFO[0].FileVersionMS = (file_version[0] << 16) | file_version[1]
pe.VS_FIXEDFILEINFO[0].FileVersionLS = (file_version[2] << 16) | file_version[3]
pe.VS_FIXEDFILEINFO[0].ProductVersionMS = (file_version[0] << 16) | file_version[1]
pe.VS_FIXEDFILEINFO[0].ProductVersionLS = (file_version[2] << 16) | file_version[3]
# Write modified PE back to file
pe.write(filename=str(exe_path))
print(f"✅ Version set to {self.version}")
else:
print("⚠️ No version resource found in EXE")
except Exception as e:
print(f"⚠️ Could not set EXE version: {e}")
print(" MSI updates may not work correctly without file version info")
def create_msi(self) -> bool:
"""Create MSI installer using WiX Toolset.
@ -245,96 +185,49 @@ class WindowsBuilder:
print(" Or use: choco install wixtoolset")
return False
# Create base WiX source file
# Create WiX source file
if not self._create_wix_source():
return False
# Harvest application files using Heat
print(f" Harvesting application files...")
dist_folder = self.dist_dir / "WebDropBridge"
if not dist_folder.exists():
print(f"❌ Distribution folder not found: {dist_folder}")
return False
harvest_file = self.build_dir / "WebDropBridge_Files.wxs"
# Use Heat to harvest all files
heat_cmd = [
str(heat_exe),
"dir",
str(dist_folder),
"-cg", "AppFiles",
"-dr", "INSTALLFOLDER",
"-sfrag",
"-srd",
"-gg",
"-o", str(harvest_file),
]
result = subprocess.run(heat_cmd, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if result.returncode != 0:
print("⚠️ Heat harvest warnings (may be non-critical)")
if result.stderr:
print(result.stderr[:200]) # Show first 200 chars of errors
else:
print(f" ✓ Harvested files")
# Post-process harvested file to mark components as 64-bit
if harvest_file.exists():
content = harvest_file.read_text()
# Add Win64="yes" to all Component tags
content = content.replace(
'<Component ',
'<Component Win64="yes" '
)
harvest_file.write_text(content)
print(f" ✓ Marked components as 64-bit")
# Compile both WiX files
# Compile and link
wix_obj = self.build_dir / "WebDropBridge.wixobj"
wix_files_obj = self.build_dir / "WebDropBridge_Files.wixobj"
msi_output = self.dist_dir / f"WebDropBridge-{self.version}-Setup.msi"
# Run candle compiler - make sure to use correct source directory
# Run candle (compiler) - pass preprocessor variables
candle_cmd = [
str(candle_exe),
"-ext", "WixUIExtension",
f"-dDistDir={self.dist_dir}",
f"-dSourceDir={self.dist_dir}\\WebDropBridge", # Set SourceDir for Heat-generated files
f"-dResourcesDir={self.project_root}\\resources", # Set ResourcesDir for branding assets
"-o", str(self.build_dir) + "\\",
"-o",
str(wix_obj),
str(self.build_dir / "WebDropBridge.wxs"),
]
if harvest_file.exists():
candle_cmd.append(str(harvest_file))
print(f" Compiling WiX source...")
result = subprocess.run(candle_cmd, text=True, cwd=str(self.build_dir))
result = subprocess.run(
candle_cmd,
encoding="utf-8",
errors="replace"
)
if result.returncode != 0:
print("❌ WiX compilation failed")
return False
# Link MSI - include both obj files if harvest was successful
# Run light (linker)
light_cmd = [
str(light_exe),
"-ext", "WixUIExtension",
"-b", str(self.dist_dir / "WebDropBridge"), # Base path for source files
"-o", str(msi_output),
"-o",
str(msi_output),
str(wix_obj),
]
if wix_files_obj.exists():
light_cmd.append(str(wix_files_obj))
print(f" Linking MSI installer...")
result = subprocess.run(light_cmd, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
result = subprocess.run(
light_cmd,
encoding="utf-8",
errors="replace"
)
if result.returncode != 0:
print("❌ MSI linking failed")
if result.stdout:
print(f" Output: {result.stdout[:500]}")
if result.stderr:
print(f" Error: {result.stderr[:500]}")
return False
if not msi_output.exists():
@ -348,60 +241,42 @@ class WindowsBuilder:
return True
def _create_wix_source(self) -> bool:
"""Create WiX source file for MSI generation.
Creates per-machine installation (Program Files).
Installation requires admin rights, but the app does not.
"""
"""Create WiX source file for MSI generation."""
wix_content = f'''<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
xmlns:ui="http://schemas.microsoft.com/wix/2010/ui">
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Product Id="*" Name="WebDrop Bridge" Language="1033" Version="{self.version}"
Manufacturer="HIM-Tools"
UpgradeCode="12345678-1234-1234-1234-123456789012">
<Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" Platform="x64" />
<Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" />
<Media Id="1" Cabinet="WebDropBridge.cab" EmbedCab="yes" />
<!-- Required property for WixUI_InstallDir dialog set -->
<Property Id="WIXUI_INSTALLDIR" Value="INSTALLFOLDER" />
<!-- Application Icon -->
<Icon Id="AppIcon.ico" SourceFile="$(var.ResourcesDir)\\icons\\app.ico" />
<!-- Custom branding for InstallDir dialog set -->
<WixVariable Id="WixUIDialogBmp" Value="$(var.ResourcesDir)\\icons\\background.bmp" />
<WixVariable Id="WixUIBannerBmp" Value="$(var.ResourcesDir)\\icons\\banner.bmp" />
<WixVariable Id="WixUILicenseRtf" Value="$(var.ResourcesDir)\\license.rtf" />
<!-- Installation UI dialogs -->
<UIRef Id="WixUI_InstallDir" />
<UIRef Id="WixUI_ErrorProgressText" />
<Feature Id="ProductFeature" Title="WebDrop Bridge" Level="1">
<ComponentGroupRef Id="AppFiles" />
<ComponentRef Id="MainExecutable" />
<ComponentRef Id="ProgramMenuShortcut" />
<ComponentRef Id="DesktopShortcut" />
</Feature>
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="ProgramFiles64Folder">
<Directory Id="ProgramFilesFolder">
<Directory Id="INSTALLFOLDER" Name="WebDrop Bridge" />
</Directory>
<Directory Id="ProgramMenuFolder">
<Directory Id="ApplicationProgramsFolder" Name="WebDrop Bridge"/>
</Directory>
<Directory Id="DesktopFolder" />
</Directory>
<DirectoryRef Id="INSTALLFOLDER">
<Component Id="MainExecutable" Guid="*">
<File Id="WebDropBridgeExe" Source="$(var.DistDir)\\WebDropBridge.exe" KeyPath="yes"/>
</Component>
</DirectoryRef>
<DirectoryRef Id="ApplicationProgramsFolder">
<Component Id="ProgramMenuShortcut" Guid="*">
<Shortcut Id="ApplicationStartMenuShortcut"
Name="WebDrop Bridge"
Description="Web Drag-and-Drop Bridge"
Target="[INSTALLFOLDER]WebDropBridge.exe"
Icon="AppIcon.ico"
IconIndex="0"
WorkingDirectory="INSTALLFOLDER" />
<RemoveFolder Id="ApplicationProgramsFolderRemove"
On="uninstall" />
@ -413,24 +288,6 @@ class WindowsBuilder:
KeyPath="yes" />
</Component>
</DirectoryRef>
<DirectoryRef Id="DesktopFolder">
<Component Id="DesktopShortcut" Guid="*">
<Shortcut Id="DesktopApplicationShortcut"
Name="WebDrop Bridge"
Description="Web Drag-and-Drop Bridge"
Target="[INSTALLFOLDER]WebDropBridge.exe"
Icon="AppIcon.ico"
IconIndex="0"
WorkingDirectory="INSTALLFOLDER" />
<RegistryValue Root="HKCU"
Key="Software\\WebDropBridge"
Name="DesktopShortcut"
Type="integer"
Value="1"
KeyPath="yes" />
</Component>
</DirectoryRef>
</Product>
</Wix>
'''
@ -440,69 +297,6 @@ class WindowsBuilder:
print(f" Created WiX source: {wix_file}")
return True
def _generate_file_elements(self, folder: Path, parent_dir_ref: str, parent_rel_path: str, indent: int = 8, file_counter: Optional[dict] = None) -> str:
"""Generate WiX File elements for all files in a folder.
Args:
folder: Root folder to scan
parent_dir_ref: Parent WiX DirectoryRef ID
parent_rel_path: Relative path for component structure
indent: Indentation level
file_counter: Dictionary to track file IDs for uniqueness
Returns:
WiX XML string with all File elements
"""
if file_counter is None:
file_counter = {}
elements = []
indent_str = " " * indent
try:
# Get all files in current folder
for item in sorted(folder.iterdir()):
if item.is_file():
# Create unique File element ID using hash of full path
import hashlib
path_hash = hashlib.md5(str(item).encode()).hexdigest()[:8]
file_id = f"File_{path_hash}"
file_path = str(item)
elements.append(f'{indent_str}<File Id="{file_id}" Source="{file_path}" />')
elif item.is_dir() and item.name != "__pycache__":
# Recursively add files from subdirectories
sub_elements = self._generate_file_elements(
item, parent_dir_ref,
f"{parent_rel_path}/{item.name}",
indent,
file_counter
)
if sub_elements:
elements.append(sub_elements)
except PermissionError:
print(f" ⚠️ Permission denied accessing {folder}")
return "\n".join(elements)
def _sanitize_id(self, filename: str) -> str:
"""Sanitize filename to be a valid WiX identifier.
Args:
filename: Filename to sanitize
Returns:
Sanitized identifier
"""
# Remove extension and invalid characters
safe_name = filename.rsplit(".", 1)[0] if "." in filename else filename
# Replace invalid characters with underscores
safe_name = "".join(c if c.isalnum() or c == "_" else "_" for c in safe_name)
# Ensure it starts with a letter or underscore
if safe_name and not (safe_name[0].isalpha() or safe_name[0] == "_"):
safe_name = f"_{safe_name}"
# Limit length to avoid WiX ID limits
return safe_name[:50] if len(safe_name) > 50 else safe_name
def sign_executable(self, cert_path: str, password: str) -> bool:
"""Sign executable with certificate (optional).
@ -536,7 +330,8 @@ class WindowsBuilder:
result = subprocess.run(
cmd,
text=True
encoding="utf-8",
errors="replace"
)
if result.returncode != 0:
print("❌ Code signing failed")
@ -607,7 +402,7 @@ def main() -> int:
args = parser.parse_args()
print("🔄 Syncing version...")
do_sync_version()
sync_version()
try:
builder = WindowsBuilder(env_file=args.env_file)

View file

@ -25,17 +25,10 @@ param(
$ErrorActionPreference = "Stop"
# Get project root (PSScriptRoot is build/scripts, go up to project root with ..\..)
$projectRoot = Resolve-Path (Join-Path $PSScriptRoot "..\..")
# Resolve file paths relative to project root
$ExePath = Join-Path $projectRoot $ExePath
$ChecksumPath = Join-Path $projectRoot $ChecksumPath
$MsiPath = Join-Path $projectRoot $MsiPath
# Function to read version from .env or .env.example
function Get-VersionFromEnv {
# Use already resolved project root
# PSScriptRoot is build/scripts, go up to project root with ../../
$projectRoot = Resolve-Path (Join-Path $PSScriptRoot "..\..")
# Try .env first (runtime config), then .env.example (template)
$envFile = Join-Path $projectRoot ".env"

View file

@ -1 +0,0 @@
WebDropBridge.wxs

View file

@ -1,9 +1,9 @@
{
"app_name": "WebDrop Bridge",
"webapp_url": "https://dev.agravity.io/",
"webapp_url": "https://wps.agravity.io/",
"url_mappings": [
{
"url_prefix": "https://devagravitystg.file.core.windows.net/devagravitysync/",
"url_prefix": "https://wpsagravitystg.file.core.windows.net/wpsagravitysync/",
"local_path": "Z:"
}
],
@ -15,7 +15,7 @@
"auto_check_updates": true,
"update_check_interval_hours": 24,
"log_level": "INFO",
"log_file": null,
"log_file": "logs/webdrop_bridge.log",
"window_width": 1024,
"window_height": 768,
"enable_logging": true

View file

@ -14,7 +14,6 @@ isort>=5.12.0
# Building
pyinstaller>=6.0.0
pefile>=2023.2.7
# Documentation
sphinx>=7.0.0

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 356 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 451 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

View file

@ -1,18 +0,0 @@
{\rtf1\ansi\ansicpg1252\cocoartf2\cuc
{\fonttbl\f0\fswiss Helvetica;}
{\colortbl;\red255\green255\blue255;}
{\*\expandedcolortbl;;}
\margl1440\margr1440\vieww11900\viewh8605\viewkind0
\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfocus1
\f0\fs20 \cf0 WebDrop Bridge - License Agreement\
\
\b MIT License\b0\
\
Copyright (c) 2026 HIM-Tools\
\
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\
\
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\
\
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.}

View file

@ -23,8 +23,9 @@ from pathlib import Path
# Enable UTF-8 output on Windows
if sys.platform == "win32":
import os
os.environ["PYTHONIOENCODING"] = "utf-8"
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8")
# Import shared version utilities
sys.path.insert(0, str(Path(__file__).parent.parent / "build" / "scripts"))
@ -130,26 +131,34 @@ def update_changelog(version: str) -> None:
print(f"✓ Added version header to CHANGELOG.md for {version}")
def do_sync_version(version: str | None = None) -> int:
def main() -> int:
"""Sync version across project.
Updates __init__.py (source of truth) and changelog.
Config and pyproject.toml automatically read from __init__.py.
Args:
version: Version to set (if None, reads from __init__.py)
Returns:
0 on success, 1 on error
"""
parser = argparse.ArgumentParser(
description="Sync version from __init__.py to dependent files"
)
parser.add_argument(
"--version",
type=str,
help="Version to set (if not provided, reads from __init__.py)",
)
args = parser.parse_args()
try:
if version:
if not re.match(r"^\d+\.\d+\.\d+", version):
if args.version:
if not re.match(r"^\d+\.\d+\.\d+", args.version):
print(
"❌ Invalid version format. Use semantic versioning"
" (e.g., 1.2.3)"
)
return 1
version = args.version
update_init_version(version)
else:
version = get_current_version_from_init()
@ -166,26 +175,5 @@ def do_sync_version(version: str | None = None) -> int:
return 1
def sync_version() -> int:
"""Sync version across project (command-line interface).
Parses command-line arguments and calls do_sync_version().
This function is only called when the script is run directly.
Returns:
0 on success, 1 on error
"""
parser = argparse.ArgumentParser(
description="Sync version from __init__.py to dependent files"
)
parser.add_argument(
"--version",
type=str,
help="Version to set (if not provided, reads from __init__.py)",
)
args = parser.parse_args()
return do_sync_version(args.version)
if __name__ == "__main__":
sys.exit(sync_version())
sys.exit(main())

View file

@ -49,7 +49,7 @@ 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 (default: https://dev.agravity.io/)
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
@ -126,15 +126,8 @@ class Config:
# Get log file path
log_file = None
if data.get("enable_logging", True):
log_file_str = data.get("log_file", None)
if log_file_str:
log_file = Path(log_file_str)
# If relative path, resolve relative to app data directory instead of cwd
if not log_file.is_absolute():
log_file = Config.get_default_log_dir() / log_file
else:
# Use default log path in app data
log_file = Config.get_default_log_path()
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__}")
@ -146,7 +139,7 @@ class Config:
log_file=log_file,
allowed_roots=allowed_roots,
allowed_urls=data.get("allowed_urls", []),
webapp_url=data.get("webapp_url", "https://dev.agravity.io/"),
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),
@ -185,10 +178,10 @@ class Config:
else:
app_version = os.getenv("APP_VERSION")
log_level = os.getenv("LOG_LEVEL", "INFO").upper()
log_file_str = os.getenv("LOG_FILE", None)
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", "https://dev.agravity.io/")
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
@ -234,14 +227,7 @@ class Config:
# Create log file path if logging enabled
log_file = None
if enable_logging:
if log_file_str:
log_file = Path(log_file_str)
# If relative path, resolve relative to app data directory instead of cwd
if not log_file.is_absolute():
log_file = Config.get_default_log_dir() / log_file
else:
# Use default log path in app data
log_file = Config.get_default_log_path()
log_file = Path(log_file_str).resolve()
# Validate webapp URL is not empty
if not webapp_url:
@ -298,9 +284,7 @@ class Config:
"""Save configuration to JSON file.
Args:
config_path: Path to save configuration to
Creates parent directories if they don't exist.
config_path: Path to save configuration
"""
data = {
"app_name": self.app_name,
@ -334,7 +318,7 @@ class Config:
"""Get the default configuration file path.
Returns:
Path to default config file in user's AppData/Roaming
Path to default config file
"""
import platform
if platform.system() == "Windows":
@ -343,32 +327,6 @@ class Config:
base = Path.home() / ".config"
return base / "webdrop_bridge" / "config.json"
@staticmethod
def get_default_log_dir() -> Path:
"""Get the default directory for log files.
Always uses user's AppData directory to ensure permissions work
correctly in both development and installed scenarios.
Returns:
Path to default logs directory in user's AppData/Roaming
"""
import platform
if platform.system() == "Windows":
base = Path.home() / "AppData" / "Roaming"
else:
base = Path.home() / ".local" / "share"
return base / "webdrop_bridge" / "logs"
@staticmethod
def get_default_log_path() -> Path:
"""Get the default log file path.
Returns:
Path to default log file in user's AppData/Roaming/webdrop_bridge/logs
"""
return Config.get_default_log_dir() / "webdrop_bridge.log"
def __repr__(self) -> str:
"""Return developer-friendly representation."""
return (

View file

@ -32,7 +32,7 @@ class URLConverter:
Example:
>>> converter.convert_url_to_path(
... "https://devagravitystg.file.core.windows.net/devagravitysync/aN5PysnXIuRECzcRbvHkjL7g0/file.png"
... "https://wpsagravitystg.file.core.windows.net/wpsagravitysync/aN5PysnXIuRECzcRbvHkjL7g0/file.png"
... )
Path("Z:/aN5PysnXIuRECzcRbvHkjL7g0/file.png")
"""

View file

@ -4,7 +4,6 @@ import asyncio
import json
import logging
import re
import sys
from datetime import datetime
from pathlib import Path
from typing import Optional
@ -281,14 +280,7 @@ class MainWindow(QMainWindow):
)
# Set window icon
# Support both development mode and PyInstaller bundle
if hasattr(sys, '_MEIPASS'):
# Running as PyInstaller bundle
icon_path = Path(sys._MEIPASS) / "resources" / "icons" / "app.ico" # type: ignore
else:
# Running in development mode
icon_path = Path(__file__).parent.parent.parent.parent / "resources" / "icons" / "app.ico"
icon_path = Path(__file__).parent.parent.parent.parent / "resources" / "icons" / "app.ico"
if icon_path.exists():
self.setWindowIcon(QIcon(str(icon_path)))
logger.debug(f"Window icon set from {icon_path}")
@ -448,72 +440,21 @@ class MainWindow(QMainWindow):
config_code = self._generate_config_injection_script()
# Load bridge script from file
# Try multiple paths to support dev mode, PyInstaller bundle, and MSI installation
# 1. Development mode: __file__.parent / script.js
# 2. PyInstaller bundle: sys._MEIPASS / webdrop_bridge / ui / script.js
# 3. MSI installation: Same directory as executable
script_path = None
download_interceptor_path = None
# List of paths to try in order of preference
search_paths = []
# 1. Development mode
search_paths.append(Path(__file__).parent / "bridge_script_intercept.js")
# 2. PyInstaller bundle (via sys._MEIPASS)
if hasattr(sys, '_MEIPASS'):
search_paths.append(Path(sys._MEIPASS) / "webdrop_bridge" / "ui" / "bridge_script_intercept.js") # type: ignore
# 3. Installed executable's directory (handles MSI installation where all files are packaged together)
exe_dir = Path(sys.executable).parent
search_paths.append(exe_dir / "webdrop_bridge" / "ui" / "bridge_script_intercept.js")
# Find the bridge script
for path in search_paths:
if path.exists():
script_path = path
logger.debug(f"Found bridge script at: {script_path}")
break
if script_path is None:
# Log all attempted paths for debugging
logger.error("Bridge script NOT found at any expected location:")
for i, path in enumerate(search_paths, 1):
logger.error(f" [{i}] {path} (exists: {path.exists()})")
logger.error(f"sys._MEIPASS: {getattr(sys, '_MEIPASS', 'NOT SET')}")
logger.error(f"sys.executable: {sys.executable}")
logger.error(f"__file__: {__file__}")
# Using intercept script - prevents browser drag, hands off to Qt
script_path = Path(__file__).parent / "bridge_script_intercept.js"
try:
if script_path is None:
raise FileNotFoundError("bridge_script_intercept.js not found in any expected location")
with open(script_path, 'r', encoding='utf-8') as f:
bridge_code = f.read()
# Load download interceptor using similar search path logic
download_search_paths = []
download_search_paths.append(Path(__file__).parent / "download_interceptor.js")
if hasattr(sys, '_MEIPASS'):
download_search_paths.append(Path(sys._MEIPASS) / "webdrop_bridge" / "ui" / "download_interceptor.js") # type: ignore
download_search_paths.append(exe_dir / "webdrop_bridge" / "ui" / "download_interceptor.js")
# Load download interceptor
download_interceptor_path = Path(__file__).parent / "download_interceptor.js"
download_interceptor_code = ""
for path in download_search_paths:
if path.exists():
download_interceptor_path = path
break
if download_interceptor_path:
try:
with open(download_interceptor_path, 'r', encoding='utf-8') as f:
download_interceptor_code = f.read()
logger.debug(f"Loaded download interceptor from {download_interceptor_path}")
except (OSError, IOError) as e:
logger.warning(f"Download interceptor exists but failed to load: {e}")
else:
logger.debug("Download interceptor not found (optional)")
try:
with open(download_interceptor_path, 'r', encoding='utf-8') as f:
download_interceptor_code = f.read()
logger.debug(f"Loaded download interceptor from {download_interceptor_path}")
except (OSError, IOError) as e:
logger.warning(f"Download interceptor not found: {e}")
# Combine: qwebchannel.js + config + bridge script + download interceptor
combined_code = qwebchannel_code + "\n\n" + config_code + "\n\n" + bridge_code
@ -532,13 +473,9 @@ class MainWindow(QMainWindow):
script.setSourceCode(combined_code)
self.web_view.page().scripts().insert(script)
logger.debug(f"✅ Successfully installed bridge script")
logger.debug(f" Script size: {len(combined_code)} chars")
logger.debug(f" Loaded from: {script_path}")
logger.debug(f"Installed bridge script from {script_path}")
except (OSError, IOError) as e:
logger.error(f"❌ Failed to load bridge script: {e}")
logger.error(f" This will break drag-and-drop functionality!")
# Don't re-raise - allow app to start (will show error in logs)
logger.warning(f"Failed to load bridge script: {e}")
def _generate_config_injection_script(self) -> str:
"""Generate JavaScript code that injects configuration.
@ -1158,21 +1095,10 @@ class MainWindow(QMainWindow):
f"<b>{self.config.app_name}</b><br>"
f"Version: {self.config.app_version}<br>"
f"<br>"
f"Bridges web-based drag-and-drop workflows with native file operations "
f"for professional desktop applications.<br>"
f"A professional Qt-based desktop application that converts "
f"web-based drag-and-drop text paths into native file operations.<br>"
f"<br>"
f"<b>Product of:</b><br>"
f"<b>Hörl Information Management GmbH</b><br>"
f"Silberburgstraße 126<br>"
f"70176 Stuttgart, Germany<br>"
f"<br>"
f"<small>"
f"<b>Email:</b> <a href='mailto:info@hoerl-im.de'>info@hoerl-im.de</a><br>"
f"<b>Phone:</b> +49 (0) 711 933 42 52 0<br>"
f"<b>Web:</b> <a href='https://www.hoerl-im.de/'>https://www.hoerl-im.de/</a><br>"
f"</small>"
f"<br>"
f"<small>© 2026 Hörl Information Management GmbH. All rights reserved.</small>"
f"<small>© 2026 WebDrop Bridge Contributors</small>"
)
QMessageBox.about(self, f"About {self.config.app_name}", about_text)

View file

@ -1,6 +1,5 @@
"""Settings dialog for configuration management."""
import logging
from pathlib import Path
from typing import List, Optional
@ -16,8 +15,6 @@ from PySide6.QtWidgets import (
QListWidgetItem,
QPushButton,
QSpinBox,
QTableWidget,
QTableWidgetItem,
QTabWidget,
QVBoxLayout,
QWidget,
@ -25,9 +22,6 @@ from PySide6.QtWidgets import (
from webdrop_bridge.config import Config, ConfigurationError
from webdrop_bridge.core.config_manager import ConfigExporter, ConfigProfile, ConfigValidator
from webdrop_bridge.utils.logging import reconfigure_logging
logger = logging.getLogger(__name__)
class SettingsDialog(QDialog):
@ -64,7 +58,6 @@ class SettingsDialog(QDialog):
self.tabs = QTabWidget()
# Add tabs
self.tabs.addTab(self._create_web_source_tab(), "Web Source")
self.tabs.addTab(self._create_paths_tab(), "Paths")
self.tabs.addTab(self._create_urls_tab(), "URLs")
self.tabs.addTab(self._create_logging_tab(), "Logging")
@ -83,200 +76,6 @@ class SettingsDialog(QDialog):
self.setLayout(layout)
def accept(self) -> None:
"""Handle OK button - save configuration changes to file.
Validates configuration and saves to the default config path.
Applies log level changes immediately in the running application.
If validation or save fails, shows error and stays in dialog.
"""
try:
# Get updated configuration data from UI
config_data = self.get_config_data()
# Convert URL mappings from dict to URLMapping objects
from webdrop_bridge.config import URLMapping
url_mappings = [
URLMapping(
url_prefix=m["url_prefix"],
local_path=m["local_path"]
)
for m in config_data["url_mappings"]
]
# Update the config object with new values
old_log_level = self.config.log_level
self.config.log_level = config_data["log_level"]
self.config.log_file = Path(config_data["log_file"]) if config_data["log_file"] else None
self.config.allowed_roots = [Path(r).resolve() for r in config_data["allowed_roots"]]
self.config.allowed_urls = config_data["allowed_urls"]
self.config.webapp_url = config_data["webapp_url"]
self.config.url_mappings = url_mappings
self.config.window_width = config_data["window_width"]
self.config.window_height = config_data["window_height"]
# Save to file (creates parent dirs if needed)
config_path = Config.get_default_config_path()
self.config.to_file(config_path)
logger.info(f"Configuration saved to {config_path}")
logger.info(f" Log level: {self.config.log_level} (was: {old_log_level})")
logger.info(f" Window size: {self.config.window_width}x{self.config.window_height}")
# Apply log level change immediately to running application
if old_log_level != self.config.log_level:
logger.info(f"🔄 Updating log level: {old_log_level}{self.config.log_level}")
reconfigure_logging(
logger_name="webdrop_bridge",
level=self.config.log_level,
log_file=self.config.log_file
)
logger.info(f"✅ Log level updated to {self.config.log_level}")
# Call parent accept to close dialog
super().accept()
except ConfigurationError as e:
logger.error(f"Configuration error: {e}")
self._show_error(f"Configuration Error:\n\n{e}")
except Exception as e:
logger.error(f"Failed to save configuration: {e}", exc_info=True)
self._show_error(f"Failed to save configuration:\n\n{e}")
def _create_web_source_tab(self) -> QWidget:
"""Create web source configuration tab."""
from PySide6.QtWidgets import QTableWidget, QTableWidgetItem
widget = QWidget()
layout = QVBoxLayout()
# Webapp URL configuration
layout.addWidget(QLabel("Web Application URL:"))
url_layout = QHBoxLayout()
self.webapp_url_input = QLineEdit()
self.webapp_url_input.setText(self.config.webapp_url)
self.webapp_url_input.setPlaceholderText("e.g., http://localhost:8080 or file:///./webapp/index.html")
url_layout.addWidget(self.webapp_url_input)
open_btn = QPushButton("Open")
open_btn.clicked.connect(self._open_webapp_url)
url_layout.addWidget(open_btn)
layout.addLayout(url_layout)
# URL Mappings (Azure Blob URL → Local Path)
layout.addWidget(QLabel("URL Mappings (Azure Blob Storage → Local Paths):"))
# Create table for URL mappings
self.url_mappings_table = QTableWidget()
self.url_mappings_table.setColumnCount(2)
self.url_mappings_table.setHorizontalHeaderLabels(["URL Prefix", "Local Path"])
self.url_mappings_table.horizontalHeader().setStretchLastSection(True)
# Populate from config
for mapping in self.config.url_mappings:
row = self.url_mappings_table.rowCount()
self.url_mappings_table.insertRow(row)
self.url_mappings_table.setItem(row, 0, QTableWidgetItem(mapping.url_prefix))
self.url_mappings_table.setItem(row, 1, QTableWidgetItem(mapping.local_path))
layout.addWidget(self.url_mappings_table)
# Buttons for URL mapping management
button_layout = QHBoxLayout()
add_mapping_btn = QPushButton("Add Mapping")
add_mapping_btn.clicked.connect(self._add_url_mapping)
button_layout.addWidget(add_mapping_btn)
edit_mapping_btn = QPushButton("Edit Selected")
edit_mapping_btn.clicked.connect(self._edit_url_mapping)
button_layout.addWidget(edit_mapping_btn)
remove_mapping_btn = QPushButton("Remove Selected")
remove_mapping_btn.clicked.connect(self._remove_url_mapping)
button_layout.addWidget(remove_mapping_btn)
layout.addLayout(button_layout)
layout.addStretch()
widget.setLayout(layout)
return widget
def _open_webapp_url(self) -> None:
"""Open the webapp URL in the default browser."""
import webbrowser
url = self.webapp_url_input.text().strip()
if url:
# Handle file:// URLs
try:
webbrowser.open(url)
except Exception as e:
logger.error(f"Failed to open URL: {e}")
self._show_error(f"Failed to open URL:\n\n{e}")
def _add_url_mapping(self) -> None:
"""Add new URL mapping."""
from PySide6.QtWidgets import QInputDialog
url_prefix, ok1 = QInputDialog.getText(
self,
"Add URL Mapping",
"Enter Azure Blob Storage URL prefix:\n(e.g., https://myblob.blob.core.windows.net/container/)"
)
if ok1 and url_prefix:
local_path, ok2 = QInputDialog.getText(
self,
"Add URL Mapping",
"Enter local file system path:\n(e.g., C:\\Share or /mnt/share)"
)
if ok2 and local_path:
row = self.url_mappings_table.rowCount()
self.url_mappings_table.insertRow(row)
self.url_mappings_table.setItem(row, 0, QTableWidgetItem(url_prefix))
self.url_mappings_table.setItem(row, 1, QTableWidgetItem(local_path))
def _edit_url_mapping(self) -> None:
"""Edit selected URL mapping."""
from PySide6.QtWidgets import QInputDialog
current_row = self.url_mappings_table.currentRow()
if current_row < 0:
self._show_error("Please select a mapping to edit")
return
url_prefix = self.url_mappings_table.item(current_row, 0).text()
local_path = self.url_mappings_table.item(current_row, 1).text()
new_url_prefix, ok1 = QInputDialog.getText(
self,
"Edit URL Mapping",
"Enter Azure Blob Storage URL prefix:",
text=url_prefix
)
if ok1 and new_url_prefix:
new_local_path, ok2 = QInputDialog.getText(
self,
"Edit URL Mapping",
"Enter local file system path:",
text=local_path
)
if ok2 and new_local_path:
self.url_mappings_table.setItem(current_row, 0, QTableWidgetItem(new_url_prefix))
self.url_mappings_table.setItem(current_row, 1, QTableWidgetItem(new_local_path))
def _remove_url_mapping(self) -> None:
"""Remove selected URL mapping."""
current_row = self.url_mappings_table.currentRow()
if current_row >= 0:
self.url_mappings_table.removeRow(current_row)
def _create_paths_tab(self) -> QWidget:
"""Create paths configuration tab."""
widget = QWidget()
@ -615,14 +414,7 @@ class SettingsDialog(QDialog):
"log_file": self.log_file_input.text() or None,
"allowed_roots": [self.paths_list.item(i).text() for i in range(self.paths_list.count())],
"allowed_urls": [self.urls_list.item(i).text() for i in range(self.urls_list.count())],
"webapp_url": self.webapp_url_input.text().strip(),
"url_mappings": [
{
"url_prefix": self.url_mappings_table.item(i, 0).text(),
"local_path": self.url_mappings_table.item(i, 1).text()
}
for i in range(self.url_mappings_table.rowCount())
],
"webapp_url": self.config.webapp_url,
"window_width": self.width_spin.value(),
"window_height": self.height_spin.value(),
"enable_logging": self.config.enable_logging,

View file

@ -154,66 +154,6 @@ def setup_logging(
return logger
def reconfigure_logging(
logger_name: str = "webdrop_bridge",
level: str = "INFO",
log_file: Optional[Path] = None,
) -> None:
"""Reconfigure existing logger at runtime (e.g., from settings dialog).
Updates the log level and log file for a running logger.
Useful when user changes logging settings in the UI.
Args:
logger_name: Name of logger to reconfigure
level: New logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
log_file: Optional path to log file
Raises:
KeyError: If level is not a valid logging level
"""
try:
numeric_level = getattr(logging, level.upper())
except AttributeError as e:
raise KeyError(f"Invalid logging level: {level}") from e
logger = logging.getLogger(logger_name)
logger.setLevel(numeric_level)
# Update level on all existing handlers
for handler in logger.handlers:
handler.setLevel(numeric_level)
# If log file changed, remove old file handler and add new one
if log_file:
# Remove old file handlers
for handler in logger.handlers[:]:
if isinstance(handler, logging.handlers.RotatingFileHandler):
handler.close()
logger.removeHandler(handler)
try:
# Create parent directories if needed
log_file.parent.mkdir(parents=True, exist_ok=True)
# Create new file handler
file_handler = logging.handlers.RotatingFileHandler(
log_file,
maxBytes=10 * 1024 * 1024, # 10 MB
backupCount=5,
encoding="utf-8",
)
file_handler.setLevel(numeric_level)
# Use same formatter as existing handlers
if logger.handlers:
file_handler.setFormatter(logger.handlers[0].formatter)
logger.addHandler(file_handler)
except (OSError, IOError) as e:
raise ValueError(f"Cannot write to log file {log_file}: {e}") from e
def get_logger(name: str = __name__) -> logging.Logger:
"""Get a logger instance for a module.

View file

@ -1,23 +0,0 @@
#!/usr/bin/env python
"""Test MSI creation."""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent / "build" / "scripts"))
from build_windows import WindowsBuilder
if __name__ == "__main__":
builder = WindowsBuilder()
print("Creating MSI installer...")
result = builder.create_msi()
print(f"MSI Creation Result: {result}")
# Check if MSI was created
msi_path = builder.dist_dir / f"WebDropBridge-{builder.version}-Setup.msi"
if msi_path.exists():
print(f"\n✅ MSI created successfully: {msi_path}")
print(f" Size: {msi_path.stat().st_size / 1024 / 1024:.1f} MB")
else:
print(f"\n❌ MSI not found: {msi_path}")

View file

@ -19,7 +19,7 @@ def test_config(tmp_path):
log_file=None,
allowed_roots=[tmp_path],
allowed_urls=[],
webapp_url="https://dev.agravity.io/",
webapp_url="https://wps.agravity.io/",
url_mappings=[],
check_file_exists=True,
)
@ -135,7 +135,7 @@ class TestDragInterceptorAzureURL:
webapp_url="https://test.com/",
url_mappings=[
URLMapping(
url_prefix="https://devagravitystg.file.core.windows.net/devagravitysync/",
url_prefix="https://wpsagravitystg.file.core.windows.net/wpsagravitysync/",
local_path=str(tmp_path)
)
],
@ -144,7 +144,7 @@ class TestDragInterceptorAzureURL:
interceptor = DragInterceptor(config)
# Azure URL
azure_url = "https://devagravitystg.file.core.windows.net/devagravitysync/test.png"
azure_url = "https://wpsagravitystg.file.core.windows.net/wpsagravitysync/test.png"
# Mock the drag operation
with patch("webdrop_bridge.core.drag_interceptor.QDrag") as mock_drag:

View file

@ -18,10 +18,10 @@ def test_config():
log_file=None,
allowed_roots=[],
allowed_urls=[],
webapp_url="https://dev.agravity.io/",
webapp_url="https://wps.agravity.io/",
url_mappings=[
URLMapping(
url_prefix="https://devagravitystg.file.core.windows.net/devagravitysync/",
url_prefix="https://wpsagravitystg.file.core.windows.net/wpsagravitysync/",
local_path="Z:"
),
URLMapping(
@ -40,7 +40,7 @@ def converter(test_config):
def test_convert_simple_url(converter):
"""Test converting a simple Azure URL to local path."""
url = "https://devagravitystg.file.core.windows.net/devagravitysync/test/file.png"
url = "https://wpsagravitystg.file.core.windows.net/wpsagravitysync/test/file.png"
result = converter.convert_url_to_path(url)
assert result is not None
@ -49,7 +49,7 @@ def test_convert_simple_url(converter):
def test_convert_url_with_special_characters(converter):
"""Test URL with special characters (URL encoded)."""
url = "https://devagravitystg.file.core.windows.net/devagravitysync/folder/file%20with%20spaces.png"
url = "https://wpsagravitystg.file.core.windows.net/wpsagravitysync/folder/file%20with%20spaces.png"
result = converter.convert_url_to_path(url)
assert result is not None
@ -58,7 +58,7 @@ def test_convert_url_with_special_characters(converter):
def test_convert_url_with_subdirectories(converter):
"""Test URL with deep directory structure."""
url = "https://devagravitystg.file.core.windows.net/devagravitysync/aN5PysnXIuRECzcRbvHkjL7g0/subfolder/file.png"
url = "https://wpsagravitystg.file.core.windows.net/wpsagravitysync/aN5PysnXIuRECzcRbvHkjL7g0/subfolder/file.png"
result = converter.convert_url_to_path(url)
assert result is not None
@ -88,7 +88,7 @@ def test_convert_none_url(converter):
def test_is_azure_url_positive(converter):
"""Test recognizing valid Azure URLs."""
url = "https://devagravitystg.file.core.windows.net/devagravitysync/file.png"
url = "https://wpsagravitystg.file.core.windows.net/wpsagravitysync/file.png"
assert converter.is_azure_url(url) is True
@ -101,7 +101,7 @@ def test_is_azure_url_negative(converter):
def test_multiple_mappings(converter):
"""Test that correct mapping is used for URL."""
url1 = "https://devagravitystg.file.core.windows.net/devagravitysync/file.png"
url1 = "https://wpsagravitystg.file.core.windows.net/wpsagravitysync/file.png"
url2 = "https://other.blob.core.windows.net/container/file.png"
result1 = converter.convert_url_to_path(url1)
@ -133,7 +133,7 @@ def test_url_mapping_adds_trailing_slash():
def test_convert_url_example_from_docs(converter):
"""Test the exact example from documentation."""
url = "https://devagravitystg.file.core.windows.net/devagravitysync/aN5PysnXIuRECzcRbvHkjL7g0/Hintergrund_Agravity.png"
url = "https://wpsagravitystg.file.core.windows.net/wpsagravitysync/aN5PysnXIuRECzcRbvHkjL7g0/Hintergrund_Agravity.png"
result = converter.convert_url_to_path(url)
assert result is not None

View file

@ -175,13 +175,13 @@
<div class="drag-item" draggable="true" id="dragItem3">
<div class="icon">☁️</div>
<h3>Azure Blob Storage Image</h3>
<p id="path3">https://devagravitystg.file.core.windows.net/devagravitysync/aN5PysnXIuRECzcRbvHkjL7g0/Hintergrund_Agravity.png</p>
<p id="path3">https://wpsagravitystg.file.core.windows.net/wpsagravitysync/aN5PysnXIuRECzcRbvHkjL7g0/Hintergrund_Agravity.png</p>
</div>
<div class="drag-item" draggable="true" id="dragItem4">
<div class="icon">☁️</div>
<h3>Azure Blob Storage Document</h3>
<p id="path4">https://devagravitystg.file.core.windows.net/devagravitysync/test/document.pdf</p>
<p id="path4">https://wpsagravitystg.file.core.windows.net/wpsagravitysync/test/document.pdf</p>
</div>
</div>