diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f26584d2..6327a3582 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,16 @@ All notable changes to Bambuddy will be documented in this file. -## [0.1.9] - Unrelased +## [0.1.8.1] - 2026-02-07 + +### Fixed +- **FTP Upload Broken on All Printer Models** — Fixed critical bug where all FTP uploads failed with "550 Failed to change directory": + - `diagnose_storage()` was running before every upload, and its CWD failures (`ftplib.error_perm`) were not caught because `error_perm` is not a subclass of `error_reply` + - Removed `diagnose_storage()` from the upload hot path + - Changed all FTP exception handlers from `except (OSError, ftplib.error_reply)` to `except (OSError, ftplib.Error)` to catch all FTP error types +- **HTTP 500 on Reprint and Print Endpoints** — Fixed 500 errors on `/api/v1/archives/{id}/reprint` and `/api/v1/library/files/{id}/print` caused by the FTP failure above +- **Exception Handling Reverted** — Reverted overly-narrow exception handling introduced in 0.1.8 that could cause uncaught errors in archive parsing, HTTP clients, 3MF/ZIP processing, Home Assistant, and firmware checks +- **4-Segment Version Support** — Version parser now supports patch releases like `0.1.8.1` for hotfixes without incrementing the minor version ## [0.1.8] - 2026-02-06 diff --git a/backend/app/api/routes/updates.py b/backend/app/api/routes/updates.py index 64560df82..ae38c8502 100644 --- a/backend/app/api/routes/updates.py +++ b/backend/app/api/routes/updates.py @@ -71,31 +71,33 @@ def _find_executable(name: str) -> str | None: def parse_version(version: str) -> tuple: """Parse version string into tuple for comparison. - Returns (major, minor, patch, is_prerelease, prerelease_num) + Returns (major, minor, patch, micro, is_prerelease, prerelease_num) where is_prerelease is 0 for release, 1 for prerelease. This ensures releases sort higher than prereleases of same version. Examples: - "0.1.5" -> (0, 1, 5, 0, 0) # release - "0.1.5b7" -> (0, 1, 5, 1, 7) # beta 7 - "0.1.5b10" -> (0, 1, 5, 1, 10) # beta 10 + "0.1.5" -> (0, 1, 5, 0, 0, 0) # release + "0.1.5b7" -> (0, 1, 5, 0, 1, 7) # beta 7 + "0.1.5b10" -> (0, 1, 5, 0, 1, 10) # beta 10 + "0.1.8.1" -> (0, 1, 8, 1, 0, 0) # patch release """ # Remove 'v' prefix if present version = version.lstrip("v") - # Match version pattern: major.minor.patch[b|beta|alpha|rc]N - match = re.match(r"(\d+)\.(\d+)\.(\d+)(?:b|beta|alpha|rc)?(\d+)?", version) + # Match version pattern: major.minor.patch[.micro][b|beta|alpha|rc]N + match = re.match(r"(\d+)\.(\d+)\.(\d+)(?:\.(\d+))?(?:b|beta|alpha|rc)?(\d+)?", version) if match: major = int(match.group(1)) minor = int(match.group(2)) patch = int(match.group(3)) - prerelease_num = int(match.group(4)) if match.group(4) else 0 + micro = int(match.group(4)) if match.group(4) else 0 + prerelease_num = int(match.group(5)) if match.group(5) else 0 # Check if this is a prerelease (has b/beta/alpha/rc suffix) is_prerelease = 1 if re.search(r"[a-zA-Z]", version.split(".")[-1]) else 0 - return (major, minor, patch, is_prerelease, prerelease_num) + return (major, minor, patch, micro, is_prerelease, prerelease_num) # Fallback: try simple split parts = [] @@ -106,7 +108,7 @@ def parse_version(version: str) -> tuple: num = "".join(c for c in part if c.isdigit()) parts.append(int(num) if num else 0) - return tuple(parts) + (0, 0) + return tuple(parts) + (0, 0, 0) def is_newer_version(latest: str, current: str) -> bool: @@ -121,9 +123,9 @@ def is_newer_version(latest: str, current: str) -> bool: latest_parsed = parse_version(latest) current_parsed = parse_version(current) - # Compare (major, minor, patch) first - latest_base = latest_parsed[:3] - current_base = current_parsed[:3] + # Compare (major, minor, patch, micro) first + latest_base = latest_parsed[:4] + current_base = current_parsed[:4] if latest_base > current_base: return True @@ -133,8 +135,8 @@ def is_newer_version(latest: str, current: str) -> bool: # Same base version - compare prerelease status # is_prerelease: 0 = release, 1 = prerelease # Release (0) should be "greater" than prerelease (1) - latest_is_prerelease = latest_parsed[3] if len(latest_parsed) > 3 else 0 - current_is_prerelease = current_parsed[3] if len(current_parsed) > 3 else 0 + latest_is_prerelease = latest_parsed[4] if len(latest_parsed) > 4 else 0 + current_is_prerelease = current_parsed[4] if len(current_parsed) > 4 else 0 if latest_is_prerelease < current_is_prerelease: # latest is release, current is prerelease -> latest is newer @@ -145,8 +147,8 @@ def is_newer_version(latest: str, current: str) -> bool: # Both are same type (both release or both prerelease) # Compare prerelease numbers - latest_prerelease_num = latest_parsed[4] if len(latest_parsed) > 4 else 0 - current_prerelease_num = current_parsed[4] if len(current_parsed) > 4 else 0 + latest_prerelease_num = latest_parsed[5] if len(latest_parsed) > 5 else 0 + current_prerelease_num = current_parsed[5] if len(current_parsed) > 5 else 0 return latest_prerelease_num > current_prerelease_num diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 58274035e..e1faffd7b 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -5,7 +5,7 @@ from pathlib import Path from pydantic_settings import BaseSettings # Application version - single source of truth -APP_VERSION = "0.1.9b" +APP_VERSION = "0.1.8.1" GITHUB_REPO = "maziggy/bambuddy" # App directory - where the application is installed (for static files)