mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-06 07:56:52 +08: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'})"
|
db_info = f"SQLite ({settings.CONFIG_PATH / 'db' / 'moviepilot.db'})"
|
||||||
else:
|
else:
|
||||||
db_password = settings.DB_POSTGRESQL_PASSWORD or ""
|
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 = [
|
info_lines = [
|
||||||
f"- 当前时间: {strftime('%Y-%m-%d %H:%M:%S')}",
|
f"- 当前时间: {strftime('%Y-%m-%d %H:%M:%S')}",
|
||||||
|
|||||||
+37
-4
@@ -10,7 +10,7 @@ import threading
|
|||||||
from asyncio import AbstractEventLoop
|
from asyncio import AbstractEventLoop
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional, Tuple, Type
|
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 dotenv import set_key
|
||||||
from pydantic import BaseModel, Field, ConfigDict, model_validator
|
from pydantic import BaseModel, Field, ConfigDict, model_validator
|
||||||
@@ -126,8 +126,8 @@ class ConfigModel(BaseModel):
|
|||||||
DB_SQLITE_MAX_OVERFLOW: int = 50
|
DB_SQLITE_MAX_OVERFLOW: int = 50
|
||||||
# PostgreSQL 主机地址
|
# PostgreSQL 主机地址
|
||||||
DB_POSTGRESQL_HOST: str = "localhost"
|
DB_POSTGRESQL_HOST: str = "localhost"
|
||||||
# PostgreSQL 端口
|
# PostgreSQL 端口;使用 Unix Socket 时可留空
|
||||||
DB_POSTGRESQL_PORT: int = 5432
|
DB_POSTGRESQL_PORT: str = "5432"
|
||||||
# PostgreSQL 数据库名
|
# PostgreSQL 数据库名
|
||||||
DB_POSTGRESQL_DATABASE: str = "moviepilot"
|
DB_POSTGRESQL_DATABASE: str = "moviepilot"
|
||||||
# PostgreSQL 用户名
|
# PostgreSQL 用户名
|
||||||
@@ -142,7 +142,7 @@ class ConfigModel(BaseModel):
|
|||||||
# ==================== 缓存配置 ====================
|
# ==================== 缓存配置 ====================
|
||||||
# 缓存类型,支持 cachetools 和 redis,默认使用 cachetools
|
# 缓存类型,支持 cachetools 和 redis,默认使用 cachetools
|
||||||
CACHE_BACKEND_TYPE: str = "cachetools"
|
CACHE_BACKEND_TYPE: str = "cachetools"
|
||||||
# 缓存连接字符串,仅外部缓存(如 Redis、Memcached)需要
|
# 缓存连接字符串,仅外部缓存(如 Redis、Memcached)需要,支持 Redis Unix Socket URL
|
||||||
CACHE_BACKEND_URL: Optional[str] = "redis://localhost:6379"
|
CACHE_BACKEND_URL: Optional[str] = "redis://localhost:6379"
|
||||||
# Redis 缓存最大内存限制,未配置时,如开启大内存模式时为 "1024mb",未开启时为 "256mb"
|
# Redis 缓存最大内存限制,未配置时,如开启大内存模式时为 "1024mb",未开启时为 "256mb"
|
||||||
CACHE_REDIS_MAXMEMORY: Optional[str] = None
|
CACHE_REDIS_MAXMEMORY: Optional[str] = None
|
||||||
@@ -921,6 +921,39 @@ class Settings(BaseSettings, ConfigModel, LogConfigModel):
|
|||||||
}
|
}
|
||||||
return None
|
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
|
@property
|
||||||
def PROXY_SERVER(self):
|
def PROXY_SERVER(self):
|
||||||
if self.PROXY_HOST:
|
if self.PROXY_HOST:
|
||||||
|
|||||||
+4
-9
@@ -116,11 +116,7 @@ def _get_postgresql_engine(is_async: bool = False):
|
|||||||
"""
|
"""
|
||||||
获取PostgreSQL数据库引擎
|
获取PostgreSQL数据库引擎
|
||||||
"""
|
"""
|
||||||
# 构建PostgreSQL连接URL
|
db_url = settings.DB_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}"
|
|
||||||
|
|
||||||
# PostgreSQL连接参数
|
# PostgreSQL连接参数
|
||||||
_connect_args = {}
|
_connect_args = {}
|
||||||
@@ -150,12 +146,11 @@ def _get_postgresql_engine(is_async: bool = False):
|
|||||||
|
|
||||||
# 创建数据库引擎
|
# 创建数据库引擎
|
||||||
engine = create_engine(**_db_kwargs)
|
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
|
return engine
|
||||||
else:
|
else:
|
||||||
# 构建异步PostgreSQL连接URL
|
async_db_url = settings.DB_POSTGRESQL_URL("asyncpg")
|
||||||
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}"
|
|
||||||
|
|
||||||
# 数据库参数,只能使用 NullPool
|
# 数据库参数,只能使用 NullPool
|
||||||
_db_kwargs = {
|
_db_kwargs = {
|
||||||
@@ -168,7 +163,7 @@ def _get_postgresql_engine(is_async: bool = False):
|
|||||||
}
|
}
|
||||||
# 创建异步数据库引擎
|
# 创建异步数据库引擎
|
||||||
async_engine = create_async_engine(**_db_kwargs)
|
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
|
return async_engine
|
||||||
|
|
||||||
|
|||||||
+1
-4
@@ -28,10 +28,7 @@ def update_db():
|
|||||||
|
|
||||||
# 根据数据库类型设置不同的URL
|
# 根据数据库类型设置不同的URL
|
||||||
if settings.DB_TYPE.lower() == "postgresql":
|
if settings.DB_TYPE.lower() == "postgresql":
|
||||||
if settings.DB_POSTGRESQL_PASSWORD:
|
db_url = settings.DB_POSTGRESQL_URL()
|
||||||
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}"
|
|
||||||
else:
|
else:
|
||||||
db_location = settings.CONFIG_PATH / 'user.db'
|
db_location = settings.CONFIG_PATH / 'user.db'
|
||||||
db_url = f"sqlite:///{db_location}"
|
db_url = f"sqlite:///{db_location}"
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ DB_TYPE=postgresql
|
|||||||
# PostgreSQL 主机地址
|
# PostgreSQL 主机地址
|
||||||
DB_POSTGRESQL_HOST=localhost
|
DB_POSTGRESQL_HOST=localhost
|
||||||
|
|
||||||
# PostgreSQL 端口
|
# PostgreSQL 端口;使用 Unix Socket 时可留空
|
||||||
DB_POSTGRESQL_PORT=5432
|
DB_POSTGRESQL_PORT=5432
|
||||||
|
|
||||||
# PostgreSQL 数据库名
|
# PostgreSQL 数据库名
|
||||||
@@ -43,6 +43,21 @@ DB_POSTGRESQL_POOL_SIZE=20
|
|||||||
DB_POSTGRESQL_MAX_OVERFLOW=30
|
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 部署
|
## Docker 部署
|
||||||
|
|
||||||
### 使用外部 PostgreSQL
|
### 使用外部 PostgreSQL
|
||||||
@@ -60,6 +75,13 @@ DB_POSTGRESQL_USERNAME=your-username
|
|||||||
DB_POSTGRESQL_PASSWORD=your-password
|
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
|
### 从 SQLite 迁移到 PostgreSQL
|
||||||
|
|||||||
@@ -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