Files
MoviePilot/app/db/plugin/locator.py
DDSRem 2de976f6b3 Merge pull request #7050 from DDSRem-Dev/codex-comment-migration
docs: 统一全项目中文注释和结构化契约说明
2026-10-17 19:26:29 +08:00

86 lines
3.3 KiB
Python

"""
插件数据库文件路径与 PostgreSQL schema 名的解析,不持有任何状态。
"""
from __future__ import annotations
import hashlib
from pathlib import Path
from app.runtime.extensions.plugin.datadir import resolve_plugin_data_directory
__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`` 在插件什么都没声明时不得凭空产生目录。
目录解析走统一入口而不是就地拼接:销毁数据库会 ``unlink`` 这些文件,而插件标识在
重置、卸载等路径上直接来自 HTTP 路径段,就地拼接会让一个 ``..`` 把删除抬到插件数据
根之外。
:param plugin_id: 插件标识
:return: 库文件绝对路径
:raise ValueError: 插件标识不是一个合法的单层目录名
"""
return resolve_plugin_data_directory(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}"