From 1d2b939e0639e935adecccfc370a6595dc95c472 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dni=20=E2=9A=A1?= Date: Tue, 30 Jan 2024 07:47:15 +0100 Subject: [PATCH] feat: `cleanup-wallets` cli command (#2176) * feat: `cleanup-wallet` cli command `lnbits-cli db cleanup-wallets` removes all wallets that never had an transaction. this helps against spammers creating random wallets, eventually slowing down the db. * add commands * add to example env * db_versions was used in app * add delta as cli argument * use days unit * simplify cli argument name (cleanup_wallet_days -> days)! --------- Co-authored-by: Vlad Stan Co-authored-by: Pavol Rusnak --- .env.example | 3 +++ lnbits/commands.py | 42 +++++++++++++++++++++++++++++++++++------- lnbits/core/crud.py | 34 ++++++++++++++++++++++++++++++++++ lnbits/settings.py | 1 + 4 files changed, 73 insertions(+), 7 deletions(-) diff --git a/.env.example b/.env.example index c749ef239..2ed3d7f65 100644 --- a/.env.example +++ b/.env.example @@ -214,3 +214,6 @@ ENABLE_LOG_TO_FILE=true # https://loguru.readthedocs.io/en/stable/api/logger.html#file LOG_ROTATION="100 MB" LOG_RETENTION="3 months" + +# for database cleanup commands +# CLEANUP_WALLETS_DAYS=90 diff --git a/lnbits/commands.py b/lnbits/commands.py index 5d8d8924d..2aa848247 100644 --- a/lnbits/commands.py +++ b/lnbits/commands.py @@ -18,10 +18,13 @@ from lnbits.settings import settings from .core import db as core_db from .core import migrations as core_migrations from .core.crud import ( + delete_accounts_no_wallets, + delete_unused_wallets, get_dbversions, get_inactive_extensions, get_installed_extension, get_installed_extensions, + remove_deleted_wallets, ) from .core.helpers import migrate_extension_database, run_migration from .db import COCKROACH, POSTGRES, SQLITE @@ -151,16 +154,41 @@ async def migrate_databases(): @db.command("versions") -def database_versions(): - """Show current database versions""" - loop = asyncio.get_event_loop() - loop.run_until_complete(db_versions()) - - +@coro async def db_versions(): """Show current database versions""" async with core_db.connect() as conn: - return await get_dbversions(conn) + click.echo(await get_dbversions(conn)) + + +@db.command("cleanup-wallets") +@click.argument("days", type=int, required=False) +@coro +async def database_cleanup_wallets(days: Optional[int] = None): + """Delete all wallets that never had any transaction""" + async with core_db.connect() as conn: + delta = days or settings.cleanup_wallets_days + delta = delta * 24 * 60 * 60 + await delete_unused_wallets(delta, conn) + + +@db.command("cleanup-deleted-wallets") +@coro +async def database_cleanup_deleted_wallets(): + """Delete all wallets that has been marked deleted""" + async with core_db.connect() as conn: + await remove_deleted_wallets(conn) + + +@db.command("cleanup-accounts") +@click.argument("days", type=int, required=False) +@coro +async def database_cleanup_accounts(days: Optional[int] = None): + """Delete all accounts that have no wallets""" + async with core_db.connect() as conn: + delta = days or settings.cleanup_wallets_days + delta = delta * 24 * 60 * 60 + await delete_accounts_no_wallets(delta, conn) async def load_disabled_extension_list() -> None: diff --git a/lnbits/core/crud.py b/lnbits/core/crud.py index c13db5495..00012df96 100644 --- a/lnbits/core/crud.py +++ b/lnbits/core/crud.py @@ -164,6 +164,21 @@ async def get_account( return user +async def delete_accounts_no_wallets( + time_delta: int, + conn: Optional[Connection] = None, +) -> None: + await (conn or db).execute( + f""" + DELETE FROM accounts + WHERE NOT EXISTS ( + SELECT wallets.id FROM wallets WHERE wallets.user = accounts.id + ) AND updated_at < {db.timestamp_placeholder} + """, + (int(time()) - time_delta,), + ) + + async def get_user_password(user_id: str) -> Optional[str]: row = await db.fetchone( "SELECT pass FROM accounts WHERE id = ?", @@ -497,6 +512,25 @@ async def delete_wallet( ) +async def remove_deleted_wallets(conn: Optional[Connection] = None) -> None: + await (conn or db).execute("DELETE FROM wallets WHERE deleted = true") + + +async def delete_unused_wallets( + time_delta: int, + conn: Optional[Connection] = None, +) -> None: + await (conn or db).execute( + f""" + DELETE FROM wallets + WHERE ( + SELECT COUNT(*) FROM apipayments WHERE wallet = wallets.id + ) = 0 AND updated_at < {db.timestamp_placeholder} + """, + (int(time()) - time_delta,), + ) + + async def get_wallet( wallet_id: str, conn: Optional[Connection] = None ) -> Optional[Wallet]: diff --git a/lnbits/settings.py b/lnbits/settings.py index 5c153d7f0..ab7de1224 100644 --- a/lnbits/settings.py +++ b/lnbits/settings.py @@ -348,6 +348,7 @@ class EnvSettings(LNbitsSettings): log_retention: str = Field(default="3 months") server_startup_time: int = Field(default=time()) lnbits_extensions_deactivate_all: bool = Field(default=False) + cleanup_wallets_days: int = Field(default=90) @property def has_default_extension_path(self) -> bool: