Compare commits
No commits in common. "7873d0a060553bbe2bfeab0fe3e4aaecef979a8b" and "dffc925bb6bb11a8d771d1f481a19d509389c84e" have entirely different histories.
7873d0a060
...
dffc925bb6
27 changed files with 132 additions and 12665 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:"
|
||||
}
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
WebDropBridge.wxs
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 |
Binary file not shown.
|
Before Width: | Height: | Size: 356 KiB After Width: | Height: | Size: 9.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 451 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 83 KiB |
|
|
@ -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.}
|
||||
|
|
@ -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())
|
||||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
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)")
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
23
test_msi.py
23
test_msi.py
|
|
@ -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}")
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue