Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support custom path for installed extensions #1940

Merged
merged 26 commits into from
Sep 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d4e9bfb
chore: experiment
motorina0 Sep 5, 2023
7f0d0ec
chore: experiment
motorina0 Sep 6, 2023
beef12d
feat: the more you fuck around the more you learn
motorina0 Sep 13, 2023
b6701b4
chore: code format
motorina0 Sep 13, 2023
91ccd41
chore: some code clean-up
motorina0 Sep 13, 2023
b5cb1fe
feat: backwards compatible static file loading
motorina0 Sep 13, 2023
2d277bb
chore: code clean-up
motorina0 Sep 13, 2023
0b38a9e
refactor: update paths for extension files
motorina0 Sep 13, 2023
01fd045
fix: static files path
motorina0 Sep 13, 2023
bf3ac3d
chore: code clean-up
motorina0 Sep 13, 2023
33ac830
fix: keep old paths too
motorina0 Sep 13, 2023
69ac96f
chore: revert test change
motorina0 Sep 13, 2023
e457494
refactor: var renaming
motorina0 Sep 13, 2023
1f90a52
doc: update `LNBITS_EXTENSIONS_PATH` documentation
motorina0 Sep 13, 2023
2b7b0d4
fix: default folder install
motorina0 Sep 13, 2023
c3cedad
refactor: rename property
motorina0 Sep 14, 2023
b125434
feat: install ext without external path
motorina0 Sep 14, 2023
c8bc411
doc: `PYTHONPATH` no longer required
motorina0 Sep 14, 2023
84a6a60
chore: code clean-up
motorina0 Sep 14, 2023
4b571f2
fix: add warnings
motorina0 Sep 14, 2023
592a1fb
chore: fix typo
motorina0 Sep 14, 2023
9389968
fix: missing path
motorina0 Sep 14, 2023
d1e699f
refactor: re-order statements
motorina0 Sep 14, 2023
2023c32
doc: typo
motorina0 Sep 14, 2023
ca01a37
fix: hardcoded path separator
motorina0 Sep 19, 2023
3dd91b8
Merge branch 'dev' into import_external_module
dni Sep 25, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ LNBITS_HIDE_API=false
# GitHub has rate-limits for its APIs. The limit can be increased specifying a GITHUB_TOKEN
# LNBITS_EXT_GITHUB_TOKEN=github_pat_xxxxxxxxxxxxxxxxxx

# Path where extensions will be installed (defaults to `./lnbits/`).
# Inside this directory the `extensions` and `upgrades` sub-directories will be created.
# LNBITS_EXTENSIONS_PATH="/path/to/some/dir"


# Extensions to be installed by default. If an extension from this list is uninstalled then it will be re-installed on the next restart.
# The extension must be removed from this list in order to not be re-installed.
LNBITS_EXTENSIONS_DEFAULT_INSTALL="tpos"
Expand Down
31 changes: 27 additions & 4 deletions lnbits/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import traceback
from hashlib import sha256
from http import HTTPStatus
from pathlib import Path
from typing import Callable, List

from fastapi import FastAPI, HTTPException, Request
Expand Down Expand Up @@ -96,6 +97,8 @@ def create_app() -> FastAPI:
app.add_middleware(InstalledExtensionMiddleware)
app.add_middleware(ExtensionsRedirectMiddleware)

register_custom_extensions_path()

# adds security middleware
add_ip_block_middleware(app)
add_ratelimit_middleware(app)
Expand Down Expand Up @@ -229,9 +232,7 @@ def check_installed_extension_files(ext: InstallableExtension) -> bool:
if ext.has_installed_version:
return True

zip_files = glob.glob(
os.path.join(settings.lnbits_data_folder, "extensions", "*.zip")
)
zip_files = glob.glob(os.path.join(settings.lnbits_data_folder, "zips", "*.zip"))
prusnak marked this conversation as resolved.
Show resolved Hide resolved

if f"./{str(ext.zip_path)}" not in zip_files:
ext.download_archive()
Expand Down Expand Up @@ -267,6 +268,25 @@ def register_routes(app: FastAPI) -> None:
logger.error(f"Could not load extension `{ext.code}`: {str(e)}")


def register_custom_extensions_path():
if settings.has_default_extension_path:
return
default_ext_path = os.path.join("lnbits", "extensions")
if os.path.isdir(default_ext_path) and len(os.listdir(default_ext_path)) != 0:
logger.warning(
"You are using a custom extensions path, "
+ "but the default extensions directory is not empty. "
+ f"Please clean-up the '{default_ext_path}' directory."
)
logger.warning(
f"You can move the existing '{default_ext_path}' directory to: "
+ f" '{settings.lnbits_extensions_path}/extensions'"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should use os.path.join() instead of a string concatenation.

Copy link
Collaborator Author

@motorina0 motorina0 Sep 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its just a log message here

)

sys.path.append(str(Path(settings.lnbits_extensions_path, "extensions")))
sys.path.append(str(Path(settings.lnbits_extensions_path, "upgrades")))
Comment on lines +286 to +287
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again os.path.join would help

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is wrong with Path?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i prefer Path



def register_new_ext_routes(app: FastAPI) -> Callable:
# Returns a function that registers new routes for an extension.
# The returned function encapsulates (creates a closure around)
Expand Down Expand Up @@ -303,7 +323,10 @@ def register_ext_routes(app: FastAPI, ext: Extension) -> None:
if hasattr(ext_module, f"{ext.code}_static_files"):
ext_statics = getattr(ext_module, f"{ext.code}_static_files")
for s in ext_statics:
app.mount(s["path"], s["app"], s["name"])
static_dir = Path(
settings.lnbits_extensions_path, "extensions", *s["path"].split("/")
)
app.mount(s["path"], StaticFiles(directory=static_dir), s["name"])

if hasattr(ext_module, f"{ext.code}_redirect_paths"):
ext_redirects = getattr(ext_module, f"{ext.code}_redirect_paths")
Expand Down
63 changes: 38 additions & 25 deletions lnbits/extension_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,12 +182,19 @@ class Extension(NamedTuple):
upgrade_hash: Optional[str] = ""

@property
def module_name(self):
return (
f"lnbits.extensions.{self.code}"
if self.upgrade_hash == ""
else f"lnbits.upgrades.{self.code}-{self.upgrade_hash}.{self.code}"
)
def module_name(self) -> str:
if self.is_upgrade_extension:
if settings.has_default_extension_path:
return f"lnbits.upgrades.{self.code}-{self.upgrade_hash}"
return f"{self.code}-{self.upgrade_hash}"

if settings.has_default_extension_path:
return f"lnbits.extensions.{self.code}"
return self.code

@property
def is_upgrade_extension(self) -> bool:
return self.upgrade_hash != ""

@classmethod
def from_installable_ext(cls, ext_info: "InstallableExtension") -> "Extension":
Expand All @@ -205,7 +212,7 @@ def from_installable_ext(cls, ext_info: "InstallableExtension") -> "Extension":

class ExtensionManager:
def __init__(self) -> None:
p = Path(settings.lnbits_path, "extensions")
p = Path(settings.lnbits_extensions_path, "extensions")
Path(p).mkdir(parents=True, exist_ok=True)
self._extension_folders: List[Path] = [f for f in p.iterdir() if f.is_dir()]

Expand Down Expand Up @@ -330,21 +337,25 @@ def hash(self) -> str:

@property
def zip_path(self) -> Path:
extensions_data_dir = Path(settings.lnbits_data_folder, "extensions")
extensions_data_dir = Path(settings.lnbits_data_folder, "zips")
Path(extensions_data_dir).mkdir(parents=True, exist_ok=True)
return Path(extensions_data_dir, f"{self.id}.zip")

@property
def ext_dir(self) -> Path:
return Path(settings.lnbits_path, "extensions", self.id)
return Path(settings.lnbits_extensions_path, "extensions", self.id)

@property
def ext_upgrade_dir(self) -> Path:
return Path("lnbits", "upgrades", f"{self.id}-{self.hash}")
return Path(
settings.lnbits_extensions_path, "upgrades", f"{self.id}-{self.hash}"
)

@property
def module_name(self) -> str:
return f"lnbits.extensions.{self.id}"
if settings.has_default_extension_path:
return f"lnbits.extensions.{self.id}"
return self.id

@property
def module_installed(self) -> bool:
Expand Down Expand Up @@ -389,21 +400,26 @@ def download_archive(self):

def extract_archive(self):
logger.info(f"Extracting extension {self.name} ({self.installed_version}).")
Path("lnbits", "upgrades").mkdir(parents=True, exist_ok=True)
shutil.rmtree(self.ext_upgrade_dir, True)
Path(settings.lnbits_extensions_path, "upgrades").mkdir(
parents=True, exist_ok=True
)

tmp_dir = Path(settings.lnbits_data_folder, "unzip-temp", self.hash)
shutil.rmtree(tmp_dir, True)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this one can error (for example insufficient permissions), consider try: except:

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in that case it is OK for the exception to be propagated (should not be hidden)


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(
Path(self.ext_upgrade_dir, generated_dir_name),
Path(self.ext_upgrade_dir, self.id),
zip_ref.extractall(tmp_dir)
generated_dir_name = os.listdir(tmp_dir)[0]
shutil.rmtree(self.ext_upgrade_dir, True)
shutil.copytree(
Path(tmp_dir, generated_dir_name),
Path(self.ext_upgrade_dir),
)
shutil.rmtree(tmp_dir, True)
Comment on lines +413 to +418
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, all this can error.

Copy link
Collaborator Author

@motorina0 motorina0 Sep 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let it fail ❄️ , let it fail ❄️ , let it fail ❄️ ☃️ 🏔️


# Pre-packed extensions can be upgraded
# Mark the extension as installed so we know it is not the pre-packed version
with open(
Path(self.ext_upgrade_dir, self.id, "config.json"), "r+"
) as json_file:
with open(Path(self.ext_upgrade_dir, "config.json"), "r+") as json_file:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should you check for existence of this file before opening?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • if the file does not exist then open will throw
  • this is fine extract_archive should fail in this case

config_json = json.load(json_file)

self.name = config_json.get("name")
Expand All @@ -419,10 +435,7 @@ def extract_archive(self):
)

shutil.rmtree(self.ext_dir, True)
shutil.copytree(
Path(self.ext_upgrade_dir, self.id),
Path(settings.lnbits_path, "extensions", self.id),
)
shutil.copytree(Path(self.ext_upgrade_dir), Path(self.ext_dir))
logger.success(f"Extension {self.name} ({self.installed_version}) installed.")

def nofiy_upgrade(self) -> None:
Expand Down
4 changes: 4 additions & 0 deletions lnbits/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ def url_for(endpoint: str, external: Optional[bool] = False, **params: Any) -> s
def template_renderer(additional_folders: Optional[List] = None) -> Jinja2Templates:
folders = ["lnbits/templates", "lnbits/core/templates"]
if additional_folders:
additional_folders += [
Path(settings.lnbits_extensions_path, "extensions", f)
Copy link
Collaborator

@prusnak prusnak Sep 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we want to perform this hack to allow compatibility with old extensions?

But I agree it is just better to perform a clean cut.

Suggested change
Path(settings.lnbits_extensions_path, "extensions", f)
Path(settings.lnbits_extensions_path, "extensions", f[18:] if f.startswith("lnbits/extensions/" else f)

for f in additional_folders
]
folders.extend(additional_folders)
t = Jinja2Templates(loader=jinja2.FileSystemLoader(folders))

Expand Down
6 changes: 4 additions & 2 deletions lnbits/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@ def main(
# create data dir if it does not exist
Path(settings.lnbits_data_folder).mkdir(parents=True, exist_ok=True)

# create extension dir if it does not exist
Path(settings.lnbits_path, "extensions").mkdir(parents=True, exist_ok=True)
# create `extensions`` dir if it does not exist
Path(settings.lnbits_extensions_path, "extensions").mkdir(
parents=True, exist_ok=True
)

set_cli_settings(host=host, port=port, forwarded_allow_ips=forwarded_allow_ips)

Expand Down
5 changes: 5 additions & 0 deletions lnbits/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,10 +275,15 @@ class EnvSettings(LNbitsSettings):
forwarded_allow_ips: str = Field(default="*")
lnbits_title: str = Field(default="LNbits API")
lnbits_path: str = Field(default=".")
lnbits_extensions_path: str = Field(default="lnbits")
lnbits_commit: str = Field(default="unknown")
super_user: str = Field(default="")
version: str = Field(default="0.0.0")

@property
def has_default_extension_path(self) -> bool:
return self.lnbits_extensions_path == "lnbits"


class SaaSSettings(LNbitsSettings):
lnbits_saas_callback: Optional[str] = Field(default=None)
Expand Down