Files
MoviePilot/app/db/plugin/locator.py
Aqr-K d7c5955031 fix(plugin): 插件自有数据库释放与销毁全程容错,并修正建表去重与 schema 命名
release/destroy 统一经 _close_handle 先清线程局部会话再 dispose,两步失败都
只记日志:单个插件的连接池故障不再让 close_database 跳过宿主同步与异步引擎的
释放,release_all_databases 也逐插件隔离。按模型建表对 __table__ 去重,单表
继承的父子类一并声明时不再对同一张表建两次。

卸载编排先删插件数据、后停插件,停机钩子里的 get_database() 会把刚销毁的库
重新建出来:注册表记下已销毁的插件标识,其后的 release 改为再次销毁并清除
标记,ensure 则撤销标记,宿主编排代码不动。PostgreSQL schema 名在归一改写过
标识或超长时追加插件标识哈希并截到 63 字节,My-Plugin、My_Plugin 与
my_plugin 不再折叠到同一个 schema 而在卸载时互相 DROP。

start 的失败分支释放已经建立、却进不了运行态的句柄,整体停止把只登记在
_classes 里的插件一并释放。声明的迁移目录不存在时在建句柄之前抛
FileNotFoundError,不再留下空库;SQLite 迁移 URL 改由引擎渲染;
_get_sqlite_engine 的连接参数下沉到实际使用它的异步分支。
2026-09-02 02:14:19 -04:00

76 lines
2.9 KiB
Python

"""插件数据库文件路径与 PostgreSQL schema 名的解析,不持有任何状态。"""
from __future__ import annotations
import hashlib
from pathlib import Path
from app.runtime.settings import get_runtime_setting
__all__ = [
"plugin_schema_name",
"sqlite_database_path",
"sqlite_sidecar_paths",
]
# 库文件名固定:所在目录已按插件标识分段,文件名无需再编码插件身份
DATABASE_FILENAME = "plugin.db"
# SQLite WAL 模式下与库文件同生共死的边车文件后缀
SIDECAR_SUFFIXES = ("-wal", "-shm")
# PostgreSQL 标识符上限 63 字节,超出部分会被静默截断成另一个 schema
SCHEMA_NAME_MAX_LENGTH = 63
# 归一改写了插件标识时用于区分两个插件的哈希长度
SCHEMA_NAME_HASH_LENGTH = 8
def sqlite_database_path(plugin_id: str) -> Path:
"""
返回插件 SQLite 库文件路径。
落在与 ``_PluginBase.get_data_path()`` 完全相同的插件数据目录下,插件库因此随插件
数据一起被备份、迁移和删除,不会形成第二份需要单独维护的持久化根。本函数不创建
目录:``ensure`` 在插件什么都没声明时不得凭空产生目录。
:param plugin_id: 插件标识
:return: 库文件绝对路径
"""
return Path(get_runtime_setting("PLUGIN_DATA_PATH")) / plugin_id / DATABASE_FILENAME
def sqlite_sidecar_paths(db_path: Path) -> tuple[Path, ...]:
"""
返回库文件对应的 WAL/SHM 边车文件路径。
:param db_path: 库文件路径
:return: 边车路径元组,不保证文件存在
"""
return tuple(db_path.with_name(db_path.name + suffix) for suffix in SIDECAR_SUFFIXES)
def plugin_schema_name(plugin_id: str) -> str:
"""
按插件标识拼出 PostgreSQL schema 名。
只保留 ASCII 小写字母、数字与下划线,避免插件标识中的字符逃逸成标识符片段,也让
名字的字符数与字节数一致。归一是多对一的:``My-Plugin``、``My_Plugin`` 与
``my_plugin`` 会折叠到同一个名字,而卸载一个插件执行的是 ``DROP SCHEMA ...
CASCADE``,折叠意味着删掉另一个插件的全部数据。
因此只要归一改写过标识,就追加插件标识的哈希把它们重新分开;超长标识同样追加哈希
再截断,截断本身也是一次折叠。
:param plugin_id: 插件标识
:return: 合法且与插件标识一一对应的 schema 名
"""
raw = f"plugin_{plugin_id}"
sanitized = "".join(
character
if (character.isascii() and character.isalnum()) or character == "_"
else "_"
for character in raw.lower()
)
if sanitized == raw and len(sanitized) <= SCHEMA_NAME_MAX_LENGTH:
return sanitized
digest = hashlib.sha1(
plugin_id.encode("utf-8"),
usedforsecurity=False,
).hexdigest()[:SCHEMA_NAME_HASH_LENGTH]
suffix = f"_{digest}"
return f"{sanitized[:SCHEMA_NAME_MAX_LENGTH - len(suffix)]}{suffix}"