Add option to drop extension db at un-install time or later (#1746)
* chore: remove un-used file * feat: allow extension DB clean-up * feat: i18n and bundle update * chore: code format * fix: button color * chore: delete temp file * chore: fix merge conflicts * chore: add extra log * chore: bump CACHE_VERSION to `37`
This commit is contained in:
parent
95281eba8c
commit
8c0e7725de
9 changed files with 170 additions and 46 deletions
|
|
@ -7,7 +7,7 @@ from uuid import UUID, uuid4
|
||||||
import shortuuid
|
import shortuuid
|
||||||
|
|
||||||
from lnbits import bolt11
|
from lnbits import bolt11
|
||||||
from lnbits.db import Connection, Filters, Page
|
from lnbits.db import Connection, Database, Filters, Page
|
||||||
from lnbits.extension_manager import InstallableExtension
|
from lnbits.extension_manager import InstallableExtension
|
||||||
from lnbits.settings import AdminSettings, EditableSettings, SuperSettings, settings
|
from lnbits.settings import AdminSettings, EditableSettings, SuperSettings, settings
|
||||||
|
|
||||||
|
|
@ -142,6 +142,25 @@ async def delete_installed_extension(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def drop_extension_db(*, ext_id: str, conn: Optional[Connection] = None) -> None:
|
||||||
|
db_version = await (conn or db).fetchone(
|
||||||
|
"SELECT * FROM dbversions WHERE db = ?", (ext_id,)
|
||||||
|
)
|
||||||
|
# Check that 'ext_id' is a valid extension id and not a malicious string
|
||||||
|
assert db_version, f"Extension '{ext_id}' db version cannot be found"
|
||||||
|
|
||||||
|
is_file_based_db = await Database.clean_ext_db_files(ext_id)
|
||||||
|
if is_file_based_db:
|
||||||
|
return
|
||||||
|
|
||||||
|
# String formatting is required, params are not accepted for 'DROP SCHEMA'.
|
||||||
|
# The `ext_id` value is verified above.
|
||||||
|
await (conn or db).execute(
|
||||||
|
f"DROP SCHEMA IF EXISTS {ext_id} CASCADE",
|
||||||
|
(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_installed_extension(ext_id: str, conn: Optional[Connection] = None):
|
async def get_installed_extension(ext_id: str, conn: Optional[Connection] = None):
|
||||||
row = await (conn or db).fetchone(
|
row = await (conn or db).fetchone(
|
||||||
"SELECT * FROM installed_extensions WHERE id = ?",
|
"SELECT * FROM installed_extensions WHERE id = ?",
|
||||||
|
|
@ -781,6 +800,15 @@ async def update_migration_version(conn, db_name, version):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_dbversion(*, ext_id: str, conn: Optional[Connection] = None) -> None:
|
||||||
|
await (conn or db).execute(
|
||||||
|
"""
|
||||||
|
DELETE FROM dbversions WHERE db = ?
|
||||||
|
""",
|
||||||
|
(ext_id,),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# tinyurl
|
# tinyurl
|
||||||
# -------
|
# -------
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
new Vue({
|
|
||||||
el: '#vue',
|
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
searchTerm: '',
|
|
||||||
filteredExtensions: [],
|
|
||||||
maxStars: 5,
|
|
||||||
user: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.filteredExtensions = this.g.extensions
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
searchTerm(term) {
|
|
||||||
// Reset the filter
|
|
||||||
this.filteredExtensions = this.g.extensions
|
|
||||||
if (term !== '') {
|
|
||||||
// Filter the extensions list
|
|
||||||
function extensionNameContains(searchTerm) {
|
|
||||||
return function (extension) {
|
|
||||||
return (
|
|
||||||
extension.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
||||||
extension.shortDescription
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(searchTerm.toLowerCase())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.filteredExtensions = this.filteredExtensions.filter(
|
|
||||||
extensionNameContains(term)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
if (window.user) {
|
|
||||||
this.user = LNbits.map.user(window.user)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mixins: [windowMixin]
|
|
||||||
})
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// update cache version every time there is a new deployment
|
// update cache version every time there is a new deployment
|
||||||
// so the service worker reinitializes the cache
|
// so the service worker reinitializes the cache
|
||||||
const CACHE_VERSION = 35
|
const CACHE_VERSION = 37
|
||||||
const CURRENT_CACHE = `lnbits-${CACHE_VERSION}-`
|
const CURRENT_CACHE = `lnbits-${CACHE_VERSION}-`
|
||||||
|
|
||||||
const getApiKey = request => {
|
const getApiKey = request => {
|
||||||
|
|
|
||||||
|
|
@ -256,6 +256,17 @@
|
||||||
{%raw%}{{ $t('confirm_continue') }}{%endraw%}
|
{%raw%}{{ $t('confirm_continue') }}{%endraw%}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<div class="row q-mt-lg">
|
||||||
|
<q-checkbox
|
||||||
|
v-model="uninstallAndDropDb"
|
||||||
|
value="false"
|
||||||
|
label="Cleanup database tables"
|
||||||
|
>
|
||||||
|
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left">
|
||||||
|
{%raw%}{{ $t('extension_db_drop_info') }}{%endraw%}
|
||||||
|
</q-tooltip>
|
||||||
|
</q-checkbox>
|
||||||
|
</div>
|
||||||
<div class="row q-mt-lg">
|
<div class="row q-mt-lg">
|
||||||
<q-btn outline color="grey" @click="uninstallExtension()"
|
<q-btn outline color="grey" @click="uninstallExtension()"
|
||||||
>{%raw%}{{ $t('uninstall_confirm') }}{%endraw%}</q-btn
|
>{%raw%}{{ $t('uninstall_confirm') }}{%endraw%}</q-btn
|
||||||
|
|
@ -267,6 +278,32 @@
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
|
||||||
|
<q-dialog v-model="showDropDbDialog">
|
||||||
|
<q-card v-if="selectedExtension" class="q-pa-lg">
|
||||||
|
<h6 class="q-my-md text-primary">{%raw%}{{ $t('warning') }}{%endraw%}</h6>
|
||||||
|
<p>{%raw%}{{ $t('extension_db_drop_warning') }}{%endraw%} <br /></p>
|
||||||
|
<q-input
|
||||||
|
v-model="dropDbExtensionId"
|
||||||
|
:label="selectedExtension.id"
|
||||||
|
></q-input>
|
||||||
|
<br />
|
||||||
|
<p>{%raw%}{{ $t('confirm_continue') }}{%endraw%}</p>
|
||||||
|
|
||||||
|
<div class="row q-mt-lg">
|
||||||
|
<q-btn
|
||||||
|
:disable="dropDbExtensionId !== selectedExtension.id"
|
||||||
|
outline
|
||||||
|
color="red"
|
||||||
|
@click="dropExtensionDb()"
|
||||||
|
>{%raw%}{{ $t('confirm') }}{%endraw%}</q-btn
|
||||||
|
>
|
||||||
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||||
|
>{%raw%}{{ $t('cancel') }}{%endraw%}</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
|
||||||
<q-dialog v-model="showUpgradeDialog">
|
<q-dialog v-model="showUpgradeDialog">
|
||||||
<q-card class="q-pa-lg lnbits__dialog-card">
|
<q-card class="q-pa-lg lnbits__dialog-card">
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
|
|
@ -395,6 +432,13 @@
|
||||||
>
|
>
|
||||||
{%raw%}{{ $t('uninstall') }}{%endraw%}</q-btn
|
{%raw%}{{ $t('uninstall') }}{%endraw%}</q-btn
|
||||||
>
|
>
|
||||||
|
<q-btn
|
||||||
|
v-else-if="selectedExtension?.hasDatabaseTables"
|
||||||
|
@click="showDropDb()"
|
||||||
|
flat
|
||||||
|
color="red"
|
||||||
|
:label="$t('drop_db')"
|
||||||
|
></q-btn>
|
||||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto">
|
||||||
{%raw%}{{ $t('close') }}{%endraw%}</q-btn
|
{%raw%}{{ $t('close') }}{%endraw%}</q-btn
|
||||||
>
|
>
|
||||||
|
|
@ -413,8 +457,11 @@
|
||||||
filteredExtensions: null,
|
filteredExtensions: null,
|
||||||
showUninstallDialog: false,
|
showUninstallDialog: false,
|
||||||
showUpgradeDialog: false,
|
showUpgradeDialog: false,
|
||||||
|
showDropDbDialog: false,
|
||||||
|
dropDbExtensionId: '',
|
||||||
selectedExtension: null,
|
selectedExtension: null,
|
||||||
selectedExtensionRepos: null,
|
selectedExtensionRepos: null,
|
||||||
|
uninstallAndDropDb: false,
|
||||||
maxStars: 5,
|
maxStars: 5,
|
||||||
user: null
|
user: null
|
||||||
}
|
}
|
||||||
|
|
@ -503,6 +550,40 @@
|
||||||
this.filteredExtensions = this.extensions.concat([])
|
this.filteredExtensions = this.extensions.concat([])
|
||||||
this.handleTabChanged('installed')
|
this.handleTabChanged('installed')
|
||||||
this.tab = 'installed'
|
this.tab = 'installed'
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Extension uninstalled!'
|
||||||
|
})
|
||||||
|
if (this.uninstallAndDropDb) {
|
||||||
|
this.showDropDb()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
LNbits.utils.notifyApiError(err)
|
||||||
|
extension.inProgress = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
dropExtensionDb: async function () {
|
||||||
|
const extension = this.selectedExtension
|
||||||
|
this.showUpgradeDialog = false
|
||||||
|
this.showDropDbDialog = false
|
||||||
|
this.dropDbExtensionId = ''
|
||||||
|
extension.inProgress = true
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'DELETE',
|
||||||
|
`/api/v1/extension/${extension.id}/db?usr=${this.g.user.id}`,
|
||||||
|
this.g.user.wallets[0].adminkey
|
||||||
|
)
|
||||||
|
.then(response => {
|
||||||
|
extension.installedRelease = null
|
||||||
|
extension.inProgress = false
|
||||||
|
extension.hasDatabaseTables = false
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Extension DB deleted!'
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
LNbits.utils.notifyApiError(err)
|
LNbits.utils.notifyApiError(err)
|
||||||
|
|
@ -531,6 +612,11 @@
|
||||||
showUninstall: function () {
|
showUninstall: function () {
|
||||||
this.showUpgradeDialog = false
|
this.showUpgradeDialog = false
|
||||||
this.showUninstallDialog = true
|
this.showUninstallDialog = true
|
||||||
|
this.uninstallAndDropDb = false
|
||||||
|
},
|
||||||
|
|
||||||
|
showDropDb: function () {
|
||||||
|
this.showDropDbDialog = true
|
||||||
},
|
},
|
||||||
|
|
||||||
showUpgrade: async function (extension) {
|
showUpgrade: async function (extension) {
|
||||||
|
|
|
||||||
|
|
@ -63,8 +63,10 @@ from .. import core_app, core_app_extra, db
|
||||||
from ..crud import (
|
from ..crud import (
|
||||||
add_installed_extension,
|
add_installed_extension,
|
||||||
create_tinyurl,
|
create_tinyurl,
|
||||||
|
delete_dbversion,
|
||||||
delete_installed_extension,
|
delete_installed_extension,
|
||||||
delete_tinyurl,
|
delete_tinyurl,
|
||||||
|
drop_extension_db,
|
||||||
get_dbversions,
|
get_dbversions,
|
||||||
get_payments,
|
get_payments,
|
||||||
get_payments_paginated,
|
get_payments_paginated,
|
||||||
|
|
@ -902,6 +904,32 @@ async def get_extension_release(org: str, repo: str, tag_name: str):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@core_app.delete(
|
||||||
|
"/api/v1/extension/{ext_id}/db",
|
||||||
|
dependencies=[Depends(check_admin)],
|
||||||
|
)
|
||||||
|
async def delete_extension_db(ext_id: str):
|
||||||
|
try:
|
||||||
|
db_version = (await get_dbversions()).get(ext_id, None)
|
||||||
|
if not db_version:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
|
detail=f"Unknown extension id: {ext_id}",
|
||||||
|
)
|
||||||
|
await drop_extension_db(ext_id=ext_id)
|
||||||
|
await delete_dbversion(ext_id=ext_id)
|
||||||
|
logger.success(f"Database removed for extension '{ext_id}'")
|
||||||
|
except HTTPException as ex:
|
||||||
|
logger.error(ex)
|
||||||
|
raise ex
|
||||||
|
except Exception as ex:
|
||||||
|
logger.error(ex)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
detail=f"Cannot delete data for extension '{ext_id}'",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# TINYURL
|
# TINYURL
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ from ..crud import (
|
||||||
create_wallet,
|
create_wallet,
|
||||||
delete_wallet,
|
delete_wallet,
|
||||||
get_balance_check,
|
get_balance_check,
|
||||||
|
get_dbversions,
|
||||||
get_inactive_extensions,
|
get_inactive_extensions,
|
||||||
get_installed_extensions,
|
get_installed_extensions,
|
||||||
get_user,
|
get_user,
|
||||||
|
|
@ -113,6 +114,7 @@ async def extensions_install(
|
||||||
|
|
||||||
all_extensions = list(map(lambda e: e.code, get_valid_extensions()))
|
all_extensions = list(map(lambda e: e.code, get_valid_extensions()))
|
||||||
inactive_extensions = await get_inactive_extensions()
|
inactive_extensions = await get_inactive_extensions()
|
||||||
|
db_version = await get_dbversions()
|
||||||
extensions = list(
|
extensions = list(
|
||||||
map(
|
map(
|
||||||
lambda ext: {
|
lambda ext: {
|
||||||
|
|
@ -124,6 +126,7 @@ async def extensions_install(
|
||||||
"isFeatured": ext.featured,
|
"isFeatured": ext.featured,
|
||||||
"dependencies": ext.dependencies,
|
"dependencies": ext.dependencies,
|
||||||
"isInstalled": ext.id in installed_exts_ids,
|
"isInstalled": ext.id in installed_exts_ids,
|
||||||
|
"hasDatabaseTables": ext.id in db_version,
|
||||||
"isAvailable": ext.id in all_extensions,
|
"isAvailable": ext.id in all_extensions,
|
||||||
"isActive": ext.id not in inactive_extensions,
|
"isActive": ext.id not in inactive_extensions,
|
||||||
"latestRelease": dict(ext.latest_release)
|
"latestRelease": dict(ext.latest_release)
|
||||||
|
|
|
||||||
15
lnbits/db.py
15
lnbits/db.py
|
|
@ -303,6 +303,21 @@ class Database(Compat):
|
||||||
async def reuse_conn(self, conn: Connection):
|
async def reuse_conn(self, conn: Connection):
|
||||||
yield conn
|
yield conn
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def clean_ext_db_files(self, ext_id: str) -> bool:
|
||||||
|
"""
|
||||||
|
If the extension DB is stored directly on the filesystem (like SQLite) then delete the files and return True.
|
||||||
|
Otherwise do nothing and return False.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if DB_TYPE == SQLITE:
|
||||||
|
db_file = os.path.join(settings.lnbits_data_folder, f"ext_{ext_id}.sqlite3")
|
||||||
|
if os.path.isfile(db_file):
|
||||||
|
os.remove(db_file)
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class Operator(Enum):
|
class Operator(Enum):
|
||||||
GT = "gt"
|
GT = "gt"
|
||||||
|
|
|
||||||
2
lnbits/static/bundle.min.js
vendored
2
lnbits/static/bundle.min.js
vendored
File diff suppressed because one or more lines are too long
|
|
@ -1,4 +1,5 @@
|
||||||
window.localisation.en = {
|
window.localisation.en = {
|
||||||
|
confirm: 'Yes',
|
||||||
server: 'Server',
|
server: 'Server',
|
||||||
theme: 'Theme',
|
theme: 'Theme',
|
||||||
funding: 'Funding',
|
funding: 'Funding',
|
||||||
|
|
@ -86,6 +87,7 @@ window.localisation.en = {
|
||||||
manage_extension_details: 'Install/uninstall extension',
|
manage_extension_details: 'Install/uninstall extension',
|
||||||
install: 'Install',
|
install: 'Install',
|
||||||
uninstall: 'Uninstall',
|
uninstall: 'Uninstall',
|
||||||
|
drop_db: 'Remove Data',
|
||||||
open: 'Open',
|
open: 'Open',
|
||||||
enable: 'Enable',
|
enable: 'Enable',
|
||||||
enable_extension_details: 'Enable extension for current user',
|
enable_extension_details: 'Enable extension for current user',
|
||||||
|
|
@ -105,6 +107,11 @@ window.localisation.en = {
|
||||||
extension_uninstall_warning:
|
extension_uninstall_warning:
|
||||||
'You are about to remove the extension for all users.',
|
'You are about to remove the extension for all users.',
|
||||||
uninstall_confirm: 'Yes, Uninstall',
|
uninstall_confirm: 'Yes, Uninstall',
|
||||||
|
extension_db_drop_info:
|
||||||
|
'All data for the extension will be permanently deleted. There is no way to undo this operation!',
|
||||||
|
extension_db_drop_warning:
|
||||||
|
'You are about to remove all data for the extension. Please type the extension name to continue:',
|
||||||
|
|
||||||
extension_min_lnbits_version: 'This release requires at least LNbits version',
|
extension_min_lnbits_version: 'This release requires at least LNbits version',
|
||||||
|
|
||||||
payment_hash: 'Payment Hash',
|
payment_hash: 'Payment Hash',
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue