mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-05-13 07:26:45 +00:00
fix(postgresql): support unix socket connections
Allow PostgreSQL socket paths without forcing a TCP port and reuse a single URL builder for sync, async, and migration flows. Document Redis socket URLs and close the socket connection request. Closes #5720
This commit is contained in:
@@ -281,7 +281,10 @@ class PromptManager:
|
||||
db_info = f"SQLite ({settings.CONFIG_PATH / 'db' / 'moviepilot.db'})"
|
||||
else:
|
||||
db_password = settings.DB_POSTGRESQL_PASSWORD or ""
|
||||
db_info = f"PostgreSQL ({settings.DB_POSTGRESQL_USERNAME}:{db_password}@{settings.DB_POSTGRESQL_HOST}:{settings.DB_POSTGRESQL_PORT}/{settings.DB_POSTGRESQL_DATABASE})"
|
||||
db_info = (
|
||||
f"PostgreSQL ({settings.DB_POSTGRESQL_USERNAME}:{db_password}@"
|
||||
f"{settings.DB_POSTGRESQL_TARGET}/{settings.DB_POSTGRESQL_DATABASE})"
|
||||
)
|
||||
|
||||
info_lines = [
|
||||
f"- 当前时间: {strftime('%Y-%m-%d %H:%M:%S')}",
|
||||
|
||||
@@ -10,7 +10,7 @@ import threading
|
||||
from asyncio import AbstractEventLoop
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Tuple, Type
|
||||
from urllib.parse import urlparse
|
||||
from urllib.parse import quote, urlencode, urlparse
|
||||
|
||||
from dotenv import set_key
|
||||
from pydantic import BaseModel, Field, ConfigDict, model_validator
|
||||
@@ -126,8 +126,8 @@ class ConfigModel(BaseModel):
|
||||
DB_SQLITE_MAX_OVERFLOW: int = 50
|
||||
# PostgreSQL 主机地址
|
||||
DB_POSTGRESQL_HOST: str = "localhost"
|
||||
# PostgreSQL 端口
|
||||
DB_POSTGRESQL_PORT: int = 5432
|
||||
# PostgreSQL 端口;使用 Unix Socket 时可留空
|
||||
DB_POSTGRESQL_PORT: str = "5432"
|
||||
# PostgreSQL 数据库名
|
||||
DB_POSTGRESQL_DATABASE: str = "moviepilot"
|
||||
# PostgreSQL 用户名
|
||||
@@ -142,7 +142,7 @@ class ConfigModel(BaseModel):
|
||||
# ==================== 缓存配置 ====================
|
||||
# 缓存类型,支持 cachetools 和 redis,默认使用 cachetools
|
||||
CACHE_BACKEND_TYPE: str = "cachetools"
|
||||
# 缓存连接字符串,仅外部缓存(如 Redis、Memcached)需要
|
||||
# 缓存连接字符串,仅外部缓存(如 Redis、Memcached)需要,支持 Redis Unix Socket URL
|
||||
CACHE_BACKEND_URL: Optional[str] = "redis://localhost:6379"
|
||||
# Redis 缓存最大内存限制,未配置时,如开启大内存模式时为 "1024mb",未开启时为 "256mb"
|
||||
CACHE_REDIS_MAXMEMORY: Optional[str] = None
|
||||
@@ -921,6 +921,39 @@ class Settings(BaseSettings, ConfigModel, LogConfigModel):
|
||||
}
|
||||
return None
|
||||
|
||||
@property
|
||||
def DB_POSTGRESQL_SOCKET_MODE(self) -> bool:
|
||||
host = (self.DB_POSTGRESQL_HOST or "").strip()
|
||||
return host.startswith("/")
|
||||
|
||||
@property
|
||||
def DB_POSTGRESQL_TARGET(self) -> str:
|
||||
if self.DB_POSTGRESQL_SOCKET_MODE:
|
||||
target = f"socket {self.DB_POSTGRESQL_HOST}"
|
||||
if self.DB_POSTGRESQL_PORT:
|
||||
target = f"{target} (port {self.DB_POSTGRESQL_PORT})"
|
||||
return target
|
||||
if self.DB_POSTGRESQL_PORT:
|
||||
return f"{self.DB_POSTGRESQL_HOST}:{self.DB_POSTGRESQL_PORT}"
|
||||
return self.DB_POSTGRESQL_HOST
|
||||
|
||||
def DB_POSTGRESQL_URL(self, driver: Optional[str] = None) -> str:
|
||||
scheme = "postgresql" if not driver else f"postgresql+{driver}"
|
||||
username = quote(str(self.DB_POSTGRESQL_USERNAME), safe="")
|
||||
database = quote(str(self.DB_POSTGRESQL_DATABASE), safe="")
|
||||
auth = username
|
||||
if self.DB_POSTGRESQL_PASSWORD:
|
||||
auth = f"{auth}:{quote(str(self.DB_POSTGRESQL_PASSWORD), safe='')}"
|
||||
|
||||
if self.DB_POSTGRESQL_SOCKET_MODE:
|
||||
query = {"host": self.DB_POSTGRESQL_HOST}
|
||||
if self.DB_POSTGRESQL_PORT:
|
||||
query["port"] = self.DB_POSTGRESQL_PORT
|
||||
return f"{scheme}://{auth}@/{database}?{urlencode(query)}"
|
||||
|
||||
port = f":{self.DB_POSTGRESQL_PORT}" if self.DB_POSTGRESQL_PORT else ""
|
||||
return f"{scheme}://{auth}@{self.DB_POSTGRESQL_HOST}{port}/{database}"
|
||||
|
||||
@property
|
||||
def PROXY_SERVER(self):
|
||||
if self.PROXY_HOST:
|
||||
|
||||
@@ -116,11 +116,7 @@ def _get_postgresql_engine(is_async: bool = False):
|
||||
"""
|
||||
获取PostgreSQL数据库引擎
|
||||
"""
|
||||
# 构建PostgreSQL连接URL
|
||||
if settings.DB_POSTGRESQL_PASSWORD:
|
||||
db_url = f"postgresql://{settings.DB_POSTGRESQL_USERNAME}:{settings.DB_POSTGRESQL_PASSWORD}@{settings.DB_POSTGRESQL_HOST}:{settings.DB_POSTGRESQL_PORT}/{settings.DB_POSTGRESQL_DATABASE}"
|
||||
else:
|
||||
db_url = f"postgresql://{settings.DB_POSTGRESQL_USERNAME}@{settings.DB_POSTGRESQL_HOST}:{settings.DB_POSTGRESQL_PORT}/{settings.DB_POSTGRESQL_DATABASE}"
|
||||
db_url = settings.DB_POSTGRESQL_URL()
|
||||
|
||||
# PostgreSQL连接参数
|
||||
_connect_args = {}
|
||||
@@ -150,12 +146,11 @@ def _get_postgresql_engine(is_async: bool = False):
|
||||
|
||||
# 创建数据库引擎
|
||||
engine = create_engine(**_db_kwargs)
|
||||
print(f"PostgreSQL database connected to {settings.DB_POSTGRESQL_HOST}:{settings.DB_POSTGRESQL_PORT}/{settings.DB_POSTGRESQL_DATABASE}")
|
||||
print(f"PostgreSQL database connected to {settings.DB_POSTGRESQL_TARGET}/{settings.DB_POSTGRESQL_DATABASE}")
|
||||
|
||||
return engine
|
||||
else:
|
||||
# 构建异步PostgreSQL连接URL
|
||||
async_db_url = f"postgresql+asyncpg://{settings.DB_POSTGRESQL_USERNAME}:{settings.DB_POSTGRESQL_PASSWORD}@{settings.DB_POSTGRESQL_HOST}:{settings.DB_POSTGRESQL_PORT}/{settings.DB_POSTGRESQL_DATABASE}"
|
||||
async_db_url = settings.DB_POSTGRESQL_URL("asyncpg")
|
||||
|
||||
# 数据库参数,只能使用 NullPool
|
||||
_db_kwargs = {
|
||||
@@ -168,7 +163,7 @@ def _get_postgresql_engine(is_async: bool = False):
|
||||
}
|
||||
# 创建异步数据库引擎
|
||||
async_engine = create_async_engine(**_db_kwargs)
|
||||
print(f"Async PostgreSQL database connected to {settings.DB_POSTGRESQL_HOST}:{settings.DB_POSTGRESQL_PORT}/{settings.DB_POSTGRESQL_DATABASE}")
|
||||
print(f"Async PostgreSQL database connected to {settings.DB_POSTGRESQL_TARGET}/{settings.DB_POSTGRESQL_DATABASE}")
|
||||
|
||||
return async_engine
|
||||
|
||||
|
||||
@@ -28,10 +28,7 @@ def update_db():
|
||||
|
||||
# 根据数据库类型设置不同的URL
|
||||
if settings.DB_TYPE.lower() == "postgresql":
|
||||
if settings.DB_POSTGRESQL_PASSWORD:
|
||||
db_url = f"postgresql://{settings.DB_POSTGRESQL_USERNAME}:{settings.DB_POSTGRESQL_PASSWORD}@{settings.DB_POSTGRESQL_HOST}:{settings.DB_POSTGRESQL_PORT}/{settings.DB_POSTGRESQL_DATABASE}"
|
||||
else:
|
||||
db_url = f"postgresql://{settings.DB_POSTGRESQL_USERNAME}@{settings.DB_POSTGRESQL_HOST}:{settings.DB_POSTGRESQL_PORT}/{settings.DB_POSTGRESQL_DATABASE}"
|
||||
db_url = settings.DB_POSTGRESQL_URL()
|
||||
else:
|
||||
db_location = settings.CONFIG_PATH / 'user.db'
|
||||
db_url = f"sqlite:///{db_location}"
|
||||
|
||||
@@ -24,7 +24,7 @@ DB_TYPE=postgresql
|
||||
# PostgreSQL 主机地址
|
||||
DB_POSTGRESQL_HOST=localhost
|
||||
|
||||
# PostgreSQL 端口
|
||||
# PostgreSQL 端口;使用 Unix Socket 时可留空
|
||||
DB_POSTGRESQL_PORT=5432
|
||||
|
||||
# PostgreSQL 数据库名
|
||||
@@ -43,6 +43,21 @@ DB_POSTGRESQL_POOL_SIZE=20
|
||||
DB_POSTGRESQL_MAX_OVERFLOW=30
|
||||
```
|
||||
|
||||
### 3. Unix Socket 连接
|
||||
|
||||
如果 PostgreSQL 通过 Unix Socket 暴露,可以把 `DB_POSTGRESQL_HOST` 设置为套接字目录。
|
||||
|
||||
```bash
|
||||
DB_TYPE=postgresql
|
||||
DB_POSTGRESQL_HOST=/var/run/postgresql
|
||||
DB_POSTGRESQL_PORT=
|
||||
DB_POSTGRESQL_DATABASE=moviepilot
|
||||
DB_POSTGRESQL_USERNAME=moviepilot
|
||||
DB_POSTGRESQL_PASSWORD=moviepilot
|
||||
```
|
||||
|
||||
如需显式指定 socket 端口,也可以保留 `DB_POSTGRESQL_PORT`,程序会生成带 `host=/path/to/socket` 查询参数的 PostgreSQL URL。
|
||||
|
||||
## Docker 部署
|
||||
|
||||
### 使用外部 PostgreSQL
|
||||
@@ -60,6 +75,13 @@ DB_POSTGRESQL_USERNAME=your-username
|
||||
DB_POSTGRESQL_PASSWORD=your-password
|
||||
```
|
||||
|
||||
使用 Redis Unix Socket 时,可直接设置 `CACHE_BACKEND_URL`,例如:
|
||||
|
||||
```bash
|
||||
CACHE_BACKEND_TYPE=redis
|
||||
CACHE_BACKEND_URL=unix:///var/run/redis/redis.sock?db=0
|
||||
```
|
||||
|
||||
## 数据迁移
|
||||
|
||||
### 从 SQLite 迁移到 PostgreSQL
|
||||
|
||||
101
tests/test_postgresql_socket_config.py
Normal file
101
tests/test_postgresql_socket_config.py
Normal file
@@ -0,0 +1,101 @@
|
||||
import sys
|
||||
import unittest
|
||||
from enum import Enum
|
||||
from types import ModuleType
|
||||
|
||||
|
||||
def _stub_module(name: str, **attrs):
|
||||
module = sys.modules.get(name)
|
||||
if module is None:
|
||||
module = ModuleType(name)
|
||||
sys.modules[name] = module
|
||||
for key, value in attrs.items():
|
||||
setattr(module, key, value)
|
||||
return module
|
||||
|
||||
|
||||
class _DummyLogger:
|
||||
def __getattr__(self, _name):
|
||||
return lambda *args, **kwargs: None
|
||||
|
||||
|
||||
_stub_module(
|
||||
"app.log",
|
||||
logger=_DummyLogger(),
|
||||
log_settings=_DummyLogger(),
|
||||
LogConfigModel=type("LogConfigModel", (), {}),
|
||||
)
|
||||
_stub_module("psutil")
|
||||
_schemas_module = _stub_module(
|
||||
"app.schemas", MediaType=Enum("MediaType", {"Movie": "Movie", "TV": "TV"})
|
||||
)
|
||||
_schemas_module.__getattr__ = lambda name: type(name, (), {})
|
||||
_stub_module("version", APP_VERSION="test")
|
||||
|
||||
|
||||
from app.core.config import Settings
|
||||
|
||||
|
||||
class PostgreSQLSocketConfigTests(unittest.TestCase):
|
||||
def test_postgresql_tcp_url_keeps_host_and_port(self):
|
||||
settings = Settings(
|
||||
DB_POSTGRESQL_HOST="db",
|
||||
DB_POSTGRESQL_PORT="5433",
|
||||
DB_POSTGRESQL_DATABASE="moviepilot",
|
||||
DB_POSTGRESQL_USERNAME="user",
|
||||
DB_POSTGRESQL_PASSWORD="pass",
|
||||
)
|
||||
|
||||
self.assertFalse(settings.DB_POSTGRESQL_SOCKET_MODE)
|
||||
self.assertEqual(
|
||||
settings.DB_POSTGRESQL_URL(),
|
||||
"postgresql://user:pass@db:5433/moviepilot",
|
||||
)
|
||||
self.assertEqual(
|
||||
settings.DB_POSTGRESQL_URL("asyncpg"),
|
||||
"postgresql+asyncpg://user:pass@db:5433/moviepilot",
|
||||
)
|
||||
self.assertEqual(settings.DB_POSTGRESQL_TARGET, "db:5433")
|
||||
|
||||
def test_postgresql_socket_url_uses_host_query_param(self):
|
||||
settings = Settings(
|
||||
DB_POSTGRESQL_HOST="/var/run/postgresql",
|
||||
DB_POSTGRESQL_PORT="",
|
||||
DB_POSTGRESQL_DATABASE="moviepilot",
|
||||
DB_POSTGRESQL_USERNAME="user",
|
||||
DB_POSTGRESQL_PASSWORD="pass",
|
||||
)
|
||||
|
||||
self.assertTrue(settings.DB_POSTGRESQL_SOCKET_MODE)
|
||||
self.assertIsNone(settings.DB_POSTGRESQL_PORT_VALUE)
|
||||
self.assertEqual(
|
||||
settings.DB_POSTGRESQL_URL(),
|
||||
"postgresql://user:pass@/moviepilot?host=%2Fvar%2Frun%2Fpostgresql",
|
||||
)
|
||||
self.assertEqual(
|
||||
settings.DB_POSTGRESQL_URL("asyncpg"),
|
||||
"postgresql+asyncpg://user:pass@/moviepilot?host=%2Fvar%2Frun%2Fpostgresql",
|
||||
)
|
||||
self.assertEqual(settings.DB_POSTGRESQL_TARGET, "socket /var/run/postgresql")
|
||||
|
||||
def test_postgresql_socket_url_can_keep_explicit_port(self):
|
||||
settings = Settings(
|
||||
DB_POSTGRESQL_HOST="/var/run/postgresql",
|
||||
DB_POSTGRESQL_PORT="5432",
|
||||
DB_POSTGRESQL_DATABASE="moviepilot",
|
||||
DB_POSTGRESQL_USERNAME="user",
|
||||
DB_POSTGRESQL_PASSWORD="",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
settings.DB_POSTGRESQL_URL(),
|
||||
"postgresql://user@/moviepilot?host=%2Fvar%2Frun%2Fpostgresql&port=5432",
|
||||
)
|
||||
self.assertEqual(
|
||||
settings.DB_POSTGRESQL_TARGET,
|
||||
"socket /var/run/postgresql (port 5432)",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user