feat: install GitHib releases also

This commit is contained in:
Vlad Stan 2023-01-17 11:16:54 +02:00
parent 41ce316fc6
commit 9d0cedfcb2
6 changed files with 79 additions and 41 deletions

View File

@ -75,25 +75,22 @@ async def add_installed_extension(
ext_id: str, ext_id: str,
version: str, version: str,
active: bool, active: bool,
hash: str,
meta: dict, meta: dict,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> None: ) -> None:
await (conn or db).execute( await (conn or db).execute(
""" """
INSERT INTO installed_extensions (id, version, active, hash, meta) VALUES (?, ?, ?, ?, ?) INSERT INTO installed_extensions (id, version, active, meta) VALUES (?, ?, ?, ?)
ON CONFLICT (id) DO ON CONFLICT (id) DO
UPDATE SET (version, active, hash, meta) = (?, ?, ?, ?) UPDATE SET (version, active, meta) = (?, ?, ?)
""", """,
( (
ext_id, ext_id,
version, version,
active, active,
hash,
json.dumps(meta), json.dumps(meta),
version, version,
active, active,
hash,
json.dumps(meta), json.dumps(meta),
), ),
) )

View File

@ -278,7 +278,6 @@ async def m009_create_installed_extensions_table(db):
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
version TEXT NOT NULL, version TEXT NOT NULL,
active BOOLEAN DEFAULT false, active BOOLEAN DEFAULT false,
hash TEXT NOT NULL,
meta TEXT NOT NULL DEFAULT '{}' meta TEXT NOT NULL DEFAULT '{}'
); );
""" """

View File

