From 9d0cedfcb23bee8e8fd8ea6d516f8c8006f98d27 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 17 Jan 2023 11:16:54 +0200 Subject: [PATCH] feat: install GitHib releases also --- lnbits/core/crud.py | 7 +--- lnbits/core/migrations.py | 1 - lnbits/core/templates/core/install.html | 20 +++++++--- lnbits/core/views/api.py | 37 +++++++++++++----- lnbits/core/views/generic.py | 3 -- lnbits/extension_manger.py | 52 +++++++++++++++++-------- 6 files changed, 79 insertions(+), 41 deletions(-) diff --git a/lnbits/core/crud.py b/lnbits/core/crud.py index ae6fbb52c..dcdf18d20 100644 --- a/lnbits/core/crud.py +++ b/lnbits/core/crud.py @@ -75,25 +75,22 @@ async def add_installed_extension( ext_id: str, version: str, active: bool, - hash: str, meta: dict, conn: Optional[Connection] = None, ) -> None: 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 - UPDATE SET (version, active, hash, meta) = (?, ?, ?, ?) + UPDATE SET (version, active, meta) = (?, ?, ?) """, ( ext_id, version, active, - hash, json.dumps(meta), version, active, - hash, json.dumps(meta), ), ) diff --git a/lnbits/core/migrations.py b/lnbits/core/migrations.py index 946d79f5b..61b08912a 100644 --- a/lnbits/core/migrations.py +++ b/lnbits/core/migrations.py @@ -278,7 +278,6 @@ async def m009_create_installed_extensions_table(db): id TEXT PRIMARY KEY, version TEXT NOT NULL, active BOOLEAN DEFAULT false, - hash TEXT NOT NULL, meta TEXT NOT NULL DEFAULT '{}' ); """ diff --git a/lnbits/core/templates/core/install.html b/lnbits/core/templates/core/install.html index 83731d751..b0776c89c 100644 --- a/lnbits/core/templates/core/install.html +++ b/lnbits/core/templates/core/install.html @@ -176,14 +176,16 @@ > - Install
@@ -258,13 +260,19 @@ ) .filter(extensionNameContains(term)) }, - installExtension: async function (extension) { + installExtension: async function (release) { + const extension = this.selectedExtension try { extension.inProgress = true await LNbits.api.request( 'POST', - `/api/v1/extension/${extension.id}/${extension.hash}?usr=${this.g.user.id}`, - this.g.user.wallets[0].adminkey + `/api/v1/extension?usr=${this.g.user.id}`, + this.g.user.wallets[0].adminkey, + { + ext_id: extension.id, + archive: release.archive, + source_repo: release.source_repo + } ) window.location.href = [ "{{ url_for('install.extensions') }}", diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py index 299f31442..42e899a2e 100644 --- a/lnbits/core/views/api.py +++ b/lnbits/core/views/api.py @@ -41,6 +41,7 @@ from lnbits.decorators import ( require_invoice_key, ) from lnbits.extension_manger import ( + CreateExtension, Extension, ExtensionRelease, InstallableExtension, @@ -720,13 +721,30 @@ async def websocket_update_get(item_id: str, data: str): 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( - 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_id, hash + # ext_info: InstallableExtension = await InstallableExtension.get_extension_info( + # 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() try: @@ -734,17 +752,16 @@ async def api_install_extension( 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 add_installed_extension( - ext_id=ext_id, - version=ext_info.version, + ext_id=data.ext_id, + version=installed_release.version, active=False, - hash=hash, - meta=dict(ext_info), + meta={"installed_release": dict(installed_release)}, ) - settings.lnbits_disabled_extensions += [ext_id] + settings.lnbits_disabled_extensions += [data.ext_id] # mount routes for the new version core_app_extra.register_new_ext_routes(extension) diff --git a/lnbits/core/views/generic.py b/lnbits/core/views/generic.py index 6d2d01af4..933a48ee2 100644 --- a/lnbits/core/views/generic.py +++ b/lnbits/core/views/generic.py @@ -103,13 +103,10 @@ async def extensions_install( lambda ext: { "id": ext.id, "name": ext.name, - "hash": ext.hash, - "version": ext.version, "icon": ext.icon, "iconUrl": ext.icon_url, "shortDescription": ext.short_description, "stars": ext.stars, - "details": ext.details, "dependencies": ext.dependencies, "isInstalled": ext.id in installed_extensions, "isActive": not ext.id in inactive_extensions, diff --git a/lnbits/extension_manger.py b/lnbits/extension_manger.py index 9d77c9127..0ad5fac5c 100644 --- a/lnbits/extension_manger.py +++ b/lnbits/extension_manger.py @@ -111,6 +111,7 @@ class ExtensionRelease(BaseModel): published_at: Optional[str] html_url: Optional[str] description: Optional[str] + details_html: Optional[str] = None @classmethod def from_github_release(cls, source_repo: str, r: dict) -> "ExtensionRelease": @@ -146,19 +147,25 @@ class ExtensionRelease(BaseModel): class InstallableExtension(BaseModel): id: str name: str - archive: str # todo: move to installed_release - hash: str short_description: Optional[str] = None - details: Optional[str] = None icon: Optional[str] = None icon_url: Optional[str] = None dependencies: List[str] = [] is_admin_only: bool = False - version: str = "none" # todo: move to Release stars: int = 0 latest_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 def zip_path(self) -> str: 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): os.remove(ext_zip_file) try: - download_url(self.archive, ext_zip_file) + download_url(self.installed_release.archive, ext_zip_file) except Exception as ex: logger.warning(ex) raise HTTPException( @@ -195,7 +202,7 @@ class InstallableExtension(BaseModel): ) 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 if os.path.isfile(ext_zip_file): os.remove(ext_zip_file) @@ -205,15 +212,22 @@ class InstallableExtension(BaseModel): ) 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) shutil.rmtree(self.ext_upgrade_dir, True) with zipfile.ZipFile(self.zip_path, "r") as zip_ref: 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: """Update the the list of upgraded extensions. The middleware will perform redirects based on this""" if not self.hash: @@ -261,15 +275,15 @@ class InstallableExtension(BaseModel): logger.warning(e) return None - @classmethod - async def get_extension_info(cls, ext_id: str, hash: str) -> "InstallableExtension": + @classmethod # todo: remove + async def get_extension_info( + cls, ext_id: str, archive: str + ) -> "InstallableExtension": installable_extensions: List[ InstallableExtension ] = await InstallableExtension.get_installable_extensions() - valid_extensions = [ - e for e in installable_extensions if e.id == ext_id and e.hash == hash - ] + valid_extensions = [e for e in installable_extensions if e.id == ext_id] if len(valid_extensions) == 0: raise HTTPException( status_code=HTTPStatus.BAD_REQUEST, @@ -322,7 +336,6 @@ class InstallableExtension(BaseModel): archive=e["archive"], hash=e["hash"], short_description=e["shortDescription"], - details=e["details"] if "details" in e else "", icon=e["icon"], dependencies=e["dependencies"] if "dependencies" in e @@ -365,6 +378,7 @@ class InstallableExtension(BaseModel): hash=e["hash"], source_repo=url, description=e["shortDescription"], + details_html=e.get("details"), ) ] @@ -415,6 +429,12 @@ class InstalledExtensionMiddleware: 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]: return [ extension