"""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)