diff --git a/.env.example b/.env.example index 6f87e30..6a13a2f 100644 --- a/.env.example +++ b/.env.example @@ -2,7 +2,7 @@ # Application APP_NAME=WebDrop Bridge -APP_VERSION=0.8.6 +APP_VERSION=0.8.5 # Web App WEBAPP_URL=file:///./webapp/index.html diff --git a/build/scripts/create_release.ps1 b/build/scripts/create_release.ps1 index 187c9aa..488c9bb 100644 --- a/build/scripts/create_release.ps1 +++ b/build/scripts/create_release.ps1 @@ -165,123 +165,24 @@ if (Test-Path $manifestOutput) { $artifactPaths.Add($manifestOutput) $assetMap = Get-AssetMap -Assets $releaseInfo.assets -$artifactsToUpload = New-Object System.Collections.Generic.List[string] +$curlAuth = "$ForgejoUser`:$ForgejoPW" +$uploadUrl = "$ForgejoUrl/api/v1/repos/$Repo/releases/$releaseId/assets" + foreach ($artifact in $artifactPaths) { $assetName = [System.IO.Path]::GetFileName($artifact) - $extension = [System.IO.Path]::GetExtension($artifact).ToLowerInvariant() - - if ($extension -eq ".msi" -and $assetMap.ContainsKey($assetName)) { - $localSize = (Get-Item $artifact).Length - $remoteSize = [int64]$assetMap[$assetName].size - if ($localSize -eq $remoteSize) { - Write-Host "[OK] Skipping already uploaded MSI $assetName ($([math]::Round($localSize / 1MB, 2)) MB)" -ForegroundColor Cyan - continue - } + if ($assetMap.ContainsKey($assetName)) { + $existingAsset = $assetMap[$assetName] + Invoke-RestMethod -Uri "$ForgejoUrl/api/v1/repos/$Repo/releases/$releaseId/assets/$($existingAsset.id)" -Method DELETE -Headers $headers -TimeoutSec 30 | Out-Null + Write-Host "[OK] Replaced existing asset $assetName" -ForegroundColor Yellow } - $artifactsToUpload.Add($artifact) -} - -if ($artifactsToUpload.Count -eq 0) { - Write-Host "[OK] All release assets already uploaded." -ForegroundColor Green - Write-Host "View at: $ForgejoUrl/$Repo/releases/tag/v$Version" -ForegroundColor Cyan - exit 0 -} - - # Use Python requests library for more reliable large file uploads -$pythonUploadScript = @" -import sys -import requests -from requests.auth import HTTPBasicAuth -from pathlib import Path -import time - -upload_url = sys.argv[1] -artifacts = sys.argv[2:] -username = '$ForgejoUser' -password = '$ForgejoPW' -delete_url_template = '${ForgejoUrl}/api/v1/repos/${Repo}/releases/$releaseId/assets/{}' -release_info_url = '${ForgejoUrl}/api/v1/repos/${Repo}/releases/$releaseId' - -session = requests.Session() -session.auth = HTTPBasicAuth(username, password) -session.headers.update({'Connection': 'close'}) - -def upload_with_retry(artifact_path, max_retries=3): - asset_name = Path(artifact_path).name - - # Check if asset already exists and delete it - try: - release_response = session.get(release_info_url, timeout=30) - release_response.raise_for_status() - for asset in release_response.json().get('assets', []): - if asset['name'] == asset_name: - delete_resp = session.delete(delete_url_template.format(asset['id']), timeout=30) - delete_resp.raise_for_status() - print(f'[OK] Replaced existing asset {asset_name}', file=sys.stderr) - break - except Exception as e: - print(f'Warning checking existing assets: {e}', file=sys.stderr) - - # Upload file with streaming and retries - retryable_status_codes = {429, 502, 503, 504} - for attempt in range(max_retries): - try: - if attempt > 0: - print(f' Retry {attempt} of {max_retries}...', file=sys.stderr) - time.sleep(min(10, 2 * attempt)) - - with open(artifact_path, 'rb') as f: - files = {'attachment': (asset_name, f)} - response = session.post( - upload_url, - files=files, - timeout=900, # 15 minute timeout - stream=False - ) - - if response.status_code in [200, 201]: - print(f'[OK] Uploaded {asset_name}') - return True - - if response.status_code in retryable_status_codes: - if attempt >= max_retries - 1: - print(f'ERROR uploading {asset_name} (HTTP {response.status_code} after {max_retries} retries)') - print(response.text) - sys.exit(1) - continue - - print(f'ERROR uploading {asset_name} (HTTP {response.status_code})') - print(response.text) - sys.exit(1) - except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e: - if attempt >= max_retries - 1: - print(f'ERROR uploading {asset_name}: Connection failed after {max_retries} retries') - print(str(e)) - sys.exit(1) - time.sleep(min(10, 2 * (attempt + 1))) - except Exception as e: - print(f'ERROR uploading {asset_name}: {e}') - sys.exit(1) - -for artifact_path in artifacts: - upload_with_retry(artifact_path) - -print(f'[OK] All files uploaded successfully') -"@ - -$uploadScriptPath = ([System.IO.Path]::GetTempFileName() -replace 'tmp$', 'py') -Set-Content -Path $uploadScriptPath -Value $pythonUploadScript -Encoding UTF8 - -try { - $uploadUrl = "$ForgejoUrl/api/v1/repos/$Repo/releases/$releaseId/assets" - & $pythonExe $uploadScriptPath $uploadUrl @artifactsToUpload - if ($LASTEXITCODE -ne 0) { - exit 1 + $response = curl.exe -s -X POST -u $curlAuth -F "attachment=@$artifact" $uploadUrl + if ($response -like "*error*" -or $response -like "*404*") { + Write-Host "WARNING: Could not upload $artifact : $response" -ForegroundColor Yellow + } + else { + Write-Host "[OK] Uploaded $assetName" -ForegroundColor Green } -} -finally { - Remove-Item $uploadScriptPath -ErrorAction SilentlyContinue } Write-Host "`n[OK] Release complete!" -ForegroundColor Green diff --git a/src/webdrop_bridge/__init__.py b/src/webdrop_bridge/__init__.py index 604705f..6baf92f 100644 --- a/src/webdrop_bridge/__init__.py +++ b/src/webdrop_bridge/__init__.py @@ -1,6 +1,6 @@ """WebDrop Bridge - Qt-based desktop application for intelligent drag-and-drop file handling.""" -__version__ = "0.8.6" +__version__ = "0.8.5" __author__ = "WebDrop Team" __license__ = "MIT" diff --git a/tests/unit/test_updater.py b/tests/unit/test_updater.py index db57ebc..f3f09a4 100644 --- a/tests/unit/test_updater.py +++ b/tests/unit/test_updater.py @@ -315,40 +315,6 @@ class TestDownloading: assert result.name == "AgravityBridge-0.0.2-win-x64.msi" mock_download.assert_called_once() - @pytest.mark.asyncio - async def test_download_update_falls_back_to_brand_prefix_without_manifest( - self, agravity_update_manager, tmp_path - ): - """Test branded download selection still works when the manifest is unavailable.""" - release = Release( - tag_name="v0.0.2", - name="WebDropBridge v0.0.2", - version="0.0.2", - body="Release notes", - assets=[ - { - "name": "WebDropBridge-0.0.2-win-x64.msi", - "browser_download_url": "https://example.com/WebDropBridge-0.0.2-win-x64.msi", - }, - { - "name": "AgravityBridge-0.0.2-win-x64.msi", - "browser_download_url": "https://example.com/AgravityBridge-0.0.2-win-x64.msi", - }, - { - "name": "AgravityBridge-0.0.2-win-x64.msi.sha256", - "browser_download_url": "https://example.com/AgravityBridge-0.0.2-win-x64.msi.sha256", - }, - ], - published_at="2026-01-29T10:00:00Z", - ) - - with patch.object(UpdateManager, "_download_file", return_value=True) as mock_download: - result = await agravity_update_manager.download_update(release, tmp_path) - - assert result is not None - assert result.name == "AgravityBridge-0.0.2-win-x64.msi" - mock_download.assert_called_once() - @pytest.mark.asyncio async def test_verify_checksum_uses_release_manifest(self, agravity_update_manager, tmp_path): """Test branded checksum selection from a shared release manifest."""