mirror of
https://git.srhtnin.garden/sarah/SaraSuite.git
synced 2026-10-14 18:28:11 +02:00
Self-updates and program list from the repo
This commit is contained in:
1 parent
d9da8438c2
commit
e4272bd606
9 files changed
+416
-15
No files matched your search
@@ -0,0 +1,93 @@
|
||||
"""SaraSuite updating itself from its Gitea releases.
|
||||
|
||||
Packaging/Release.py publishes a release (tag v<AppVersion>) with the
|
||||
AppImage attached. Here SaraSuite asks Gitea for the latest release, and if
|
||||
it's newer than AppVersion and SaraSuite runs as an AppImage, downloads it
|
||||
next to the running one and renames it into place (allowed while it's
|
||||
running), then restarts into it. The AppImage keeps its path, so its app
|
||||
menu entry keeps working.
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Callable, Optional
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from Core import AppName, AppVersion, ChildEnvironment, LoadConfig
|
||||
from Sources import GetBytes, GetJson, SourceError
|
||||
|
||||
AssetName = f"{AppName}-x86_64.AppImage"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Release:
|
||||
Version: str
|
||||
AssetUrl: str # empty if the release has no AppImage attached
|
||||
|
||||
|
||||
def RunningAppImage() -> Optional[Path]:
|
||||
Value = os.environ.get("APPIMAGE", "")
|
||||
return Path(Value) if Value else None
|
||||
|
||||
|
||||
def _VersionTuple(Version: str) -> tuple[int, ...]:
|
||||
Parts = []
|
||||
for Part in Version.strip().lstrip("vV").split("."):
|
||||
Digits = "".join(Char for Char in Part if Char.isdigit())
|
||||
Parts.append(int(Digits) if Digits else 0)
|
||||
return tuple(Parts)
|
||||
|
||||
|
||||
def IsNewer(Version: str) -> bool:
|
||||
return _VersionTuple(Version) > _VersionTuple(AppVersion)
|
||||
|
||||
|
||||
def LatestRelease() -> Optional[Release]:
|
||||
"""The latest published release, or None if there's none or the git host
|
||||
can't be reached. GitHost looks like https://host/owner; the API lives at
|
||||
https://host/api/v1."""
|
||||
Parts = urlsplit(LoadConfig().GitHost.rstrip("/"))
|
||||
Owner = Parts.path.strip("/").split("/")[-1]
|
||||
Url = f"{Parts.scheme}://{Parts.netloc}/api/v1/repos/{Owner}/{AppName}/releases/latest"
|
||||
try:
|
||||
Data = GetJson(Url)
|
||||
except SourceError:
|
||||
return None
|
||||
Asset = next((Item for Item in Data.get("assets", []) if Item.get("name") == AssetName), None)
|
||||
return Release(Data.get("tag_name", ""), Asset.get("browser_download_url", "") if Asset else "")
|
||||
|
||||
|
||||
def Install(Latest: Release, Log: Callable[[str], None]) -> Path:
|
||||
"""Replace the running AppImage with Latest's. Raises OSError/SourceError."""
|
||||
Target = RunningAppImage()
|
||||
if Target is None:
|
||||
raise OSError("SaraSuite isn't running as an AppImage, so it can't replace itself.")
|
||||
if not Latest.AssetUrl:
|
||||
raise OSError(f"Release {Latest.Version} has no {AssetName} attached.")
|
||||
Log(f"== Updating {AppName} {AppVersion} -> {Latest.Version.lstrip('v')}")
|
||||
Log(f"Downloading {Latest.AssetUrl}")
|
||||
Data = GetBytes(Latest.AssetUrl)
|
||||
if not Data.startswith(b"\x7fELF"):
|
||||
raise OSError("The download isn't an AppImage; leaving the current one alone.")
|
||||
# Same folder, so the rename is atomic; renaming over a running AppImage
|
||||
# is fine (overwriting it in place isn't: "Text file busy").
|
||||
Temporary = Target.with_name(Target.name + ".new")
|
||||
Temporary.write_bytes(Data)
|
||||
Temporary.chmod(0o755)
|
||||
Temporary.replace(Target)
|
||||
Log(f"== {AppName} updated; it's the new version from the next start")
|
||||
return Target
|
||||
|
||||
|
||||
def Restart() -> None:
|
||||
"""Start the (new) AppImage; the caller then closes this one."""
|
||||
Target = RunningAppImage()
|
||||
if Target is None:
|
||||
return
|
||||
Environment = ChildEnvironment()
|
||||
for Key in ("APPIMAGE", "APPDIR", "ARGV0", "OWD"): # the new AppImage sets its own
|
||||
Environment.pop(Key, None)
|
||||
subprocess.Popen([str(Target)], env=Environment, start_new_session=True,
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
Reference in new issue
Block a user