Описание
Отсутствует механизм обновления приложения. Пользователи должны вручную скачивать и устанавливать новые версии, что создаёт friction и риск использования устаревших версий с известными уязвимостями.
Цели
- Автоматическая проверка обновлений
- Фоновое скачивание обновлений
- Уведомление пользователя о доступных обновлениях
- Безопасное применение обновлений с rollback
Технические требования
1. Модель данных обновлений
from dataclasses import dataclass
from datetime import datetime
@dataclass
class ReleaseInfo:
"""Информация о релизе."""
version: str
release_date: datetime
download_url: str
changelog: str
checksum: str # SHA256
size_bytes: int
is_prerelease: bool = False
is_minimum_required: bool = False # Обязательное обновление
@dataclass
class UpdateStatus:
"""Статус обновления."""
current_version: str
latest_version: str | None
update_available: bool
release_info: ReleaseInfo | None
download_progress: float | None # 0.0 - 1.0
is_downloaded: bool
error: str | None
2. Менеджер обновлений
import hashlib
import subprocess
from urllib.request import urlopen, Request
import json
class UpdateManager:
"""Менеджер обновлений приложения."""
UPDATE_CHECK_URL = "https://api.github.com/repos/{owner}/{repo}/releases"
UPDATE_CHANNEL_STABLE = "stable"
UPDATE_CHANNEL_BETA = "beta"
def __init__(self, owner: str, repo: str, current_version: str):
self._owner = owner
self._repo = repo
self._current_version = current_version
self._channel = self.UPDATE_CHANNEL_STABLE
def check_for_updates(self) -> UpdateStatus:
"""Проверяет наличие обновлений."""
try:
url = self.UPDATE_CHECK_URL.format(owner=self._owner, repo=self._repo)
request = Request(url, headers={"Accept": "application/vnd.github.v3+json"})
with urlopen(request, timeout=10) as response:
releases = json.loads(response.read().decode("utf-8"))
# Найти последний стабильный релиз
latest = self._find_latest_stable(releases)
if latest is None:
return UpdateStatus(
current_version=self._current_version,
latest_version=None,
update_available=False,
release_info=None,
download_progress=None,
is_downloaded=False,
error=None
)
update_available = self._compare_versions(
latest["tag_name"],
self._current_version
) > 0
return UpdateStatus(
current_version=self._current_version,
latest_version=latest["tag_name"],
update_available=update_available,
release_info=self._parse_release(latest),
download_progress=None,
is_downloaded=False,
error=None
)
except Exception as e:
return UpdateStatus(
current_version=self._current_version,
latest_version=None,
update_available=False,
release_info=None,
download_progress=None,
is_downloaded=False,
error=str(e)
)
def download_update(self, release: ReleaseInfo,
progress_callback: Callable[[float], None] = None
) -> Path | None:
"""Скачивает обновление с прогрессом."""
try:
request = Request(release.download_url)
response = urlopen(request, timeout=300)
total_size = int(response.headers.get("Content-Length", 0))
downloaded = 0
chunk_size = 8192
temp_dir = Path(tempfile.gettempdir()) / "mia_update"
temp_dir.mkdir(exist_ok=True)
output_path = temp_dir / f"mia_setup_{release.version}.exe"
with open(output_path, "wb") as f:
while True:
chunk = response.read(chunk_size)
if not chunk:
break
f.write(chunk)
downloaded += len(chunk)
if progress_callback and total_size > 0:
progress_callback(downloaded / total_size)
# Проверить checksum
if not self._verify_checksum(output_path, release.checksum):
output_path.unlink()
return None
return output_path
except Exception as e:
logger.error(f"Download failed: {e}")
return None
def apply_update(self, installer_path: Path) -> bool:
"""Применяет обновление (запускает инсталлятор)."""
try:
# Создать backup текущей версии
self._create_backup()
# Запустить инсталлятор
subprocess.Popen(
[str(installer_path), "/SILENT"],
creationflags=get_subprocess_creationflags()
)
# Завершить текущее приложение
QApplication.quit()
return True
except Exception as e:
logger.error(f"Update apply failed: {e}")
return False
def rollback(self) -> bool:
"""Откатывает обновление."""
backup_dir = self._get_backup_dir()
if not backup_dir.exists():
return False
# Восстановить файлы из backup
# ...
return True
3. API endpoints
# Проверка обновлений
GET /api/v1/system/check-update
Response: {
"current_version": "1.2.3",
"latest_version": "1.3.0",
"update_available": true,
"release": {
"version": "1.3.0",
"changelog": "...",
"download_url": "...",
"size_bytes": 52428800
}
}
# Скачивание обновления
POST /api/v1/system/download-update
Response: {
"downloaded": true,
"path": "C:\\Users\\...\\mia_setup_1.3.0.exe"
}
# Применение обновления
POST /api/v1/system/apply-update
Response: {
"started": true,
"message": "Приложение будет обновлено и перезапущено"
}
4. UI компонент
class UpdateNotificationWidget(QWidget):
"""Виджет уведомления об обновлении."""
update_available = pyqtSignal()
def __init__(self, update_manager: UpdateManager):
super().__init__()
self._manager = update_manager
self._setup_ui()
def check_and_notify(self) -> None:
"""Проверяет обновления и показывает уведомление."""
status = self._manager.check_for_updates()
if status.update_available:
self._show_update_dialog(status)
def _show_update_dialog(self, status: UpdateStatus) -> None:
"""Показывает диалог обновления."""
msg = QMessageBox()
msg.setIcon(QMessageBox.Icon.Information)
msg.setWindowTitle("Доступно обновление")
msg.setText(f"MIA-ScreenCapture {status.latest_version} доступно!")
msg.setDetailedText(status.release_info.changelog)
msg.addButton("Скачать", QMessageBox.ButtonRole.AcceptRole)
msg.addButton("Позже", QMessageBox.ButtonRole.RejectRole)
if msg.exec() == QMessageBox.Accepted:
self._start_download(status.release_info)
Конфигурация
# config.py
class AppSettingsSchema(BaseModel):
# ... existing fields ...
auto_check_updates: bool = Field(default=True)
update_channel: Literal["stable", "beta"] = Field(default="stable")
update_check_interval_hours: int = Field(default=24, ge=1, le=168)
Критерии приёмки
Файлы для изменения
core/update_manager.py — новый модуль
config.py — настройки обновлений
api/routes_system.py — API endpoints
gui/views/settings_view.py — UI настроек
main.py — проверка при старте
Приоритет: P2
Тэги: enhancement, devops
Описание
Отсутствует механизм обновления приложения. Пользователи должны вручную скачивать и устанавливать новые версии, что создаёт friction и риск использования устаревших версий с известными уязвимостями.
Цели
Технические требования
1. Модель данных обновлений
2. Менеджер обновлений
3. API endpoints
4. UI компонент
Конфигурация
Критерии приёмки
Файлы для изменения
core/update_manager.py— новый модульconfig.py— настройки обновленийapi/routes_system.py— API endpointsgui/views/settings_view.py— UI настроекmain.py— проверка при стартеПриоритет: P2
Тэги:
enhancement,devops