feat: use github releases for installing extensions
This commit is contained in:
parent
38a132604b
commit
496346b3ba
6 changed files with 139 additions and 51 deletions
|
|
@ -73,7 +73,7 @@ async def get_user(user_id: str, conn: Optional[Connection] = None) -> Optional[
|
||||||
async def add_installed_extension(
|
async def add_installed_extension(
|
||||||
*,
|
*,
|
||||||
ext_id: str,
|
ext_id: str,
|
||||||
version,
|
version: str,
|
||||||
active: bool,
|
active: bool,
|
||||||
hash: str,
|
hash: str,
|
||||||
meta: dict,
|
meta: dict,
|
||||||
|
|
|
||||||
|
|
@ -276,7 +276,7 @@ async def m009_create_installed_extensions_table(db):
|
||||||
"""
|
"""
|
||||||
CREATE TABLE IF NOT EXISTS installed_extensions (
|
CREATE TABLE IF NOT EXISTS installed_extensions (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
version INT NOT NULL,
|
version TEXT NOT NULL,
|
||||||
active BOOLEAN DEFAULT false,
|
active BOOLEAN DEFAULT false,
|
||||||
hash TEXT NOT NULL,
|
hash TEXT NOT NULL,
|
||||||
meta TEXT NOT NULL DEFAULT '{}'
|
meta TEXT NOT NULL DEFAULT '{}'
|
||||||
|
|
|
||||||
|
|
@ -40,8 +40,14 @@
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-3">
|
<div class="col-3">
|
||||||
<!-- hack must find better solution -->
|
<q-img
|
||||||
|
v-if="extension.iconUrl"
|
||||||
|
:src="extension.iconUrl"
|
||||||
|
spinner-color="white"
|
||||||
|
style="max-width: 100%"
|
||||||
|
></q-img>
|
||||||
<q-icon
|
<q-icon
|
||||||
|
v-else
|
||||||
:name="extension.icon"
|
:name="extension.icon"
|
||||||
color="grey-5"
|
color="grey-5"
|
||||||
style="font-size: 4rem"
|
style="font-size: 4rem"
|
||||||
|
|
@ -107,34 +113,20 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<q-rating
|
<div class="float-right">
|
||||||
max="5"
|
<small v-text="extension.stars"> </small>
|
||||||
v-model="maxStars"
|
<q-rating
|
||||||
size="1.5em"
|
max="1"
|
||||||
color="yellow"
|
v-model="maxStars"
|
||||||
icon="star_border"
|
size="1.5em"
|
||||||
icon-selected="star"
|
color="yellow"
|
||||||
icon-half="star_half"
|
icon="star"
|
||||||
readonly
|
icon-selected="star"
|
||||||
no-dimming
|
readonly
|
||||||
class="float-right"
|
no-dimming
|
||||||
>
|
>
|
||||||
<template v-slot:tip-1>
|
</q-rating>
|
||||||
<q-tooltip>User Review Comming Soon</q-tooltip>
|
</div>
|
||||||
</template>
|
|
||||||
<template v-slot:tip-2>
|
|
||||||
<q-tooltip>User Review Comming Soon</q-tooltip>
|
|
||||||
</template>
|
|
||||||
<template v-slot:tip-3>
|
|
||||||
<q-tooltip>User Review Comming Soon</q-tooltip>
|
|
||||||
</template>
|
|
||||||
<template v-slot:tip-4>
|
|
||||||
<q-tooltip>User Review Comming Soon</q-tooltip>
|
|
||||||
</template>
|
|
||||||
<template v-slot:tip-5>
|
|
||||||
<q-tooltip>User Review Comming Soon</q-tooltip>
|
|
||||||
</template>
|
|
||||||
</q-rating>
|
|
||||||
</div>
|
</div>
|
||||||
</q-card-actions>
|
</q-card-actions>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
@ -279,6 +271,7 @@
|
||||||
inProgress: false
|
inProgress: false
|
||||||
}))
|
}))
|
||||||
this.filteredExtensions = this.extensions.concat([])
|
this.filteredExtensions = this.extensions.concat([])
|
||||||
|
console.log('### his.filteredExtensions', this.filteredExtensions)
|
||||||
},
|
},
|
||||||
mixins: [windowMixin]
|
mixins: [windowMixin]
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -740,7 +740,6 @@ async def api_install_extension(
|
||||||
db_version = (await get_dbversions()).get(ext_id, 0)
|
db_version = (await get_dbversions()).get(ext_id, 0)
|
||||||
await migrate_extension_database(extension, db_version)
|
await migrate_extension_database(extension, db_version)
|
||||||
|
|
||||||
# disable by default
|
|
||||||
await add_installed_extension(
|
await add_installed_extension(
|
||||||
ext_id=ext_id,
|
ext_id=ext_id,
|
||||||
version=ext_info.version,
|
version=ext_info.version,
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,9 @@ async def extensions_install(
|
||||||
"name": ext.name,
|
"name": ext.name,
|
||||||
"hash": ext.hash,
|
"hash": ext.hash,
|
||||||
"icon": ext.icon,
|
"icon": ext.icon,
|
||||||
|
"iconUrl": ext.icon_url,
|
||||||
"shortDescription": ext.short_description,
|
"shortDescription": ext.short_description,
|
||||||
|
"stars": ext.stars,
|
||||||
"details": ext.details,
|
"details": ext.details,
|
||||||
"dependencies": ext.dependencies,
|
"dependencies": ext.dependencies,
|
||||||
"isInstalled": ext.id in installed_extensions,
|
"isInstalled": ext.id in installed_extensions,
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import sys
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import zipfile
|
import zipfile
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
from platform import release
|
||||||
from typing import List, NamedTuple, Optional
|
from typing import List, NamedTuple, Optional
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
@ -102,6 +103,27 @@ class ExtensionManager:
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
class ExtensionRelease(BaseModel):
|
||||||
|
name: str
|
||||||
|
version: str
|
||||||
|
archive: str
|
||||||
|
description: str
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_github_releases(cls, releases: dict) -> List["ExtensionRelease"]:
|
||||||
|
return list(
|
||||||
|
map(
|
||||||
|
lambda r: ExtensionRelease(
|
||||||
|
name=r["name"],
|
||||||
|
version=r["tag_name"],
|
||||||
|
archive=r["zipball_url"],
|
||||||
|
description=r["body"],
|
||||||
|
),
|
||||||
|
releases,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class InstallableExtension(BaseModel):
|
class InstallableExtension(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
name: str
|
name: str
|
||||||
|
|
@ -110,9 +132,12 @@ class InstallableExtension(BaseModel):
|
||||||
short_description: Optional[str] = None
|
short_description: Optional[str] = None
|
||||||
details: Optional[str] = None
|
details: Optional[str] = None
|
||||||
icon: Optional[str] = None
|
icon: Optional[str] = None
|
||||||
|
icon_url: Optional[str] = None
|
||||||
dependencies: List[str] = []
|
dependencies: List[str] = []
|
||||||
is_admin_only: bool = False
|
is_admin_only: bool = False
|
||||||
version: Optional[int] = 0
|
version: str = "none" # todo: move to Release
|
||||||
|
stars: int = 0
|
||||||
|
releases: Optional[List[ExtensionRelease]]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def zip_path(self) -> str:
|
def zip_path(self) -> str:
|
||||||
|
|
@ -194,6 +219,25 @@ class InstallableExtension(BaseModel):
|
||||||
|
|
||||||
shutil.rmtree(self.ext_upgrade_dir, True)
|
shutil.rmtree(self.ext_upgrade_dir, True)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def from_repo(cls, org, repository) -> Optional["InstallableExtension"]:
|
||||||
|
try:
|
||||||
|
repo, releases, config = await fetch_github_repo_info(org, repository)
|
||||||
|
|
||||||
|
return InstallableExtension(
|
||||||
|
id=repo["name"],
|
||||||
|
name=config.get("name"),
|
||||||
|
short_description=config.get("short_description"),
|
||||||
|
archive="xx",
|
||||||
|
hash="123",
|
||||||
|
stars=repo["stargazers_count"],
|
||||||
|
icon_url=icon_to_github_url(org, config.get("tile")),
|
||||||
|
releases=ExtensionRelease.from_github_releases(releases),
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(e)
|
||||||
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def get_extension_info(cls, ext_id: str, hash: str) -> "InstallableExtension":
|
async def get_extension_info(cls, ext_id: str, hash: str) -> "InstallableExtension":
|
||||||
installable_extensions: List[
|
installable_extensions: List[
|
||||||
|
|
@ -229,27 +273,35 @@ class InstallableExtension(BaseModel):
|
||||||
try:
|
try:
|
||||||
resp = await client.get(url)
|
resp = await client.get(url)
|
||||||
if resp.status_code != 200:
|
if resp.status_code != 200:
|
||||||
logger.warning(
|
logger.warning(f"Cannot fetch extensions manifest at: {url}")
|
||||||
f"Unable to fetch extension list for repository: {url}"
|
|
||||||
)
|
|
||||||
continue
|
continue
|
||||||
for e in resp.json()["extensions"]:
|
manifest = resp.json()
|
||||||
extension_list += [
|
if "extensions" in manifest:
|
||||||
InstallableExtension(
|
for e in manifest["extensions"] or []:
|
||||||
id=e["id"],
|
extension_list += [
|
||||||
name=e["name"],
|
InstallableExtension(
|
||||||
archive=e["archive"],
|
id=e["id"],
|
||||||
hash=e["hash"],
|
name=e["name"],
|
||||||
short_description=e["shortDescription"],
|
archive=e["archive"],
|
||||||
details=e["details"] if "details" in e else "",
|
hash=e["hash"],
|
||||||
icon=e["icon"],
|
short_description=e["shortDescription"],
|
||||||
dependencies=e["dependencies"]
|
details=e["details"] if "details" in e else "",
|
||||||
if "dependencies" in e
|
icon=e["icon"],
|
||||||
else [],
|
dependencies=e["dependencies"]
|
||||||
|
if "dependencies" in e
|
||||||
|
else [],
|
||||||
|
)
|
||||||
|
]
|
||||||
|
if "repos" in manifest:
|
||||||
|
for r in manifest["repos"]:
|
||||||
|
ext = await InstallableExtension.from_repo(
|
||||||
|
r["organisation"], r["repository"]
|
||||||
)
|
)
|
||||||
]
|
print("#### repo_extensions", ext)
|
||||||
|
if ext:
|
||||||
|
extension_list += [ext]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(e)
|
logger.warning(f"Manifest {url} failed with '{str(e)}'")
|
||||||
|
|
||||||
return extension_list
|
return extension_list
|
||||||
|
|
||||||
|
|
@ -317,3 +369,45 @@ def file_hash(filename):
|
||||||
while n := f.readinto(mv):
|
while n := f.readinto(mv):
|
||||||
h.update(mv[:n])
|
h.update(mv[:n])
|
||||||
return h.hexdigest()
|
return h.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def icon_to_github_url(org: str, path: Optional[str]) -> str:
|
||||||
|
if not path:
|
||||||
|
return ""
|
||||||
|
_, repo, *rest = path.split("/")
|
||||||
|
tail = "/".join(rest)
|
||||||
|
return f"https://github.com/{org}/{repo}/raw/main/{tail}"
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_github_repo_info(org: str, repository: str):
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
repo_url = f"https://api.github.com/repos/{org}/{repository}"
|
||||||
|
resp = await client.get(repo_url)
|
||||||
|
if resp.status_code != 200:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND,
|
||||||
|
detail=f"Cannot fetch extension repo: {repo_url}",
|
||||||
|
)
|
||||||
|
repo = resp.json()
|
||||||
|
|
||||||
|
releases_url = f"https://api.github.com/repos/{org}/{repository}/releases"
|
||||||
|
resp = await client.get(releases_url)
|
||||||
|
if resp.status_code != 200:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND,
|
||||||
|
detail=f"Cannot fetch extension releases: {releases_url}",
|
||||||
|
)
|
||||||
|
|
||||||
|
releases = resp.json()
|
||||||
|
|
||||||
|
config_url = f"""https://raw.githubusercontent.com/{org}/{repository}/{repo["default_branch"]}/config.json"""
|
||||||
|
resp = await client.get(config_url)
|
||||||
|
if resp.status_code != 200:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND,
|
||||||
|
detail=f"Cannot fetch config for extension: {config_url}",
|
||||||
|
)
|
||||||
|
|
||||||
|
config = resp.json()
|
||||||
|
|
||||||
|
return repo, releases, config
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue