운영 NAS에서 DSM_HOST=https://192.168.x.x:5001 같은 LAN IP 사용 시 DSM의 self-signed 인증서가 IP 주소에 매칭되지 않아 SSL 검증 실패 (SSL: CERTIFICATE_VERIFY_FAILED — IP address mismatch). LAN 내부 통신이라 verify=False 허용 가능. 환경변수로 토글: - DSM_VERIFY_SSL=true (default) — 도메인 + 정상 cert 환경 - DSM_VERIFY_SSL=false — LAN IP + self-signed 환경 dsm_client.py가 환경변수 읽어 httpx.AsyncClient(verify=...)에 전달. docker-compose.yml + .env.example + CLAUDE.md에 신규 env 명시. 회귀 25/25 passing.
106 lines
3.4 KiB
Python
106 lines
3.4 KiB
Python
"""Synology DSM 7.x API 클라이언트.
|
|
|
|
각 호출 = login → 작업 → logout (세션 풀링은 v1.1+에서). 단일 컨테이너 + 동시성 낮음 가정.
|
|
|
|
- create_share_link(file_path, expires_in_sec) -> share URL
|
|
"""
|
|
import logging
|
|
import os
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
import httpx
|
|
|
|
logger = logging.getLogger("packs-lab.dsm")
|
|
|
|
DSM_HOST = os.getenv("DSM_HOST", "") # 예: https://gahusb.synology.me:5001
|
|
DSM_USER = os.getenv("DSM_USER", "")
|
|
DSM_PASS = os.getenv("DSM_PASS", "")
|
|
# LAN IP로 DSM 접근 시 self-signed cert가 IP에 매칭 안 되어 검증 실패. LAN 내부 통신이라 false 허용.
|
|
# 운영에서 LAN IP + self-signed면 DSM_VERIFY_SSL=false. 도메인 + 정상 cert면 기본값(true) 유지.
|
|
DSM_VERIFY_SSL = os.getenv("DSM_VERIFY_SSL", "true").strip().lower() != "false"
|
|
|
|
API_AUTH = "/webapi/auth.cgi"
|
|
API_SHARE = "/webapi/entry.cgi"
|
|
|
|
|
|
class DSMError(RuntimeError):
|
|
pass
|
|
|
|
|
|
async def _login(client: httpx.AsyncClient) -> str:
|
|
"""DSM 세션 sid 반환."""
|
|
if not all([DSM_HOST, DSM_USER, DSM_PASS]):
|
|
raise DSMError("DSM 환경변수 미설정")
|
|
r = await client.get(
|
|
f"{DSM_HOST}{API_AUTH}",
|
|
params={
|
|
"api": "SYNO.API.Auth",
|
|
"version": "7",
|
|
"method": "login",
|
|
"account": DSM_USER,
|
|
"passwd": DSM_PASS,
|
|
"session": "FileStation",
|
|
"format": "sid",
|
|
},
|
|
timeout=15.0,
|
|
)
|
|
r.raise_for_status()
|
|
data = r.json()
|
|
if not data.get("success"):
|
|
raise DSMError(f"DSM login 실패: {data.get('error')}")
|
|
return data["data"]["sid"]
|
|
|
|
|
|
async def _logout(client: httpx.AsyncClient, sid: str) -> None:
|
|
try:
|
|
await client.get(
|
|
f"{DSM_HOST}{API_AUTH}",
|
|
params={
|
|
"api": "SYNO.API.Auth",
|
|
"version": "7",
|
|
"method": "logout",
|
|
"session": "FileStation",
|
|
"_sid": sid,
|
|
},
|
|
timeout=10.0,
|
|
)
|
|
except Exception as e:
|
|
logger.warning("DSM logout 실패: %s", e)
|
|
|
|
|
|
async def create_share_link(file_path: str, expires_in_sec: int = 14400) -> tuple[str, datetime]:
|
|
"""파일 공유 링크 생성. 반환: (URL, expires_at).
|
|
|
|
file_path: NAS 절대경로 (예: /volume1/docker/webpage/media/packs/master/x.mp4)
|
|
expires_in_sec: 만료 (기본 4시간)
|
|
"""
|
|
expires_at = datetime.now(timezone.utc) + timedelta(seconds=expires_in_sec)
|
|
expire_time_ms = int(expires_at.timestamp() * 1000)
|
|
|
|
async with httpx.AsyncClient(verify=DSM_VERIFY_SSL) as client:
|
|
sid = await _login(client)
|
|
try:
|
|
r = await client.get(
|
|
f"{DSM_HOST}{API_SHARE}",
|
|
params={
|
|
"api": "SYNO.FileStation.Sharing",
|
|
"version": "3",
|
|
"method": "create",
|
|
"path": file_path,
|
|
"date_expired": expire_time_ms,
|
|
"_sid": sid,
|
|
},
|
|
timeout=15.0,
|
|
)
|
|
r.raise_for_status()
|
|
data = r.json()
|
|
if not data.get("success"):
|
|
raise DSMError(f"DSM Sharing.create 실패: {data.get('error')}")
|
|
links = data["data"]["links"]
|
|
if not links:
|
|
raise DSMError("Sharing 응답에 링크 없음")
|
|
url = links[0]["url"]
|
|
return url, expires_at
|
|
finally:
|
|
await _logout(client, sid)
|