@ -176,14 +176,16 @@
> >
<q-card> <q-card>
<q-card-section> <q-card-section>
<q-btn type="a" color="primary unelevated mt-lg pt-lg" <q-btn
@click="installExtension(release)"
color="primary unelevated mt-lg pt-lg"
>Install</q-btn >Install</q-btn
> >
</q-card-section> </q-card-section>
<q-separator></q-separator> <q-separator></q-separator>
<div <div
v-if="selectedExtension.details" v-if="release.details_html"
v-html="selectedExtension.details" v-html="release.details_html"
></div> </q-card ></div> </q-card
></q-expansion-item> ></q-expansion-item>
</q-list> </q-list>
@ -258,13 +260,19 @@
) )
.filter(extensionNameContains(term)) .filter(extensionNameContains(term))
}, },
installExtension: async function (extension) { installExtension: async function (release) {
const extension = this.selectedExtension
try { try {
extension.inProgress = true extension.inProgress = true
await LNbits.api.request( await LNbits.api.request(
'POST', 'POST',
`/api/v1/extension/${extension.id}/${extension.hash}?usr=${this.g.user.id}`, `/api/v1/extension?usr=${this.g.user.id}`,
this.g.user.wallets[0].adminkey this.g.user.wallets[0].adminkey,
{
ext_id: extension.id,
archive: release.archive,
source_repo: release.source_repo
}
) )
window.location.href = [ window.location.href = [
"{{ url_for('install.extensions') }}", "{{ url_for('install.extensions') }}",

View File

@ -41,6 +41,7 @@ from lnbits.decorators import (
require_invoice_key, require_invoice_key,
) )
from lnbits.extension_manger import ( from lnbits.extension_manger import (
CreateExtension,
Extension, Extension,
ExtensionRelease, ExtensionRelease,
InstallableExtension, InstallableExtension,
@ -720,13 +721,30 @@ async def websocket_update_get(item_id: str, data: str):
return {"sent": False, "data": data} return {"sent": False, "data": data}
@core_app.post("/api/v1/extension/{ext_id}/{hash}") @core_app.post("/api/v1/extension")
async def api_install_extension( async def api_install_extension(
ext_id: str, hash: str, user: User = Depends(check_admin) data: CreateExtension, user: User = Depends(check_admin)
): ):
ext_info: InstallableExtension = await InstallableExtension.get_extension_info( # ext_info: InstallableExtension = await InstallableExtension.get_extension_info(
ext_id, hash # data.ext_id, data.archive
# )
all_releases: List[
ExtensionRelease
] = await InstallableExtension.get_extension_releases(data.ext_id)
selected_release = [
r
for r in all_releases
if r.archive == data.archive and r.source_repo == data.source_repo
]
if len(selected_release) == 0:
raise Exception("uuuuuuu")
installed_release = selected_release[0]
ext_info = InstallableExtension(
id=data.ext_id, name=data.ext_id, installed_release=installed_release
) )
ext_info.download_archive() ext_info.download_archive()
try: try:
@ -734,17 +752,16 @@ async def api_install_extension(
extension = Extension.from_installable_ext(ext_info) extension = Extension.from_installable_ext(ext_info)
db_version = (await get_dbversions()).get(ext_id, 0) db_version = (await get_dbversions()).get(data.ext_id, 0)
await migrate_extension_database(extension, db_version) await migrate_extension_database(extension, db_version)
await add_installed_extension( await add_installed_extension(
ext_id=ext_id, ext_id=data.ext_id,
version=ext_info.version, version=installed_release.version,
active=False, active=False,
hash=hash, meta={"installed_release": dict(installed_release)},
meta=dict(ext_info),
) )
settings.lnbits_disabled_extensions += [ext_id] settings.lnbits_disabled_extensions += [data.ext_id]
# mount routes for the new version # mount routes for the new version
core_app_extra.register_new_ext_routes(extension) core_app_extra.register_new_ext_routes(extension)

View File

@ -103,13 +103,10 @@ async def extensions_install(
lambda ext: { lambda ext: {
"id": ext.id, "id": ext.id,
"name": ext.name, "name": ext.name,
"hash": ext.hash,
"version": ext.version,
"icon": ext.icon, "icon": ext.icon,
"iconUrl": ext.icon_url, "iconUrl": ext.icon_url,
"shortDescription": ext.short_description, "shortDescription": ext.short_description,
"stars": ext.stars, "stars": ext.stars,
"details": ext.details,
"dependencies": ext.dependencies, "dependencies": ext.dependencies,
"isInstalled": ext.id in installed_extensions, "isInstalled": ext.id in installed_extensions,
"isActive": not ext.id in inactive_extensions, "isActive": not ext.id in inactive_extensions,

View File

@ -111,6 +111,7 @@ class ExtensionRelease(BaseModel):
published_at: Optional[str] published_at: Optional[str]
html_url: Optional[str] html_url: Optional[str]
description: Optional[str] description: Optional[str]
details_html: Optional[str] = None
@classmethod @classmethod
def from_github_release(cls, source_repo: str, r: dict) -> "ExtensionRelease": def from_github_release(cls, source_repo: str, r: dict) -> "ExtensionRelease":
@ -146,19 +147,25 @@ class ExtensionRelease(BaseModel):
class InstallableExtension(BaseModel): class InstallableExtension(BaseModel):
id: str id: str
name: str name: str
archive: str # todo: move to installed_release
hash: str
short_description: Optional[str] = None short_description: Optional[str] = None
details: Optional[str] = None
icon: Optional[str] = None icon: Optional[str] = None
icon_url: 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: str = "none" # todo: move to Release
stars: int = 0 stars: int = 0
latest_release: Optional[ExtensionRelease] latest_release: Optional[ExtensionRelease]
installed_release: Optional[ExtensionRelease] installed_release: Optional[ExtensionRelease]
@property
def hash(self) -> str:
if self.installed_release:
if self.installed_release.hash:
return self.installed_release.hash
m = hashlib.sha256()
m.update(f"{self.installed_release.archive}".encode())
return m.hexdigest()
return "not-installed"
@property @property
def zip_path(self) -> str: def zip_path(self) -> str:
extensions_data_dir = os.path.join(settings.lnbits_data_folder, "extensions") extensions_data_dir = os.path.join(settings.lnbits_data_folder, "extensions")
@ -186,7 +193,7 @@ class InstallableExtension(BaseModel):
if os.path.isfile(ext_zip_file): if os.path.isfile(ext_zip_file):
os.remove(ext_zip_file) os.remove(ext_zip_file)
try: try:
download_url(self.archive, ext_zip_file) download_url(self.installed_release.archive, ext_zip_file)
except Exception as ex: except Exception as ex:
logger.warning(ex) logger.warning(ex)
raise HTTPException( raise HTTPException(
@ -195,7 +202,7 @@ class InstallableExtension(BaseModel):
) )
archive_hash = file_hash(ext_zip_file) archive_hash = file_hash(ext_zip_file)
if self.hash != archive_hash: if self.installed_release.hash and self.installed_release.hash != archive_hash:
# remove downloaded archive # remove downloaded archive
if os.path.isfile(ext_zip_file): if os.path.isfile(ext_zip_file):
os.remove(ext_zip_file) os.remove(ext_zip_file)
@ -205,14 +212,21 @@ class InstallableExtension(BaseModel):
) )
def extract_archive(self): def extract_archive(self):
shutil.rmtree(self.ext_dir, True)
with zipfile.ZipFile(self.zip_path, "r") as zip_ref:
zip_ref.extractall(os.path.join("lnbits", "extensions"))
os.makedirs(os.path.join("lnbits", "upgrades"), exist_ok=True) os.makedirs(os.path.join("lnbits", "upgrades"), exist_ok=True)
shutil.rmtree(self.ext_upgrade_dir, True) shutil.rmtree(self.ext_upgrade_dir, True)
with zipfile.ZipFile(self.zip_path, "r") as zip_ref: with zipfile.ZipFile(self.zip_path, "r") as zip_ref:
zip_ref.extractall(self.ext_upgrade_dir) zip_ref.extractall(self.ext_upgrade_dir)
generated_dir_name = os.listdir(self.ext_upgrade_dir)[0]
os.rename(
os.path.join(self.ext_upgrade_dir, generated_dir_name),
os.path.join(self.ext_upgrade_dir, self.id),
)
shutil.rmtree(self.ext_dir, True)
shutil.copytree(
os.path.join(self.ext_upgrade_dir, self.id),
os.path.join("lnbits", "extensions", self.id),
)
def nofiy_upgrade(self) -> None: def nofiy_upgrade(self) -> None:
"""Update the the list of upgraded extensions. The middleware will perform redirects based on this""" """Update the the list of upgraded extensions. The middleware will perform redirects based on this"""
@ -261,15 +275,15 @@ class InstallableExtension(BaseModel):
logger.warning(e) logger.warning(e)
return None return None
@classmethod @classmethod # todo: remove
async def get_extension_info(cls, ext_id: str, hash: str) -> "InstallableExtension": async def get_extension_info(
cls, ext_id: str, archive: str
) -> "InstallableExtension":
installable_extensions: List[ installable_extensions: List[
InstallableExtension InstallableExtension
] = await InstallableExtension.get_installable_extensions() ] = await InstallableExtension.get_installable_extensions()
valid_extensions = [ valid_extensions = [e for e in installable_extensions if e.id == ext_id]
e for e in installable_extensions if e.id == ext_id and e.hash == hash
]
if len(valid_extensions) == 0: if len(valid_extensions) == 0:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, status_code=HTTPStatus.BAD_REQUEST,
@ -322,7 +336,6 @@ class InstallableExtension(BaseModel):
archive=e["archive"], archive=e["archive"],
hash=e["hash"], hash=e["hash"],
short_description=e["shortDescription"], short_description=e["shortDescription"],
details=e["details"] if "details" in e else "",
icon=e["icon"], icon=e["icon"],
dependencies=e["dependencies"] dependencies=e["dependencies"]
if "dependencies" in e if "dependencies" in e
@ -365,6 +378,7 @@ class InstallableExtension(BaseModel):
hash=e["hash"], hash=e["hash"],
source_repo=url, source_repo=url,
description=e["shortDescription"], description=e["shortDescription"],
details_html=e.get("details"),
) )
] ]
@ -415,6 +429,12 @@ class InstalledExtensionMiddleware:
await self.app(scope, receive, send) await self.app(scope, receive, send)
class CreateExtension(BaseModel):
ext_id: str
archive: str
source_repo: str
def get_valid_extensions(include_disabled_exts=False) -> List[Extension]: def get_valid_extensions(include_disabled_exts=False) -> List[Extension]:
return [ return [
extension extension