|
13 | 13 | import sys |
14 | 14 | import tempfile |
15 | 15 | import time |
| 16 | +import shutil |
16 | 17 | from datetime import datetime, timezone |
17 | 18 | from logging import debug as logging_debug |
18 | 19 | from logging import error as logging_error |
|
28 | 29 | from requests import get as requests_get |
29 | 30 | from requests.exceptions import RequestException |
30 | 31 |
|
31 | | -from ardupilot_methodic_configurator import _ |
| 32 | +from platformdirs import user_config_dir |
| 33 | +from pathlib import Path |
| 34 | + |
| 35 | +from ardupilot_methodic_configurator import _, __version__ |
32 | 36 |
|
33 | 37 | # Constants |
34 | 38 | GITHUB_API_URL_RELEASES = "https://api.github.com/repos/ArduPilot/MethodicConfigurator/releases/" |
@@ -145,6 +149,47 @@ def get_release_info(name: str, should_be_pre_release: bool, timeout: int = 30) |
145 | 149 | logging_error(_("Invalid release data: {}").format(e)) |
146 | 150 | raise |
147 | 151 |
|
| 152 | +def create_backup(progress_callback: Optional[Callable[[float, str], None]] = None) -> bool: |
| 153 | + """ |
| 154 | + Backup AMC installation and Vehicles folder. |
| 155 | + Returns True on success, False on any error. |
| 156 | + """ |
| 157 | + try: |
| 158 | + version = __version__ |
| 159 | + config_dir = Path(user_config_dir(".ardupilot_methodic_configurator", appauthor=False, roaming=True)) |
| 160 | + backups_dir = config_dir / "backups" / version |
| 161 | + backups_dir.mkdir(parents=True, exist_ok=True) |
| 162 | + |
| 163 | + # Backup Vehicles folder |
| 164 | + vehicles_dir = config_dir / "vehicles" |
| 165 | + if vehicles_dir.exists(): |
| 166 | + shutil.copytree(vehicles_dir, backups_dir / "vehicles", dirs_exist_ok=True) |
| 167 | + logging_info(_("Vehicles folder backed up to %s"), backups_dir / "Vehicles") |
| 168 | + else: |
| 169 | + logging_info(_("No Vehicles folder found to backup.")) |
| 170 | + |
| 171 | + # Backup AMC wheel |
| 172 | + try: |
| 173 | + subprocess.run( |
| 174 | + [sys.executable, "-m", "pip", "download", f"ardupilot_methodic_configurator=={version}", "-d", str(backups_dir)], |
| 175 | + check=True, |
| 176 | + ) |
| 177 | + logging_info(_("AMC wheel backup complete at %s"), backups_dir) |
| 178 | + except subprocess.CalledProcessError as e: |
| 179 | + logging_error(_("Failed to backup AMC wheel: %s"), e) |
| 180 | + return False |
| 181 | + |
| 182 | + if progress_callback: |
| 183 | + progress_callback(100.0, _("Backup complete")) |
| 184 | + |
| 185 | + return True |
| 186 | + |
| 187 | + except OSError as e: |
| 188 | + logging_error(_("File system error during backup: %s"), e) |
| 189 | + return False |
| 190 | + except Exception as e: |
| 191 | + logging_error(_("Unexpected error during backup: %s"), e) |
| 192 | + return False |
148 | 193 |
|
149 | 194 | def download_and_install_on_windows( |
150 | 195 | download_url: str, |
@@ -172,6 +217,13 @@ def download_and_install_on_windows( |
172 | 217 |
|
173 | 218 | """ |
174 | 219 | logging_info(_("Downloading and installing new version for Windows...")) |
| 220 | + |
| 221 | + try: |
| 222 | + create_backup(progress_callback) |
| 223 | + |
| 224 | + except: |
| 225 | + logging_error(_("Backup failed")) |
| 226 | + |
175 | 227 | try: |
176 | 228 | with tempfile.TemporaryDirectory() as temp_dir: |
177 | 229 | temp_path = os.path.join(temp_dir, file_name) |
@@ -233,22 +285,42 @@ def download_and_install_on_windows( |
233 | 285 | return False |
234 | 286 |
|
235 | 287 |
|
236 | | -def download_and_install_pip_release(progress_callback: Optional[Callable[[float, str], None]] = None) -> int: |
| 288 | +def download_and_install_pip_release( |
| 289 | + progress_callback: Optional[Callable[[float, str], None]] = None |
| 290 | +) -> int: |
237 | 291 | logging_info(_("Updating via pip for Linux and macOS...")) |
238 | 292 |
|
239 | 293 | if progress_callback: |
240 | | - progress_callback(0.0, _("Starting installation...")) |
| 294 | + progress_callback(0.0, _("Backing up current version...")) |
| 295 | + |
| 296 | + backup_ok = create_backup(progress_callback) |
| 297 | + |
| 298 | + if not backup_ok: |
| 299 | + logging_error(_("Backup failed. Aborting update.")) |
| 300 | + if progress_callback: |
| 301 | + progress_callback(0.0, _("Backup failed. Update aborted.")) |
| 302 | + return False |
241 | 303 |
|
| 304 | + if progress_callback: |
| 305 | + progress_callback(100.0, _("Backup complete")) |
| 306 | + progress_callback(0.0, _("Starting installation...")) |
| 307 | + |
242 | 308 | ret = subprocess.check_call( # noqa: S603 |
243 | | - [sys.executable, "-m", "pip", "install", "--upgrade", "ardupilot_methodic_configurator"] |
| 309 | + [ |
| 310 | + sys.executable, |
| 311 | + "-m", |
| 312 | + "pip", |
| 313 | + "install", |
| 314 | + "--upgrade", |
| 315 | + "ardupilot_methodic_configurator", |
| 316 | + ] |
244 | 317 | ) |
245 | 318 |
|
246 | 319 | if ret == 0 and progress_callback: |
247 | 320 | progress_callback(100.0, _("Download complete")) |
248 | 321 |
|
249 | 322 | return ret |
250 | 323 |
|
251 | | - |
252 | 324 | def verify_and_open_url(url: str) -> bool: |
253 | 325 | """ |
254 | 326 | Verify if a URL is accessible and open it in the default web browser if successful. |
|
0 commit comments